Back to snippets

android_feature_module_generator_nowinandroid_mvvm_hilt_compose.py

python

Generated 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```