./gradlew ktlintCheck before committing.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.
com.shrivatsav.monomail.feature.{featurename}com.shrivatsav.monomail.core.{modulename}feature.settings.pgp, core.data.auth)@Composable
fun InboxScreen(
viewModel: InboxViewModel = hiltViewModel(),
onNavigateToCompose: () -> Unit,
onNavigateToThread: (String) -> Unit,
onNavigateToSettings: () -> Unit
) onNavigateTo.on{Action} 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() 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.
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.
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.
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 WorkManager workers with Hilt injection need libs.hilt.work and libs.hilt.work.compiler (KSP). The App class must implement Configuration.Provider.
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.
Always dispose properly with DisposableEffect — call webView.removeAllViews() and webView.destroy() in onDispose.
Data classes used with Gson must be kept in ProGuard rules, or you'll get empty objects in release builds.
Release builds require secrets.properties and keystore.properties in the project root. These are not committed. CI provides them from stored secrets.
ActionQueueManager.start() must be called from MonoMailApp.onCreate(). If pending actions aren't being processed, check that it's injected and started.
All Compose dependencies across all modules must use the same BOM version. Always import via platform(libs.androidx.compose.bom).