agskills.dev
MARKETPLACE

android-architecture

Use when implementing MVVM, clean architecture, dependency injection with Hilt, or structuring Android app layers.

TheBushidoCollective9712

Aperçu

SKILL.md
Metadata
name
android-architecture
user-invocable
false
description
Use when implementing MVVM, clean architecture, dependency injection with Hilt, or structuring Android app layers.

Android - Architecture

Modern Android architecture patterns following Google's recommended practices.

Key Concepts

MVVM Architecture

Model-View-ViewModel separates UI from business logic:

// UI State data class UserUiState( val user: User? = null, val isLoading: Boolean = false, val error: String? = null ) // ViewModel class UserViewModel( private val userRepository: UserRepository ) : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow<UserUiState> = _uiState.asStateFlow() fun loadUser(userId: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) } userRepository.getUser(userId) .onSuccess { user -> _uiState.update { it.copy(user = user, isLoading = false) } } .onFailure { error -> _uiState.update { it.copy(error = error.message, isLoading = false) } } } } } // Composable @Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> LoadingIndicator() uiState.error != null -> ErrorMessage(uiState.error!!) uiState.user != null -> UserContent(uiState.user!!) } }

Clean Architecture Layers

app/
├── data/
│   ├── local/           # Room database, DataStore
│   │   ├── dao/
│   │   └── entities/
│   ├── remote/          # Retrofit, network
│   │   ├── api/
│   │   └── dto/
│   └── repository/      # Repository implementations
├── domain/
│   ├── model/           # Domain models
│   ├── repository/      # Repository interfaces
│   └── usecase/         # Business logic
└── presentation/
    ├── ui/              # Composables
    └── viewmodel/       # ViewModels

Repository Pattern

// Domain layer - interface interface UserRepository { fun getUser(id: String): Flow<User> suspend fun saveUser(user: User): Result<Unit> suspend fun deleteUser(id: String): Result<Unit> } // Data layer - implementation class UserRepositoryImpl( private val userApi: UserApi, private val userDao: UserDao ) : UserRepository { override fun getUser(id: String): Flow<User> = flow { // Emit cached data first userDao.getUser(id)?.let { emit(it.toDomain()) } // Fetch fresh data try { val remoteUser = userApi.getUser(id) userDao.insertUser(remoteUser.toEntity()) emit(remoteUser.toDomain()) } catch (e: Exception) { // Network error, cached data already emitted } } override suspend fun saveUser(user: User): Result<Unit> = runCatching { userApi.updateUser(user.toDto()) userDao.insertUser(user.toEntity()) } }

Best Practices

Dependency Injection with Hilt

// Module definition @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl(BuildConfig.API_BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .build() } @Provides @Singleton fun provideUserApi(retrofit: Retrofit): UserApi { return retrofit.create(UserApi::class.java) } } @Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { @Binds @Singleton abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository } // ViewModel injection @HiltViewModel class UserViewModel @Inject constructor( private val getUserUseCase: GetUserUseCase, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val userId: String = savedStateHandle.get<String>("userId") ?: throw IllegalArgumentException("userId required") // ViewModel implementation }

Use Cases for Business Logic

class GetUserUseCase @Inject constructor( private val userRepository: UserRepository, private val analyticsTracker: AnalyticsTracker ) { operator fun invoke(userId: String): Flow<Result<User>> = flow { emit(Result.Loading) userRepository.getUser(userId) .catch { e -> analyticsTracker.trackError("get_user_failed", e) emit(Result.Error(e)) } .collect { user -> emit(Result.Success(user)) } } } // Sealed class for results sealed class Result<out T> { data class Success<T>(val data: T) : Result<T>() data class Error(val exception: Throwable) : Result<Nothing>() object Loading : Result<Nothing>() }

Room Database Setup

@Entity(tableName = "users") data class UserEntity( @PrimaryKey val id: String, val name: String, val email: String, @ColumnInfo(name = "created_at") val createdAt: Long ) @Dao interface UserDao { @Query("SELECT * FROM users WHERE id = :id") suspend fun getUser(id: String): UserEntity? @Query("SELECT * FROM users ORDER BY name ASC") fun getAllUsers(): Flow<List<UserEntity>> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: UserEntity) @Delete suspend fun deleteUser(user: UserEntity) } @Database(entities = [UserEntity::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } // Hilt module @Module @InstallIn(SingletonComponent::class) object DatabaseModule { @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder( context, AppDatabase::class.java, "app_database" ).build() } @Provides fun provideUserDao(database: AppDatabase): UserDao { return database.userDao() } }

Data Mapping

// DTO (Data Transfer Object) - from API data class UserDto( @Json(name = "id") val id: String, @Json(name = "full_name") val fullName: String, @Json(name = "email_address") val email: String ) // Entity - for Room @Entity(tableName = "users") data class UserEntity( @PrimaryKey val id: String, val name: String, val email: String ) // Domain model data class User( val id: String, val name: String, val email: String ) // Mappers fun UserDto.toEntity() = UserEntity( id = id, name = fullName, email = email ) fun UserDto.toDomain() = User( id = id, name = fullName, email = email ) fun UserEntity.toDomain() = User( id = id, name = name, email = email ) fun User.toEntity() = UserEntity( id = id, name = name, email = email )

Common Patterns

Single Source of Truth

class OfflineFirstRepository @Inject constructor( private val api: ItemApi, private val dao: ItemDao ) : ItemRepository { override fun getItems(): Flow<List<Item>> { return dao.getAllItems() .map { entities -> entities.map { it.toDomain() } } .onStart { // Refresh from network in background refreshItems() } } private suspend fun refreshItems() { try { val remoteItems = api.getItems() dao.deleteAll() dao.insertAll(remoteItems.map { it.toEntity() }) } catch (e: Exception) { // Log error, local data still available } } }

Navigation with Type-Safe Args

// Define routes sealed class Screen(val route: String) { object Home : Screen("home") object Detail : Screen("detail/{itemId}") { fun createRoute(itemId: String) = "detail/$itemId" } object Settings : Screen("settings") } // Navigation setup @Composable fun AppNavigation(navController: NavHostController) { NavHost(navController = navController, startDestination = Screen.Home.route) { composable(Screen.Home.route) { HomeScreen( onItemClick = { itemId -> navController.navigate(Screen.Detail.createRoute(itemId)) } ) } composable( route = Screen.Detail.route, arguments = listOf(navArgument("itemId") { type = NavType.StringType }) ) { backStackEntry -> DetailScreen( itemId = backStackEntry.arguments?.getString("itemId") ?: return@composable ) } } }

Error Handling

sealed class UiState<out T> { object Loading : UiState<Nothing>() data class Success<T>(val data: T) : UiState<T>() data class Error(val message: String, val retry: (() -> Unit)? = null) : UiState<Nothing>() } @Composable fun <T> StateHandler( state: UiState<T>, onRetry: () -> Unit = {}, content: @Composable (T) -> Unit ) { when (state) { is UiState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } is UiState.Error -> { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text(state.message) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetry) { Text("Retry") } } } is UiState.Success -> content(state.data) } }

Anti-Patterns

God Activity/Fragment

Bad: All logic in one Activity.

Good: Use MVVM with clear separation of concerns.

Network Calls in ViewModel

Bad:

class BadViewModel : ViewModel() { fun loadData() { val client = OkHttpClient() // Direct network dependency // ... } }

Good: Inject repository through constructor.

Exposing Mutable State

Bad:

class BadViewModel : ViewModel() { val uiState = MutableStateFlow(UiState()) // Mutable exposed! }

Good:

class GoodViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow() }

Related Skills

  • android-jetpack-compose: UI layer patterns
  • android-kotlin-coroutines: Async operations