Spaces:
Sleeping
Sleeping
| # Android Developer Markdown Brain | |
| > Used by AI pipeline to plan, generate, and validate Android apps (Jetpack Compose, XML, SDK APIs). | |
| --- | |
| ## Project Structure (Compose) | |
| ``` | |
| app/ | |
| ├── src/main/ | |
| │ ├── AndroidManifest.xml | |
| │ ├── java/com/dolor3v/app/ | |
| │ │ ├── MainActivity.kt | |
| │ │ ├── ui/ | |
| │ │ │ ├── theme/ | |
| │ │ │ │ ├── Color.kt | |
| │ │ │ │ ├── Theme.kt | |
| │ │ │ │ └── Type.kt | |
| │ │ │ ├── screens/ | |
| │ │ │ │ ├── HomeScreen.kt | |
| │ │ │ │ └── DetailScreen.kt | |
| │ │ │ └── components/ | |
| │ │ ├── data/ | |
| │ │ │ ├── repository/ | |
| │ │ │ ├── model/ | |
| │ │ │ └── api/ | |
| │ │ ├── domain/ | |
| │ │ │ └── usecase/ | |
| │ │ └── di/ | |
| │ └── res/ | |
| │ ├── drawable/ | |
| │ ├── values/ | |
| │ │ ├── strings.xml | |
| │ │ ├── colors.xml | |
| │ │ └── themes.xml | |
| │ └── mipmap/ | |
| ├── build.gradle.kts | |
| └── proguard-rules.pro | |
| build.gradle.kts (project level) | |
| settings.gradle.kts | |
| gradle/libs.versions.toml | |
| ``` | |
| --- | |
| ## build.gradle.kts (App Level) | |
| ```kotlin | |
| plugins { | |
| alias(libs.plugins.android.application) | |
| alias(libs.plugins.kotlin.android) | |
| alias(libs.plugins.kotlin.compose) | |
| alias(libs.plugins.hilt) | |
| id("kotlin-kapt") | |
| } | |
| android { | |
| namespace = "com.dolor3v.app" | |
| compileSdk = 35 | |
| defaultConfig { | |
| applicationId = "com.dolor3v.app" | |
| minSdk = 26 | |
| targetSdk = 35 | |
| versionCode = 1 | |
| versionName = "1.0.0" | |
| testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
| } | |
| buildTypes { | |
| release { | |
| isMinifyEnabled = true | |
| isShrinkResources = true | |
| proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") | |
| signingConfig = signingConfigs.getByName("release") | |
| } | |
| debug { | |
| applicationIdSuffix = ".debug" | |
| isDebuggable = true | |
| } | |
| } | |
| compileOptions { | |
| sourceCompatibility = JavaVersion.VERSION_17 | |
| targetCompatibility = JavaVersion.VERSION_17 | |
| } | |
| kotlinOptions { jvmTarget = "17" } | |
| buildFeatures { | |
| compose = true | |
| buildConfig = true | |
| } | |
| } | |
| dependencies { | |
| // Core | |
| implementation(libs.androidx.core.ktx) | |
| implementation(libs.androidx.lifecycle.runtime.ktx) | |
| implementation(libs.androidx.activity.compose) | |
| // Compose BOM | |
| implementation(platform(libs.androidx.compose.bom)) | |
| implementation(libs.androidx.ui) | |
| implementation(libs.androidx.ui.graphics) | |
| implementation(libs.androidx.ui.tooling.preview) | |
| implementation(libs.androidx.material3) | |
| // Navigation | |
| implementation(libs.androidx.navigation.compose) | |
| // ViewModel | |
| implementation(libs.androidx.lifecycle.viewmodel.compose) | |
| // Hilt DI | |
| implementation(libs.hilt.android) | |
| kapt(libs.hilt.compiler) | |
| implementation(libs.androidx.hilt.navigation.compose) | |
| // Retrofit + OkHttp | |
| implementation(libs.retrofit) | |
| implementation(libs.retrofit.converter.gson) | |
| implementation(libs.okhttp.logging.interceptor) | |
| // Room | |
| implementation(libs.room.runtime) | |
| implementation(libs.room.ktx) | |
| kapt(libs.room.compiler) | |
| // Coil (images) | |
| implementation(libs.coil.compose) | |
| // DataStore | |
| implementation(libs.datastore.preferences) | |
| // Coroutines | |
| implementation(libs.kotlinx.coroutines.android) | |
| // Testing | |
| testImplementation(libs.junit) | |
| androidTestImplementation(libs.androidx.junit) | |
| androidTestImplementation(libs.androidx.espresso.core) | |
| androidTestImplementation(platform(libs.androidx.compose.bom)) | |
| androidTestImplementation(libs.androidx.ui.test.junit4) | |
| debugImplementation(libs.androidx.ui.tooling) | |
| } | |
| ``` | |
| --- | |
| ## Jetpack Compose Patterns | |
| ### MainActivity | |
| ```kotlin | |
| @AndroidEntryPoint | |
| class MainActivity : ComponentActivity() { | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| enableEdgeToEdge() | |
| setContent { | |
| MyAppTheme { | |
| Surface( | |
| modifier = Modifier.fillMaxSize(), | |
| color = MaterialTheme.colorScheme.background | |
| ) { | |
| AppNavHost() | |
| } | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| ### Navigation | |
| ```kotlin | |
| @Composable | |
| fun AppNavHost( | |
| navController: NavHostController = rememberNavController(), | |
| startDestination: String = Screen.Home.route | |
| ) { | |
| NavHost(navController = navController, startDestination = startDestination) { | |
| composable(Screen.Home.route) { | |
| HomeScreen(navController = navController) | |
| } | |
| composable( | |
| route = Screen.Detail.route, | |
| arguments = listOf(navArgument("id") { type = NavType.IntType }) | |
| ) { backStack -> | |
| val id = backStack.arguments?.getInt("id") ?: 0 | |
| DetailScreen(id = id, navController = navController) | |
| } | |
| } | |
| } | |
| sealed class Screen(val route: String) { | |
| object Home : Screen("home") | |
| object Detail : Screen("detail/{id}") { | |
| fun createRoute(id: Int) = "detail/$id" | |
| } | |
| } | |
| ``` | |
| ### ViewModel + StateFlow | |
| ```kotlin | |
| @HiltViewModel | |
| class HomeViewModel @Inject constructor( | |
| private val repository: PostRepository | |
| ) : ViewModel() { | |
| private val _uiState = MutableStateFlow<UiState<List<Post>>>(UiState.Loading) | |
| val uiState: StateFlow<UiState<List<Post>>> = _uiState.asStateFlow() | |
| init { loadPosts() } | |
| fun loadPosts() { | |
| viewModelScope.launch { | |
| _uiState.value = UiState.Loading | |
| repository.getPosts() | |
| .onSuccess { _uiState.value = UiState.Success(it) } | |
| .onFailure { _uiState.value = UiState.Error(it.message ?: "Unknown error") } | |
| } | |
| } | |
| } | |
| sealed class UiState<out T> { | |
| object Loading : UiState<Nothing>() | |
| data class Success<T>(val data: T) : UiState<T>() | |
| data class Error(val message: String) : UiState<Nothing>() | |
| } | |
| ``` | |
| ### Screen with State Handling | |
| ```kotlin | |
| @Composable | |
| fun HomeScreen( | |
| viewModel: HomeViewModel = hiltViewModel(), | |
| navController: NavController | |
| ) { | |
| val uiState by viewModel.uiState.collectAsStateWithLifecycle() | |
| Scaffold( | |
| topBar = { | |
| TopAppBar(title = { Text("Home") }) | |
| } | |
| ) { padding -> | |
| Box(modifier = Modifier.padding(padding).fillMaxSize()) { | |
| when (val state = uiState) { | |
| is UiState.Loading -> CircularProgressIndicator(Modifier.align(Alignment.Center)) | |
| is UiState.Error -> Text(state.message, color = MaterialTheme.colorScheme.error) | |
| is UiState.Success -> PostList(posts = state.data, onPostClick = { id -> | |
| navController.navigate(Screen.Detail.createRoute(id)) | |
| }) | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| ### LazyColumn (RecyclerView equivalent) | |
| ```kotlin | |
| @Composable | |
| fun PostList(posts: List<Post>, onPostClick: (Int) -> Unit) { | |
| LazyColumn( | |
| contentPadding = PaddingValues(16.dp), | |
| verticalArrangement = Arrangement.spacedBy(8.dp) | |
| ) { | |
| items(posts, key = { it.id }) { post -> | |
| PostCard(post = post, onClick = { onPostClick(post.id) }) | |
| } | |
| } | |
| } | |
| @Composable | |
| fun PostCard(post: Post, onClick: () -> Unit) { | |
| Card( | |
| onClick = onClick, | |
| modifier = Modifier.fillMaxWidth(), | |
| elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) | |
| ) { | |
| Column(modifier = Modifier.padding(16.dp)) { | |
| Text(post.title, style = MaterialTheme.typography.titleMedium) | |
| Spacer(Modifier.height(4.dp)) | |
| Text(post.excerpt, style = MaterialTheme.typography.bodySmall, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant, | |
| maxLines = 2, overflow = TextOverflow.Ellipsis) | |
| } | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Room Database | |
| ### Entity | |
| ```kotlin | |
| @Entity(tableName = "posts") | |
| data class PostEntity( | |
| @PrimaryKey val id: Int, | |
| @ColumnInfo(name = "title") val title: String, | |
| @ColumnInfo(name = "content") val content: String, | |
| @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() | |
| ) | |
| ``` | |
| ### DAO | |
| ```kotlin | |
| @Dao | |
| interface PostDao { | |
| @Query("SELECT * FROM posts ORDER BY created_at DESC") | |
| fun getAllPosts(): Flow<List<PostEntity>> | |
| @Query("SELECT * FROM posts WHERE id = :id") | |
| suspend fun getPostById(id: Int): PostEntity? | |
| @Insert(onConflict = OnConflictStrategy.REPLACE) | |
| suspend fun insertPosts(posts: List<PostEntity>) | |
| @Delete | |
| suspend fun deletePost(post: PostEntity) | |
| @Query("DELETE FROM posts") | |
| suspend fun clearAll() | |
| } | |
| ``` | |
| ### Database | |
| ```kotlin | |
| @Database(entities = [PostEntity::class], version = 1, exportSchema = true) | |
| @TypeConverters(Converters::class) | |
| abstract class AppDatabase : RoomDatabase() { | |
| abstract fun postDao(): PostDao | |
| companion object { | |
| @Volatile private var INSTANCE: AppDatabase? = null | |
| fun getInstance(context: Context): AppDatabase = | |
| INSTANCE ?: synchronized(this) { | |
| Room.databaseBuilder(context, AppDatabase::class.java, "app.db") | |
| .fallbackToDestructiveMigration() | |
| .build().also { INSTANCE = it } | |
| } | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Retrofit API Service | |
| ```kotlin | |
| interface ApiService { | |
| @GET("posts") | |
| suspend fun getPosts( | |
| @Query("page") page: Int = 1, | |
| @Query("per_page") perPage: Int = 20 | |
| ): Response<List<PostDto>> | |
| @GET("posts/{id}") | |
| suspend fun getPost(@Path("id") id: Int): Response<PostDto> | |
| @POST("posts") | |
| suspend fun createPost(@Body post: CreatePostDto): Response<PostDto> | |
| @Multipart | |
| @POST("media") | |
| suspend fun uploadMedia( | |
| @Part file: MultipartBody.Part, | |
| @Part("title") title: RequestBody | |
| ): Response<MediaDto> | |
| } | |
| // Retrofit instance | |
| @Provides @Singleton | |
| fun provideRetrofit(): Retrofit = Retrofit.Builder() | |
| .baseUrl(BuildConfig.API_BASE_URL) | |
| .addConverterFactory(GsonConverterFactory.create()) | |
| .client( | |
| OkHttpClient.Builder() | |
| .addInterceptor(HttpLoggingInterceptor().apply { | |
| level = HttpLoggingInterceptor.Level.BODY | |
| }) | |
| .addInterceptor { chain -> | |
| chain.proceed( | |
| chain.request().newBuilder() | |
| .addHeader("Authorization", "Bearer ${BuildConfig.API_TOKEN}") | |
| .build() | |
| ) | |
| } | |
| .connectTimeout(30, TimeUnit.SECONDS) | |
| .readTimeout(30, TimeUnit.SECONDS) | |
| .build() | |
| ) | |
| .build() | |
| ``` | |
| --- | |
| ## AndroidManifest.xml | |
| ```xml | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |
| <!-- Permissions --> | |
| <uses-permission android:name="android.permission.INTERNET" /> | |
| <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
| <uses-permission android:name="android.permission.CAMERA" /> | |
| <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> | |
| <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | |
| <application | |
| android:name=".MyApplication" | |
| android:allowBackup="true" | |
| android:icon="@mipmap/ic_launcher" | |
| android:label="@string/app_name" | |
| android:roundIcon="@mipmap/ic_launcher_round" | |
| android:supportsRtl="true" | |
| android:theme="@style/Theme.MyApp" | |
| android:usesCleartextTraffic="false"> | |
| <activity | |
| android:name=".MainActivity" | |
| android:exported="true" | |
| android:windowSoftInputMode="adjustResize"> | |
| <intent-filter> | |
| <action android:name="android.intent.action.MAIN" /> | |
| <category android:name="android.intent.category.LAUNCHER" /> | |
| </intent-filter> | |
| </activity> | |
| <!-- Deep links --> | |
| <activity android:name=".DeepLinkActivity"> | |
| <intent-filter android:autoVerify="true"> | |
| <action android:name="android.intent.action.VIEW" /> | |
| <category android:name="android.intent.category.DEFAULT" /> | |
| <category android:name="android.intent.category.BROWSABLE" /> | |
| <data android:scheme="https" android:host="dolor3v.com" /> | |
| </intent-filter> | |
| </activity> | |
| <!-- FCM --> | |
| <service android:name=".service.MyFirebaseMessagingService" | |
| android:exported="false"> | |
| <intent-filter> | |
| <action android:name="com.google.firebase.MESSAGING_EVENT" /> | |
| </intent-filter> | |
| </service> | |
| </application> | |
| </manifest> | |
| ``` | |
| --- | |
| ## Material 3 Theme | |
| ```kotlin | |
| // Color.kt | |
| val BlueElectric = Color(0xFF3B82F6) | |
| val AmberSpark = Color(0xFFF59E0B) | |
| val DeepSpace = Color(0xFF090E1A) | |
| val SpaceSurface = Color(0xFF0F1629) | |
| private val DarkColorScheme = darkColorScheme( | |
| primary = BlueElectric, | |
| secondary = AmberSpark, | |
| background = DeepSpace, | |
| surface = SpaceSurface, | |
| onPrimary = Color.White, | |
| onBackground = Color(0xFFE2E8F0), | |
| onSurface = Color(0xFF94A3B8), | |
| ) | |
| @Composable | |
| fun MyAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { | |
| val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme | |
| MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) | |
| } | |
| ``` | |