Android Clean Architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories…

You say
Install this skill Read the source first Free Written by affaan-m · unverified publisher
Context cost
2.2k tokensestimated from the bundle, loaded when it triggers
Bundle
1 file · 8.9 kBtext throughout, nothing executable
Licence
MITfree to use
Last change
no release on file
Servers it uses
Noneruns standalone

What it does

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

Installed, it changes the agent in these ways.

What this skill changes about the agent is not written down here yet. The listing was collected from its source, and the description is in its own SKILL.md.

Expertise

Domain judgement the base model does not have.

mobilekotlinandroid

The skill itself

This is the whole product. A skill is instructions the model reads, so there is nothing behind the listing you cannot see first — the front matter loads with every session, and the body below it loads when the skill triggers.

SKILL.md8.9 kB · 341 lines
--- name: android-clean-architecture description: Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project. metadata: origin: ECC ---
8# Android Clean Architecture
9
10Clean Architecture patterns for Android and KMP projects. Covers module boundaries, dependency inversion, UseCase/Repository patterns, and data layer design with Room, SQLDelight, and Ktor.
11
12## When to Activate
13
14- Structuring Android or KMP project modules
15- Implementing UseCases, Repositories, or DataSources
16- Designing data flow between layers (domain, data, presentation)
17- Setting up dependency injection with Koin or Hilt
18- Working with Room, SQLDelight, or Ktor in a layered architecture
19
20## Module Structure
21
22### Recommended Layout
23
24```
25project/
26├── app/ # Android entry point, DI wiring, Application class
27├── core/ # Shared utilities, base classes, error types
28├── domain/ # UseCases, domain models, repository interfaces (pure Kotlin)
29├── data/ # Repository implementations, DataSources, DB, network
30├── presentation/ # Screens, ViewModels, UI models, navigation
31├── design-system/ # Reusable Compose components, theme, typography
32└── feature/ # Feature modules (optional, for larger projects)
33 ├── auth/
34 ├── settings/
35 └── profile/
36```
37
38### Dependency Rules
39
40```
41app → presentation, domain, data, core
42presentation → domain, design-system, core
43data → domain, core
44domain → core (or no dependencies)
45core → (nothing)
46```
47
48**Critical**: domain must NEVER depend on data, presentation, or any framework. It contains pure Kotlin only.
49
50## Domain Layer
51
52### UseCase Pattern
53
54Each UseCase represents one business operation. Use operator fun invoke for clean call sites:
55
56```kotlin
57class GetItemsByCategoryUseCase(
58 private val repository: ItemRepository
59) {
60 suspend operator fun invoke(category: String): Result<List<Item>> {
61 return repository.getItemsByCategory(category)
62 }
63}
64
65// Flow-based UseCase for reactive streams
66class ObserveUserProgressUseCase(
67 private val repository: UserRepository
68) {
69 operator fun invoke(userId: String): Flow<UserProgress> {
70 return repository.observeProgress(userId)
71 }
72}
73```
74
75### Domain Models
76
77Domain models are plain Kotlin data classes — no framework annotations:
78
79```kotlin
80data class Item(
81 val id: String,
82 val title: String,
83 val description: String,
84 val tags: List<String>,
85 val status: Status,
86 val category: String
87)
88
89enum class Status { DRAFT, ACTIVE, ARCHIVED }
90```
91
92### Repository Interfaces
93
94Defined in domain, implemented in data:
95
96```kotlin
97interface ItemRepository {
98 suspend fun getItemsByCategory(category: String): Result<List<Item>>
99 suspend fun saveItem(item: Item): Result<Unit>
100 fun observeItems(): Flow<List<Item>>
101}
102```
103
104## Data Layer
105
106### Repository Implementation
107
108Coordinates between local and remote data sources:
109
110```kotlin
111class ItemRepositoryImpl(
112 private val localDataSource: ItemLocalDataSource,
113 private val remoteDataSource: ItemRemoteDataSource
114) : ItemRepository {
115
116 override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
117 return runCatching {
118 val remote = remoteDataSource.fetchItems(category)
119 localDataSource.insertItems(remote.map { it.toEntity() })
120 localDataSource.getItemsByCategory(category).map { it.toDomain() }
121 }
122 }
123
124 override suspend fun saveItem(item: Item): Result<Unit> {
125 return runCatching {
126 localDataSource.insertItems(listOf(item.toEntity()))
127 }
128 }
129
130 override fun observeItems(): Flow<List<Item>> {
131 return localDataSource.observeAll().map { entities ->
132 entities.map { it.toDomain() }
133 }
134 }
135}
136```
137
138### Mapper Pattern
139
140Keep mappers as extension functions near the data models:
141
142```kotlin
143// In data layer
144fun ItemEntity.toDomain() = Item(
145 id = id,
146 title = title,
147 description = description,
148 tags = tags.split("|"),
149 status = Status.valueOf(status),
150 category = category
151)
152
153fun ItemDto.toEntity() = ItemEntity(
154 id = id,
155 title = title,
156 description = description,
157 tags = tags.joinToString("|"),
158 status = status,
159 category = category
160)
161```
162
163### Room Database (Android)
164
165```kotlin
166@Entity(tableName = "items")
167data class ItemEntity(
168 @PrimaryKey val id: String,
169 val title: String,
170 val description: String,
171 val tags: String,
172 val status: String,
173 val category: String
174)
175
176@Dao
177interface ItemDao {
178 @Query("SELECT * FROM items WHERE category = :category")
179 suspend fun getByCategory(category: String): List<ItemEntity>
180
181 @Upsert
182 suspend fun upsert(items: List<ItemEntity>)
183
184 @Query("SELECT * FROM items")
185 fun observeAll(): Flow<List<ItemEntity>>
186}
187```
188
189### SQLDelight (KMP)
190
191```sql
192-- Item.sq
193CREATE TABLE ItemEntity (
194 id TEXT NOT NULL PRIMARY KEY,
195 title TEXT NOT NULL,
196 description TEXT NOT NULL,
197 tags TEXT NOT NULL,
198 status TEXT NOT NULL,
199 category TEXT NOT NULL
200);
201
202getByCategory:
203SELECT * FROM ItemEntity WHERE category = ?;
204
205upsert:
206INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category)
207VALUES (?, ?, ?, ?, ?, ?);
208
209observeAll:
210SELECT * FROM ItemEntity;
211```
212
213### Ktor Network Client (KMP)
214
215```kotlin
216class ItemRemoteDataSource(private val client: HttpClient) {
217
218 suspend fun fetchItems(category: String): List<ItemDto> {
219 return client.get("api/items") {
220 parameter("category", category)
221 }.body()
222 }
223}
224
225// HttpClient setup with content negotiation
226val httpClient = HttpClient {
227 install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
228 install(Logging) { level = LogLevel.HEADERS }
229 defaultRequest { url("https://api.example.com/") }
230}
231```
232
233## Dependency Injection
234
235### Koin (KMP-friendly)
236
237```kotlin
238// Domain module
239val domainModule = module {
240 factory { GetItemsByCategoryUseCase(get()) }
241 factory { ObserveUserProgressUseCase(get()) }
242}
243
244// Data module
245val dataModule = module {
246 single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
247 single { ItemLocalDataSource(get()) }
248 single { ItemRemoteDataSource(get()) }
249}
250
251// Presentation module
252val presentationModule = module {
253 viewModelOf(::ItemListViewModel)
254 viewModelOf(::DashboardViewModel)
255}
256```
257
258### Hilt (Android-only)
259
260```kotlin
261@Module
262@InstallIn(SingletonComponent::class)
263abstract class RepositoryModule {
264 @Binds
265 abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
266}
267
268@HiltViewModel
269class ItemListViewModel @Inject constructor(
270 private val getItems: GetItemsByCategoryUseCase
271) : ViewModel()
272```
273
274## Error Handling
275
276### Result/Try Pattern
277
278Use Result<T> or a custom sealed type for error propagation:
279
280```kotlin
281sealed interface Try<out T> {
282 data class Success<T>(val value: T) : Try<T>
283 data class Failure(val error: AppError) : Try<Nothing>
284}
285
286sealed interface AppError {
287 data class Network(val message: String) : AppError
288 data class Database(val message: String) : AppError
289 data object Unauthorized : AppError
290}
291
292// In ViewModel — map to UI state
293viewModelScope.launch {
294 when (val result = getItems(category)) {
295 is Try.Success -> _state.update { it.copy(items = result.value, isLoading = false) }
296 is Try.Failure -> _state.update { it.copy(error = result.error.toMessage(), isLoading = false) }
297 }
298}
299```
300
301## Convention Plugins (Gradle)
302
303For KMP projects, use convention plugins to reduce build file duplication:
304
305```kotlin
306// build-logic/src/main/kotlin/kmp-library.gradle.kts
307plugins {
308 id("org.jetbrains.kotlin.multiplatform")
309}
310
311kotlin {
312 androidTarget()
313 iosX64(); iosArm64(); iosSimulatorArm64()
314 sourceSets {
315 commonMain.dependencies { /* shared deps */ }
316 commonTest.dependencies { implementation(kotlin("test")) }
317 }
318}
319```
320
321Apply in modules:
322
323```kotlin
324// domain/build.gradle.kts
325plugins { id("kmp-library") }
326```
327
328## Anti-Patterns to Avoid
329
330- Importing Android framework classes in domain — keep it pure Kotlin
331- Exposing database entities or DTOs to the UI layer — always map to domain models
332- Putting business logic in ViewModels — extract to UseCases
333- Using GlobalScope or unstructured coroutines — use viewModelScope or structured concurrency
334- Fat repository implementations — split into focused DataSources
335- Circular module dependencies — if A depends on B, B must not depend on A
336
337## References
338
339See skill: compose-multiplatform-patterns for UI patterns.
340See skill: kotlin-coroutines-flows for async patterns.
341
In the file
SKILL.md1,055 words
Files1
LicenceMIT
Why you can read it

Nothing in a skill executes. The client loads the text and the model follows it, so a skill can be audited the way a runbook is — by reading it.

What it costs in context

Skills are not billed by the call. They are paid for in context: every token the instructions occupy is a token your code, your diff and your conversation cannot use. Here is what this one takes and when it takes it.

≈80
always loaded
The name and description, so the model knows the skill exists and when to reach for it.
2,145
on trigger
The instruction body, read only when the skill fires.
1.1%
of a 200k window
Ten skills this size would take about 11% of the window before you open a file.
050k100k150k200k context window

2.2k tokens, estimated from the bundle at four bytes to the token, held for the rest of the session once it triggers. Middling. Fine to keep on in a project where you use it weekly, worth unloading in one where you never do.

Servers bill, skills cost

A server charges by the month. A skill charges once per session, in context, and then keeps charging it for as long as the session lives.

Before and after

The same question, put to the same model twice: once as it comes, and once with these instructions loaded.

No worked example has been published for this skill yet.

Adoption
Installsnone yet
Ratingno reviews yet

The procedure it runs

The procedure has not been published here. It is in the skill’s own SKILL.md, which its author has not sent to the marketplace yet.

Prose, not code

These steps are written for a model to follow, not executed by a runtime. It can still be told to skip one, and it will say so when it does.

Servers it uses

None. This skill calls no MCP servers at all.

Everything it needs is in the instructions, so it works in a project with nothing connected — the model reads the file and changes how it works with what it can already reach.

It writes no files and reaches no network. All it changes is how the model reasons and writes.

What it asks for
Writes filesno
Network accessno

Read from the allowed-tools line of this skill’s own SKILL.md. A skill grants no permissions of its own — it can only ask for tools your client already has.

What it will not do

Every skill is narrow, and the useful ones say where they stop. These are the jobs this one is the wrong tool for.

What this skill is not for has not been published here. Nothing is implied by that: it is a section the author has not filled in.

What is in the bundle

1 file, 8.9 kB on disk. A bundle is text throughout: the instructions the model reads, plus the templates it fills in.

  • SKILL.md8.9 kB
What is not in it

No dependencies and nothing executable: a skill is text the agent reads, so the bundle is 1 file you can review in full before installing. The MIT licence covers the templates and examples as well as the instructions.

Install

Installing copies the bundle into your project. Nothing runs at install time — the files sit on disk until the model reads them.

# Android Clean Architecture · 2.2k tokens when loaded npx mcprush@latest skill add affaan-m/android-clean-architecture

Writes to .claude/skills/android-clean-architecture/ in the current project. Add --global to put it in your home directory instead, for every project.

Which clients pick it up on their own

A skill is a folder of text. A client with a skills folder reads it without being told; everywhere else the same text works, it is just handed to the model rather than found.

Claude Code.claude/skills/
Claude Desktop
ChatGPT
Cursor.cursor/skills/
VS Code.github/skills/
Codex CLI.agents/skills/
Gemini CLI.gemini/skills/
Grok.grok/skills/
Zed.agents/skills/
Windsurf.windsurf/skills/
Agent SDK.claude/skills/
HTTP / API
This release
Versionnot versioned
Publishedno release date on file
PriceFree
Referenceaffaan-m/android-clean-architecture

Versions

Its author publishes no version number, so there is nothing here to pin to: what you install is the folder as it stands today. Instructions change more often than APIs do — a skill can be rewritten entirely without anything it depends on moving.

v
  • No earlier releases have been published to the marketplace.
Pinning

Nothing to pin to: this skill carries no version number of its own. What you install is what the folder holds on the day you install it.

Reviews

no reviews yet · no installs yet

Nobody has reviewed this skill yet. The rating is the mean of the reviews written here, so there is none until somebody writes the first.

Who can post

Only accounts that have had the skill installed for fourteen days, so a review is written after living with it rather than after reading it. Publishers may reply once.

Publisher
Servers0