Style Guide

Kotlin

File structure

ScreenName.kt   # Composable(s) at top
ViewModelName.kt # ViewModel at top

One screen = one file. One ViewModel = one file. Exceptions for small related composables within the same file.

Package naming

Composable naming

@Composable
fun InboxScreen(
    viewModel: InboxViewModel = hiltViewModel(),
    onNavigateToCompose: () -> Unit,
    onNavigateToThread: (String) -> Unit,
    onNavigateToSettings: () -> Unit
)

State naming

sealed class ScreenState {
    data object Loading : ScreenState()
    data class Success(...) : ScreenState()
    data class Error(val message: String) : ScreenState()
}

// ViewModel
private val _state = MutableStateFlow<ScreenState>(ScreenState.Loading)
val state: StateFlow<ScreenState> = _state.asStateFlow()

Commit messages

type(scope): short summary (max 72 chars)

Longer explanation if needed. Wrap at 72 chars.

Types: fix, feat, refactor, chore, docs, test, style.

Scopes: inbox, detail, compose, settings, auth, data, push, pgp, ci, etc.


Common Pitfalls

1. Mixing up module namespaces

Each module has its own namespace. A module can only see classes from its declared dependencies. Add a project(":core:xyz") dependency if you need cross-module access.

2. Flavor mismatch

If a module doesn't have flavor-specific variations but the app does, you may get variant missing errors. Solution: add the flavor dimension with empty product flavors to every module.

3. Missing flavor source set

When adding flavor-specific files, put them in the correct source set:

src/github/java/.../GoogleAuthHelperImpl.kt     // github variant
src/playstore/java/.../GoogleAuthHelperImpl.kt  // playstore variant

4. HiltWorker without KSP

WorkManager workers with Hilt injection need libs.hilt.work and libs.hilt.work.compiler (KSP). The App class must implement Configuration.Provider.

5. Database migration failures

When changing the schema, create a new MIGRATION_X_Y constant in AppDatabase.kt. Test against a device with populated data. fallbackToDestructiveMigration(true) is set as safety net but will wipe user data.

6. WebView leaks in Compose

Always dispose properly with DisposableEffect — call webView.removeAllViews() and webView.destroy() in onDispose.

7. Gson serialization with ProGuard

Data classes used with Gson must be kept in ProGuard rules, or you'll get empty objects in release builds.

8. Release build credentials

Release builds require secrets.properties and keystore.properties in the project root. These are not committed. CI provides them from stored secrets.

9. ActionQueueManager not started

ActionQueueManager.start() must be called from MonoMailApp.onCreate(). If pending actions aren't being processed, check that it's injected and started.

10. Multiple Compose BOM versions

All Compose dependencies across all modules must use the same BOM version. Always import via platform(libs.androidx.compose.bom).

← CI/CD Back to Dev Guide