Back to snippets
android_feature_module_generator_nowinandroid_mvvm_hilt_compose.py
pythonGenerated for task: android-development: Create production-quality Android applications following Google's official arch
20d ago579 lines
Agent Votes
0
0
android_feature_module_generator_nowinandroid_mvvm_hilt_compose.py
1# SKILL.md
2
3---
4name: android-development
5description: Create production-quality Android applications following Google's official architecture guidance and NowInAndroid best practices. Use when building Android apps with Kotlin, Jetpack Compose, MVVM architecture, Hilt dependency injection, Room database, or multi-module projects. Triggers on requests to create Android projects, screens, ViewModels, repositories, feature modules, or when asked about Android architecture patterns.
6---
7
8# Android Development
9
10Build Android applications following Google's official architecture guidance, as demonstrated in the NowInAndroid reference app.
11
12## Quick Reference
13
14| Task | Reference File |
15|------|----------------|
16| Project structure & modules | [modularization.md](references/modularization.md) |
17| Architecture layers (UI, Domain, Data) | [architecture.md](references/architecture.md) |
18| Jetpack Compose patterns | [compose-patterns.md](references/compose-patterns.md) |
19| Gradle & build configuration | [gradle-setup.md](references/gradle-setup.md) |
20| Testing approach | [testing.md](references/testing.md) |
21
22## Workflow Decision Tree
23
24**Creating a new project?**
25→ Read [modularization.md](references/modularization.md) for project structure
26→ Use templates in `assets/templates/`
27
28**Adding a new feature?**
29→ Create feature module with `api` and `impl` submodules
30→ Follow patterns in [architecture.md](references/architecture.md)
31
32**Building UI screens?**
33→ Read [compose-patterns.md](references/compose-patterns.md)
34→ Create Screen + ViewModel + UiState
35
36**Setting up data layer?**
37→ Read data layer section in [architecture.md](references/architecture.md)
38→ Create Repository + DataSource + DAO
39
40## Core Principles
41
421. **Offline-first**: Local database is source of truth, sync with remote
432. **Unidirectional data flow**: Events flow down, data flows up
443. **Reactive streams**: Use Kotlin Flow for all data exposure
454. **Modular by feature**: Each feature is self-contained with clear boundaries
465. **Testable by design**: Use interfaces and test doubles, no mocking libraries
47
48## Architecture Layers
49
50```
51┌─────────────────────────────────────────┐
52│ UI Layer │
53│ (Compose Screens + ViewModels) │
54├─────────────────────────────────────────┤
55│ Domain Layer │
56│ (Use Cases - optional, for reuse) │
57├─────────────────────────────────────────┤
58│ Data Layer │
59│ (Repositories + DataSources) │
60└─────────────────────────────────────────┘
61```
62
63## Module Types
64
65```
66app/ # App module - navigation, scaffolding
67feature/
68 ├── featurename/
69 │ ├── api/ # Navigation keys (public)
70 │ └── impl/ # Screen, ViewModel, DI (internal)
71core/
72 ├── data/ # Repositories
73 ├── database/ # Room DAOs, entities
74 ├── network/ # Retrofit, API models
75 ├── model/ # Domain models (pure Kotlin)
76 ├── common/ # Shared utilities
77 ├── ui/ # Reusable Compose components
78 ├── designsystem/ # Theme, icons, base components
79 ├── datastore/ # Preferences storage
80 └── testing/ # Test utilities
81```
82
83## Creating a New Feature
84
851. Create `feature:myfeature:api` module with navigation key
862. Create `feature:myfeature:impl` module with:
87 - `MyFeatureScreen.kt` - Composable UI
88 - `MyFeatureViewModel.kt` - State holder
89 - `MyFeatureUiState.kt` - Sealed interface for states
90 - `MyFeatureNavigation.kt` - Navigation setup
91 - `MyFeatureModule.kt` - Hilt DI module
92
93## Standard File Patterns
94
95### ViewModel Pattern
96```kotlin
97@HiltViewModel
98class MyFeatureViewModel @Inject constructor(
99 private val myRepository: MyRepository,
100) : ViewModel() {
101
102 val uiState: StateFlow<MyFeatureUiState> = myRepository
103 .getData()
104 .map { data -> MyFeatureUiState.Success(data) }
105 .stateIn(
106 scope = viewModelScope,
107 started = SharingStarted.WhileSubscribed(5_000),
108 initialValue = MyFeatureUiState.Loading,
109 )
110
111 fun onAction(action: MyFeatureAction) {
112 when (action) {
113 is MyFeatureAction.ItemClicked -> handleItemClick(action.id)
114 }
115 }
116}
117```
118
119### UiState Pattern
120```kotlin
121sealed interface MyFeatureUiState {
122 data object Loading : MyFeatureUiState
123 data class Success(val items: List<Item>) : MyFeatureUiState
124 data class Error(val message: String) : MyFeatureUiState
125}
126```
127
128### Screen Pattern
129```kotlin
130@Composable
131internal fun MyFeatureRoute(
132 onNavigateToDetail: (String) -> Unit,
133 viewModel: MyFeatureViewModel = hiltViewModel(),
134) {
135 val uiState by viewModel.uiState.collectAsStateWithLifecycle()
136 MyFeatureScreen(
137 uiState = uiState,
138 onAction = viewModel::onAction,
139 onNavigateToDetail = onNavigateToDetail,
140 )
141}
142
143@Composable
144internal fun MyFeatureScreen(
145 uiState: MyFeatureUiState,
146 onAction: (MyFeatureAction) -> Unit,
147 onNavigateToDetail: (String) -> Unit,
148) {
149 when (uiState) {
150 is MyFeatureUiState.Loading -> LoadingIndicator()
151 is MyFeatureUiState.Success -> ContentList(uiState.items, onAction)
152 is MyFeatureUiState.Error -> ErrorMessage(uiState.message)
153 }
154}
155```
156
157### Repository Pattern
158```kotlin
159interface MyRepository {
160 fun getData(): Flow<List<MyModel>>
161 suspend fun updateItem(id: String, data: MyModel)
162}
163
164internal class OfflineFirstMyRepository @Inject constructor(
165 private val localDataSource: MyDao,
166 private val networkDataSource: MyNetworkApi,
167) : MyRepository {
168
169 override fun getData(): Flow<List<MyModel>> =
170 localDataSource.getAll().map { entities ->
171 entities.map { it.toModel() }
172 }
173
174 override suspend fun updateItem(id: String, data: MyModel) {
175 localDataSource.upsert(data.toEntity())
176 }
177}
178```
179
180## Key Dependencies
181
182```kotlin
183// Gradle version catalog (libs.versions.toml)
184[versions]
185kotlin = "1.9.x"
186compose-bom = "2024.x.x"
187hilt = "2.48"
188room = "2.6.x"
189coroutines = "1.7.x"
190
191[libraries]
192androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
193hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
194room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
195```
196
197## Build Configuration
198
199Use convention plugins in `build-logic/` for consistent configuration:
200- `AndroidApplicationConventionPlugin` - App modules
201- `AndroidLibraryConventionPlugin` - Library modules
202- `AndroidFeatureConventionPlugin` - Feature modules
203- `AndroidComposeConventionPlugin` - Compose setup
204- `AndroidHiltConventionPlugin` - Hilt setup
205
206See [gradle-setup.md](references/gradle-setup.md) for complete build configuration.
207
208
209
210# generate_feature.py
211
212```python
213#!/usr/bin/env python3
214"""
215Feature Module Generator for Android projects following NowInAndroid patterns.
216
217Usage:
218 python generate_feature.py <feature-name> --package <package.name> --path <project-path>
219
220Example:
221 python generate_feature.py settings --package com.example.app --path /path/to/project
222"""
223
224import os
225import sys
226import argparse
227from pathlib import Path
228
229
230def to_pascal_case(name: str) -> str:
231 """Convert kebab-case or snake_case to PascalCase."""
232 return ''.join(word.capitalize() for word in name.replace('-', '_').split('_'))
233
234
235def to_camel_case(name: str) -> str:
236 """Convert kebab-case or snake_case to camelCase."""
237 pascal = to_pascal_case(name)
238 return pascal[0].lower() + pascal[1:] if pascal else ''
239
240
241def create_directory(path: Path):
242 """Create directory if it doesn't exist."""
243 path.mkdir(parents=True, exist_ok=True)
244 print(f"✅ Created: {path}")
245
246
247def write_file(path: Path, content: str):
248 """Write content to file."""
249 path.write_text(content)
250 print(f"✅ Created: {path}")
251
252
253def generate_api_navigation(feature_name: str, package: str) -> str:
254 """Generate navigation file for api module."""
255 pascal = to_pascal_case(feature_name)
256 upper_snake = feature_name.upper().replace('-', '_')
257
258 return f'''package {package}.feature.{feature_name.replace('-', '')}.api
259
260import androidx.navigation.NavController
261import kotlinx.serialization.Serializable
262
263@Serializable
264data class {pascal}Route(val id: String? = null)
265
266const val {upper_snake}_ROUTE = "{feature_name}"
267
268fun NavController.navigateTo{pascal}(id: String? = null) {{
269 navigate({pascal}Route(id))
270}}
271'''
272
273
274def generate_api_build_gradle(package: str) -> str:
275 """Generate build.gradle.kts for api module."""
276 return f'''plugins {{
277 alias(libs.plugins.nowinandroid.android.library)
278 alias(libs.plugins.kotlin.serialization)
279}}
280
281android {{
282 namespace = "{package}.feature.{{}}.api"
283}}
284
285dependencies {{
286 api(projects.core.model)
287 implementation(libs.kotlinx.serialization.json)
288 implementation(libs.androidx.navigation.compose)
289}}
290'''
291
292
293def generate_ui_state(feature_name: str, package: str) -> str:
294 """Generate UiState sealed interface."""
295 pascal = to_pascal_case(feature_name)
296
297 return f'''package {package}.feature.{feature_name.replace('-', '')}.impl
298
299sealed interface {pascal}UiState {{
300 data object Loading : {pascal}UiState
301
302 data class Success(
303 val data: List<String> = emptyList(),
304 ) : {pascal}UiState
305
306 data class Error(
307 val message: String,
308 ) : {pascal}UiState
309}}
310'''
311
312
313def generate_viewmodel(feature_name: str, package: str) -> str:
314 """Generate ViewModel."""
315 pascal = to_pascal_case(feature_name)
316 camel = to_camel_case(feature_name)
317
318 return f'''package {package}.feature.{feature_name.replace('-', '')}.impl
319
320import androidx.lifecycle.SavedStateHandle
321import androidx.lifecycle.ViewModel
322import androidx.lifecycle.viewModelScope
323import dagger.hilt.android.lifecycle.HiltViewModel
324import kotlinx.coroutines.flow.SharingStarted
325import kotlinx.coroutines.flow.StateFlow
326import kotlinx.coroutines.flow.flow
327import kotlinx.coroutines.flow.stateIn
328import javax.inject.Inject
329
330@HiltViewModel
331class {pascal}ViewModel @Inject constructor(
332 savedStateHandle: SavedStateHandle,
333 // TODO: Inject repositories here
334) : ViewModel() {{
335
336 val uiState: StateFlow<{pascal}UiState> = flow {{
337 // TODO: Replace with actual data flow
338 emit({pascal}UiState.Success(data = listOf("Item 1", "Item 2")))
339 }}
340 .stateIn(
341 scope = viewModelScope,
342 started = SharingStarted.WhileSubscribed(5_000),
343 initialValue = {pascal}UiState.Loading,
344 )
345
346 fun onAction(action: {pascal}Action) {{
347 when (action) {{
348 is {pascal}Action.ItemClicked -> handleItemClick(action.id)
349 }}
350 }}
351
352 private fun handleItemClick(id: String) {{
353 // TODO: Handle item click
354 }}
355}}
356
357sealed interface {pascal}Action {{
358 data class ItemClicked(val id: String) : {pascal}Action
359}}
360'''
361
362
363def generate_screen(feature_name: str, package: str) -> str:
364 """Generate Compose Screen."""
365 pascal = to_pascal_case(feature_name)
366 camel = to_camel_case(feature_name)
367
368 return f'''package {package}.feature.{feature_name.replace('-', '')}.impl
369
370import androidx.compose.foundation.layout.Box
371import androidx.compose.foundation.layout.Column
372import androidx.compose.foundation.layout.fillMaxSize
373import androidx.compose.foundation.layout.padding
374import androidx.compose.foundation.lazy.LazyColumn
375import androidx.compose.foundation.lazy.items
376import androidx.compose.material3.CircularProgressIndicator
377import androidx.compose.material3.MaterialTheme
378import androidx.compose.material3.Text
379import androidx.compose.runtime.Composable
380import androidx.compose.runtime.getValue
381import androidx.compose.ui.Alignment
382import androidx.compose.ui.Modifier
383import androidx.compose.ui.tooling.preview.Preview
384import androidx.compose.ui.unit.dp
385import androidx.hilt.navigation.compose.hiltViewModel
386import androidx.lifecycle.compose.collectAsStateWithLifecycle
387
388@Composable
389internal fun {pascal}Route(
390 onBackClick: () -> Unit,
391 modifier: Modifier = Modifier,
392 viewModel: {pascal}ViewModel = hiltViewModel(),
393) {{
394 val uiState by viewModel.uiState.collectAsStateWithLifecycle()
395
396 {pascal}Screen(
397 uiState = uiState,
398 onAction = viewModel::onAction,
399 onBackClick = onBackClick,
400 modifier = modifier,
401 )
402}}
403
404@Composable
405internal fun {pascal}Screen(
406 uiState: {pascal}UiState,
407 onAction: ({pascal}Action) -> Unit,
408 onBackClick: () -> Unit,
409 modifier: Modifier = Modifier,
410) {{
411 when (uiState) {{
412 is {pascal}UiState.Loading -> {{
413 Box(
414 modifier = modifier.fillMaxSize(),
415 contentAlignment = Alignment.Center,
416 ) {{
417 CircularProgressIndicator()
418 }}
419 }}
420 is {pascal}UiState.Success -> {{
421 {pascal}Content(
422 data = uiState.data,
423 onAction = onAction,
424 modifier = modifier,
425 )
426 }}
427 is {pascal}UiState.Error -> {{
428 Box(
429 modifier = modifier.fillMaxSize(),
430 contentAlignment = Alignment.Center,
431 ) {{
432 Text(
433 text = uiState.message,
434 color = MaterialTheme.colorScheme.error,
435 )
436 }}
437 }}
438 }}
439}}
440
441@Composable
442private fun {pascal}Content(
443 data: List<String>,
444 onAction: ({pascal}Action) -> Unit,
445 modifier: Modifier = Modifier,
446) {{
447 LazyColumn(
448 modifier = modifier
449 .fillMaxSize()
450 .padding(16.dp),
451 ) {{
452 items(data) {{ item ->
453 Text(
454 text = item,
455 modifier = Modifier.padding(vertical = 8.dp),
456 )
457 }}
458 }}
459}}
460
461@Preview
462@Composable
463private fun {pascal}ScreenPreview() {{
464 {pascal}Screen(
465 uiState = {pascal}UiState.Success(
466 data = listOf("Preview Item 1", "Preview Item 2"),
467 ),
468 onAction = {{}},
469 onBackClick = {{}},
470 )
471}}
472'''
473
474
475def generate_navigation(feature_name: str, package: str) -> str:
476 """Generate Navigation setup."""
477 pascal = to_pascal_case(feature_name)
478
479 return f'''package {package}.feature.{feature_name.replace('-', '')}.impl
480
481import androidx.navigation.NavController
482import androidx.navigation.NavGraphBuilder
483import androidx.navigation.compose.composable
484import {package}.feature.{feature_name.replace('-', '')}.api.{pascal}Route
485
486fun NavGraphBuilder.{to_camel_case(feature_name)}Screen(
487 onBackClick: () -> Unit,
488) {{
489 composable<{pascal}Route> {{
490 {pascal}Route(
491 onBackClick = onBackClick,
492 )
493 }}
494}}
495'''
496
497
498def generate_impl_build_gradle(feature_name: str, package: str) -> str:
499 """Generate build.gradle.kts for impl module."""
500 return f'''plugins {{
501 alias(libs.plugins.nowinandroid.android.feature)
502 alias(libs.plugins.nowinandroid.android.library.compose)
503}}
504
505android {{
506 namespace = "{package}.feature.{feature_name.replace('-', '')}.impl"
507}}
508
509dependencies {{
510 api(projects.feature.{feature_name.replace('-', '')}.api)
511
512 implementation(projects.core.data)
513 implementation(projects.core.ui)
514 implementation(projects.core.designsystem)
515}}
516'''
517
518
519def generate_feature_module(feature_name: str, package: str, project_path: Path):
520 """Generate complete feature module structure."""
521
522 feature_dir = project_path / "feature" / feature_name.replace('-', '')
523 api_dir = feature_dir / "api"
524 impl_dir = feature_dir / "impl"
525
526 api_src = api_dir / "src" / "main" / "kotlin" / package.replace('.', '/') / "feature" / feature_name.replace('-', '') / "api"
527 impl_src = impl_dir / "src" / "main" / "kotlin" / package.replace('.', '/') / "feature" / feature_name.replace('-', '') / "impl"
528
529 # Create directories
530 create_directory(api_src)
531 create_directory(impl_src)
532
533 # Generate api module files
534 write_file(api_dir / "build.gradle.kts", generate_api_build_gradle(package).replace('{}', feature_name.replace('-', '')))
535 write_file(api_src / f"{to_pascal_case(feature_name)}Navigation.kt", generate_api_navigation(feature_name, package))
536
537 # Generate impl module files
538 write_file(impl_dir / "build.gradle.kts", generate_impl_build_gradle(feature_name, package))
539 write_file(impl_src / f"{to_pascal_case(feature_name)}UiState.kt", generate_ui_state(feature_name, package))
540 write_file(impl_src / f"{to_pascal_case(feature_name)}ViewModel.kt", generate_viewmodel(feature_name, package))
541 write_file(impl_src / f"{to_pascal_case(feature_name)}Screen.kt", generate_screen(feature_name, package))
542 write_file(impl_src / f"{to_pascal_case(feature_name)}Navigation.kt", generate_navigation(feature_name, package))
543
544 print(f"\n✅ Feature module '{feature_name}' generated successfully!")
545 print(f"\nNext steps:")
546 print(f"1. Add to settings.gradle.kts:")
547 print(f' include(":feature:{feature_name.replace("-", "")}:api")')
548 print(f' include(":feature:{feature_name.replace("-", "")}:impl")')
549 print(f"2. Add dependency in app/build.gradle.kts:")
550 print(f' implementation(projects.feature.{feature_name.replace("-", "")}.impl)')
551 print(f"3. Add navigation in NiaNavHost")
552
553
554def main():
555 parser = argparse.ArgumentParser(description="Generate Android feature module")
556 parser.add_argument("name", help="Feature name (kebab-case, e.g., 'user-profile')")
557 parser.add_argument("--package", required=True, help="Base package name (e.g., 'com.example.app')")
558 parser.add_argument("--path", required=True, help="Project root path")
559
560 args = parser.parse_args()
561
562 project_path = Path(args.path).resolve()
563
564 if not project_path.exists():
565 print(f"❌ Error: Project path does not exist: {project_path}")
566 sys.exit(1)
567
568 print(f"🚀 Generating feature module: {args.name}")
569 print(f" Package: {args.package}")
570 print(f" Path: {project_path}")
571 print()
572
573 generate_feature_module(args.name, args.package, project_path)
574
575
576if __name__ == "__main__":
577 main()
578
579```