agentsclimarketplace

Android architecture

Skill piyushverma0/android-agent-skills/skills/android-architecture

27 Android skills for AI agents (Claude Code, Codex, Cursor). Fixes Supabase auth, Hilt errors, design inconsistency, kapt→ksp, missing UiState states. Reduced my token bills 5×. FitGenZ AI shipped in 18 days.

Install
npx -y skills add piyushverma0/android-agent-skills --skill android-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 14 stars14 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

MVVM + Clean Architecture + Unidirectional Data Flow patterns for Android AI agents. Use this skill whenever designing app structure, creating ViewModels, Repositories, UseCases, data classes, domain models, UiState, UiEvent, package structure, separation of concerns, data layer, domain layer, presentation layer, dependency inversion, multi-module architecture, or any discussion of how to organize Android code. Always apply before writing any ViewModel, Repository, or UseCase. Triggers on: ViewModel, Repository, UseCase, UiState, architecture, data layer, domain layer, Clean Architecture, MVVM, MVI, package structure, module structure.

SKILL.md

9.6 KB, as published. Nobody here has run it

Android Architecture

MVVM + Clean Architecture with Unidirectional Data Flow. These patterns prevent the most common structural mistakes AI agents make when building Android apps.

The three layers — strict separation

Presentation (UI)          →  only depends on Domain
    ViewModel              →  calls UseCases, exposes UiState + Events
    Composables            →  observes ViewModel, sends user actions

Domain (Business Logic)    →  pure Kotlin, zero Android dependencies
    UseCases               →  orchestrate one business operation
    Repository interfaces  →  contracts, implemented in Data layer
    Domain models          →  pure data classes

Data                       →  implements Domain interfaces
    Repository impls       →  coordinate local + remote sources
    Remote DataSource      →  Retrofit API calls
    Local DataSource       →  Room DAO calls
    DTOs / Mappers         →  never expose DTOs to Domain

Rule: Domain layer must never import android.*. If it does, the architecture is broken.

Package structure

com.company.app/
├── di/                        ← Hilt modules only
│   ├── AppModule.kt
│   └── NetworkModule.kt
├── ui/
│   ├── theme/
│   ├── navigation/
│   │   └── AppNavGraph.kt
│   └── feature/
│       └── home/
│           ├── HomeScreen.kt      ← Composable
│           ├── HomeViewModel.kt
│           └── HomeUiState.kt
├── domain/
│   ├── model/
│   │   └── Item.kt               ← pure data class
│   ├── repository/
│   │   └── ItemRepository.kt     ← interface
│   └── usecase/
│       └── GetItemsUseCase.kt
└── data/
    ├── repository/
    │   └── ItemRepositoryImpl.kt
    ├── remote/
    │   ├── ItemApiService.kt
    │   └── dto/
    │       └── ItemDto.kt
    └── local/
        ├── ItemDao.kt
        └── entity/
            └── ItemEntity.kt

ViewModel — complete pattern

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getItems: GetItemsUseCase,
    private val toggleFavorite: ToggleFavoriteUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

    private val _events = MutableSharedFlow<HomeEvent>()
    val events: SharedFlow<HomeEvent> = _events.asSharedFlow()

    init {
        loadItems()
    }

    fun loadItems() {
        viewModelScope.launch {
            _uiState.value = HomeUiState.Loading
            getItems()
                .onSuccess { items ->
                    _uiState.value = if (items.isEmpty()) HomeUiState.Empty
                    else HomeUiState.Success(items)
                }
                .onFailure { error ->
                    _uiState.value = HomeUiState.Error(error.message ?: "Unknown error")
                }
        }
    }

    fun onItemClick(itemId: String) {
        viewModelScope.launch {
            _events.emit(HomeEvent.NavigateToDetail(itemId))
        }
    }

    fun onFavoriteClick(itemId: String) {
        viewModelScope.launch {
            toggleFavorite(itemId)
                .onFailure { _events.emit(HomeEvent.ShowError("Failed to update favorite")) }
        }
    }
}

UiState + UiEvent — define them together

sealed interface HomeUiState {
    data object Loading : HomeUiState
    data object Empty : HomeUiState
    data class Success(val items: List<Item>) : HomeUiState
    data class Error(val message: String) : HomeUiState
}

sealed interface HomeEvent {
    data class NavigateToDetail(val itemId: String) : HomeEvent
    data class ShowError(val message: String) : HomeEvent
    data object NavigateToCreate : HomeEvent
}

UseCase — single responsibility

// ✅ One UseCase = one business operation
class GetItemsUseCase @Inject constructor(
    private val repository: ItemRepository
) {
    suspend operator fun invoke(): Result<List<Item>> =
        repository.getItems()
}

// ✅ UseCases return Result<T> — never throw exceptions to ViewModel
class ToggleFavoriteUseCase @Inject constructor(
    private val repository: ItemRepository
) {
    suspend operator fun invoke(itemId: String): Result<Unit> =
        repository.toggleFavorite(itemId)
}

// ❌ UseCase doing too much — split into separate UseCases
class ItemUseCase @Inject constructor(val repo: ItemRepository) {
    suspend fun getItems() = repo.getItems()
    suspend fun createItem(item: Item) = repo.create(item)
    suspend fun deleteItem(id: String) = repo.delete(id)
    // This is a service, not a use case
}

Repository — interface in Domain, implementation in Data

// domain/repository/ItemRepository.kt
interface ItemRepository {
    suspend fun getItems(): Result<List<Item>>
    suspend fun getItem(id: String): Result<Item>
    suspend fun toggleFavorite(id: String): Result<Unit>
    fun getItemsStream(): Flow<List<Item>>  // for real-time updates
}

// data/repository/ItemRepositoryImpl.kt
class ItemRepositoryImpl @Inject constructor(
    private val remoteSource: ItemRemoteDataSource,
    private val localSource: ItemLocalDataSource,
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ItemRepository {

    override suspend fun getItems(): Result<List<Item>> = withContext(dispatcher) {
        runCatching {
            val remote = remoteSource.getItems()
            localSource.saveItems(remote.map { it.toEntity() })
            remote.map { it.toDomain() }
        }.recoverCatching {
            localSource.getItems().map { it.toDomain() }  // fallback to cache
        }
    }

    override fun getItemsStream(): Flow<List<Item>> =
        localSource.getItemsFlow().map { entities -> entities.map { it.toDomain() } }
}

Mappers — DTOs never cross the data layer boundary

// ✅ Map at the data layer boundary
fun ItemDto.toDomain() = Item(
    id = id,
    title = title,
    description = description,
    isFavorite = isFavorite
)

fun ItemEntity.toDomain() = Item(
    id = id,
    title = title,
    description = description,
    isFavorite = isFavorite
)

fun Item.toEntity() = ItemEntity(
    id = id,
    title = title,
    description = description,
    isFavorite = isFavorite
)

// ❌ Never expose DTOs or Entities to ViewModel or Composable
class HomeViewModel(val repo: ItemRepositoryImpl) {
    val items: StateFlow<List<ItemDto>> = ...  // DTO leaking into ViewModel!
}

Screen — ViewModel wiring

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = hiltViewModel(),
    navController: NavController
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        viewModel.events.collect { event ->
            when (event) {
                is HomeEvent.NavigateToDetail -> navController.navigate(DetailRoute(event.itemId))
                is HomeEvent.ShowError -> { /* show snackbar */ }
                is HomeEvent.NavigateToCreate -> navController.navigate(CreateRoute)
            }
        }
    }

    HomeContent(
        uiState = uiState,
        onItemClick = viewModel::onItemClick,
        onFavoriteClick = viewModel::onFavoriteClick,
        onRetry = viewModel::loadItems
    )
}

// ✅ Separate content composable — testable without ViewModel
@Composable
private fun HomeContent(
    uiState: HomeUiState,
    onItemClick: (String) -> Unit,
    onFavoriteClick: (String) -> Unit,
    onRetry: () -> Unit
) {
    when (uiState) {
        is HomeUiState.Loading -> LoadingScreen()
        is HomeUiState.Empty -> EmptyScreen()
        is HomeUiState.Success -> ItemList(uiState.items, onItemClick, onFavoriteClick)
        is HomeUiState.Error -> ErrorScreen(uiState.message, onRetry)
    }
}

Common Mistakes

❌ ViewModel calling Room DAO directly — always go through Repository ❌ Repository in Domain layer — interface in Domain, implementation in Data ❌ Domain model with @Entity or @SerialName annotations — pure Kotlin only ❌ ViewModel holding Context — use Application context via Hilt if needed ❌ StateFlow for navigation events — use SharedFlow ❌ Try/catch in ViewModel — handle in Repository, return Result<T> ❌ Multiple ViewModels per screen — one ViewModel per screen ❌ Business logic in Composable — always in ViewModel or UseCase

Deep-dive references

  • references/multi-module-arch.md — structuring feature and core modules
  • references/flow-patterns.md — stateIn, flatMapLatest, combine patterns

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.