This guide helps you resolve common issues when working with this Android starter template.
Error:
Jetpack requires JDK 17+ but it is currently using JDK 11.
Java Home: [/path/to/jdk-11]
Solution:
- Install JDK 21 (required by this template)
- Configure Android Studio to use JDK 21:
- File → Project Structure → SDK Location → Gradle Settings
- Set Gradle JDK to version 21
- Verify in
settings.gradle.kts:check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17))
References:
- settings.gradle.kts:88-94
- Android Studio JDK Configuration
Error:
Could not resolve com.google.firebase:firebase-bom:34.4.0
Solution:
- Check
settings.gradle.ktsrepository configuration:repositories { google { content { includeGroupByRegex("com\\.google.*") } } mavenCentral() } - Verify internet connection
- Clear Gradle cache:
./gradlew clean --refresh-dependencies
- Check if behind a corporate proxy (configure in
gradle.properties)
References:
- settings.gradle.kts:32-44
Error:
Could not resolve libs.androidx.core.ktx
Solution:
- Ensure
gradle/libs.versions.tomlexists and is valid - Check version catalog syntax:
[libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
- Verify version references exist in
[versions]section - Sync project with Gradle files
References:
- gradle/libs.versions.toml
Error:
[Dagger/MissingBinding] Cannot be provided without an @Inject constructor or an @Provides-annotated method
Solution:
- Verify Hilt plugin is applied in module's
build.gradle.kts:plugins { alias(libs.plugins.jetpack.dagger.hilt) } - Check if class is annotated properly:
@HiltViewModel class MyViewModel @Inject constructor(...)
- Ensure repository has
@Bindsor@Providesin a Hilt module - Clean and rebuild:
./gradlew clean build
References:
- build-logic/convention/src/main/kotlin/DaggerHiltConventionPlugin.kt
- See Dependency Injection Guide
Error:
error: Cannot find setter for field
Solution:
- Ensure entity class properties match DAO query column names
- Add
@ColumnInfoannotation if database column name differs:@Entity data class MyEntity( @ColumnInfo(name = "user_id") val userId: String )
- Verify
@PrimaryKeyis present - Clean and rebuild project
References:
- core/room/src/main/kotlin/dev/atick/core/room/
Error:
Duplicate class kotlin.collections.CollectionsKt found in modules
Solution:
- Check for conflicting dependency versions in
gradle/libs.versions.toml - Use BOM (Bill of Materials) for consistent versioning:
implementation(platform(libs.firebase.bom))
- Exclude transitive dependencies if needed:
implementation(libs.some.library) { exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") }
References:
- gradle/libs.versions.toml
- build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt:35
Error:
Configuration cache problems found in this build
Solution:
- This is expected due to google-services plugin (see gradle.properties:28)
- Warning mode is configured intentionally:
org.gradle.configuration-cache.problems=warn - Build will complete successfully - these are warnings, not errors
- Reference issue: google/play-services-plugins#246
References:
- gradle.properties:24-28
Error (Logcat):
java.lang.IllegalStateException: Default FirebaseApp is not initialized
Solution:
- Verify
google-services.jsonexists inapp/directory - Check Firebase plugin is applied in
app/build.gradle.kts:plugins { alias(libs.plugins.jetpack.firebase) } - Ensure
google-servicesplugin is applied (happens automatically via convention plugin) - If using custom
google-services.json:- Verify package name matches
applicationIdinbuild.gradle.kts - Check Firebase project configuration in Firebase Console
- Verify package name matches
References:
- app/build.gradle.kts:30
- build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt:30
- Firebase Setup Guide
Error (Logcat):
java.lang.RuntimeException: Unable to create application:
java.lang.IllegalStateException: Hilt entry point not found
Solution:
- Verify
Applicationclass is annotated with@HiltAndroidApp:@HiltAndroidApp class JetpackApplication : Application()
- Check activities are annotated with
@AndroidEntryPoint:@AndroidEntryPoint class MainActivity : ComponentActivity()
- Ensure ViewModel uses
@HiltViewModel:@HiltViewModel class MyViewModel @Inject constructor(...) : ViewModel()
- Clean and rebuild project
References:
- app/src/main/kotlin/dev/atick/compose/JetpackApplication.kt
- app/src/main/kotlin/dev/atick/compose/ui/MainActivity.kt
Error (Logcat):
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest cannot be found
Solution:
- Verify destination is defined in navigation graph:
@Serializable data object MyDestination fun NavGraphBuilder.myScreen(...) { composable<MyDestination> { ... } }
- Ensure navigation graph is added to
NavHost:NavHost(...) { myScreen(...) }
- Check if using correct navigation route type
- Verify nested graphs have correct start destination
References:
- app/src/main/kotlin/dev/atick/compose/navigation/
- Navigation Deep Dive
Error (Logcat):
kotlinx.serialization.SerializationException: Serializer for class 'MyData' is not found
Solution:
- Add
@Serializableannotation to data class:@Serializable data class MyDestination(val id: String, val data: MyData) @Serializable data class MyData(val name: String)
- Ensure Kotlin serialization plugin is applied:
plugins { alias(libs.plugins.kotlin.serialization) } - For custom types, provide custom serializer
References:
Problem: Unexpected backstack behavior (duplicate screens, can't go back, wrong screen when pressing back)
Solution:
- For "pop to specific destination", use
popUpTowithinclusive:navController.navigate(Home) { popUpTo(Login) { inclusive = true } // Remove Login from backstack }
- For "single instance" screens (like Home), use
launchSingleTop:navController.navigate(Home) { launchSingleTop = true restoreState = true }
- For bottom navigation, use proper state restoration:
navController.navigate(destination) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } - To clear entire backstack and start fresh:
navController.navigate(Home) { popUpTo(0) { inclusive = true } // Clear all backstack }
- Debug backstack state:
Timber.d("Backstack: ${navController.currentBackStack.value.map { it.destination.route }}")
References:
- Navigation Deep Dive
- app/src/main/kotlin/dev/atick/compose/navigation/TopLevelNavigation.kt
Problem: Nested graphs not working, start destination errors, or can't navigate to nested destinations
Solution:
- Ensure nested graph has explicit start destination:
@Serializable data object AuthNavGraph @Serializable data object Login fun NavGraphBuilder.authNavGraph() { navigation<AuthNavGraph>(startDestination = Login) { // Must specify startDestination composable<Login> { LoginRoute(...) } composable<Register> { RegisterRoute(...) } } }
- Navigate to nested graph's start destination (not the graph itself):
// Wrong - can't navigate to graph navController.navigate(AuthNavGraph) // Correct - navigate to start destination navController.navigate(Login)
- For deep links into nested graphs, ensure route hierarchy is correct:
// Nested graph route hierarchy: AuthNavGraph > Login composable<Login>( deepLinks = listOf(navDeepLink<Login>(basePath = "app://auth/login")) ) { ... }
- When popping from nested graph, pop to parent graph's destination:
navController.navigate(Home) { popUpTo(AuthNavGraph) { inclusive = true } // Remove entire auth flow }
- Check if parent NavHost includes nested graph:
NavHost(...) { authNavGraph() // Must be called to register nested graph homeNavGraph() }
References:
- Navigation Deep Dive
- app/src/main/kotlin/dev/atick/compose/navigation/JetpackNavHost.kt
Problem: Arguments passed to destination are null or have default values instead of passed values
Solution:
- Ensure destination parameter names match navigation arguments:
@Serializable data class Profile(val userId: String) // Parameter name must match // In composable composable<Profile> { backStackEntry -> val profile: Profile = backStackEntry.toRoute() ProfileRoute(userId = profile.userId) // Use parameter from route }
- Verify arguments are passed when navigating:
// Wrong - missing argument navController.navigate(Profile("")) // Correct - pass actual argument navController.navigate(Profile(userId = currentUserId))
- For optional arguments, use nullable types or default values:
@Serializable data class Profile( val userId: String, val tab: String? = null // Optional argument with default )
- Check for serialization issues (see "Navigation Argument Serialization Errors" above)
- Debug arguments:
composable<Profile> { backStackEntry -> val profile: Profile = backStackEntry.toRoute() Timber.d("Received userId: ${profile.userId}") ProfileRoute(userId = profile.userId) }
References:
Problem: UI doesn't reflect ViewModel state changes
Solution:
- Ensure using
collectAsStateWithLifecycle()in composables:val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Verify ViewModel uses
MutableStateFlow:private val _uiState = MutableStateFlow(UiState(MyScreenData())) val uiState = _uiState.asStateFlow()
- Use proper state update functions:
// Synchronous updates _uiState.updateState { copy(name = newName) } // Async operations with Result<T> _uiState.updateStateWith { repository.getData() } // Async operations with Result<Unit> _uiState.updateWith { repository.saveData() }
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/StatefulComposable.kt
- State Management Guide
Problem: Error messages or navigation events trigger multiple times
Solution:
- Use
OneTimeEventwrapper for single-consumption events:data class UiState<T>( val data: T, val error: OneTimeEvent<Throwable?> = OneTimeEvent(null) )
- Consume event properly in UI:
StatefulComposable( state = uiState, onShowSnackbar = onShowSnackbar ) { ... }
StatefulComposablehandles event consumption automatically
References:
- core/android/src/main/kotlin/dev/atick/core/android/utils/OneTimeEvent.kt
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/StatefulComposable.kt
Problem: Multiple loading indicators showing at once or loading state stuck
Solution:
- Use single
UiState<T>wrapper per screen (not multiple):// Wrong - multiple loading states data class ScreenData( val data1Loading: Boolean, val data2Loading: Boolean ) // Correct - single loading state in UiState wrapper data class ScreenData( val data1: List<Item>, val data2: List<Item> ) // UiState<ScreenData> has single loading field
- If truly need multiple loading states, manage them explicitly:
data class ScreenData( val items: List<Item> = emptyList(), val isRefreshing: Boolean = false // Separate from main loading )
- Ensure
updateStateWithcompletes properly (sets loading = false) - Check for exception swallowing that prevents loading state reset
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/UiState.kt
- State Management Guide
Problem:
updateStateWith or updateWith doesn't update state or shows compilation error
Solution:
- Ensure Kotlin context parameters feature is enabled (already configured):
// In build.gradle.kts freeCompilerArgs += "-Xcontext-receivers"
- Verify you're calling from ViewModel (context parameters require ViewModel scope):
@HiltViewModel class MyViewModel @Inject constructor() : ViewModel() { fun loadData() { _uiState.updateStateWith { // Has implicit access to viewModelScope repository.getData() } } }
- For
updateStateWith, repository must returnResult<T>:// Repository override suspend fun getData(): Result<Data> = suspendRunCatching { ... }
- For
updateWith, repository must returnResult<Unit>:// Repository override suspend fun saveData(): Result<Unit> = suspendRunCatching { ... }
- If still issues, use explicit
viewModelScope.launchas fallback
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/StateFlowExtensions.kt
- State Management Guide
Problem: UI doesn't update even though state has changed
Solution:
- Ensure using
collectAsStateWithLifecycle()instead ofcollectAsState():// Wrong - doesn't respect lifecycle val uiState by viewModel.uiState.collectAsState() // Correct - lifecycle-aware collection val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- Verify state is immutable data class with
copy()for updates:data class ScreenData(val name: String) // Updates must create new instance _uiState.updateState { copy(name = newName) }
- Check if accidentally mutating state instead of replacing it
- Ensure
StateFlowis being used, notFlow
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/extensions/LifecycleExtensions.kt
- feature/home/src/main/kotlin/dev/atick/feature/home/ui/home/HomeScreen.kt:68
Problem: ViewModel continues executing after screen is destroyed
Solution:
- Always use
viewModelScopefor coroutines in ViewModel:fun loadData() { viewModelScope.launch { // Automatically cancelled when ViewModel is cleared } }
- For background operations, use
updateStateWith(uses context parameters):fun loadData() { _uiState.updateStateWith { // Auto-uses viewModelScope repository.getData() } }
- Never launch coroutines with
GlobalScope - Verify ViewModel is scoped to navigation destination, not activity
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/StateFlowExtensions.kt
- State Management Guide
Problem: Error snackbar appears after user has navigated away
Solution:
- This is expected behavior when using
StatefulComposable - To prevent, handle navigation before error occurs:
fun saveAndNavigate() { viewModelScope.launch { repository.save().onSuccess { navigator.navigate(NextScreen) // Navigate before error can trigger }.onFailure { _uiState.updateState { copy(error = OneTimeEvent(it)) } } } }
- Or consume errors before navigation:
// In composable LaunchedEffect(saveSuccess) { if (saveSuccess) { navigator.navigate(NextScreen) } }
- Consider using navigation with result pattern if needed
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/StatefulComposable.kt
- core/android/src/main/kotlin/dev/atick/core/android/utils/OneTimeEvent.kt
Problem: App state lost during rotation or configuration change
Solution:
- ViewModels survive configuration changes automatically
- Ensure state is in ViewModel, not composable:
// Wrong - state lost on rotation @Composable fun MyScreen() { var name by remember { mutableStateOf("") } } // Correct - state survives rotation @HiltViewModel class MyViewModel @Inject constructor() : ViewModel() { private val _uiState = MutableStateFlow(UiState(ScreenData())) val uiState = _uiState.asStateFlow() }
- For non-ViewModel state that should persist, use
rememberSaveable:var searchQuery by rememberSaveable { mutableStateOf("") }
- Complex objects need custom Saver implementation
References:
- State Management Guide
- feature/home/src/main/kotlin/dev/atick/feature/home/ui/home/HomeViewModel.kt
Error:
com.google.android.gms.common.api.ApiException: 10:
Solution:
- Add SHA-1 fingerprint to Firebase Console:
# Get debug SHA-1 ./gradlew signingReport - Copy SHA-1 from output under "Variant: debug, Config: debug"
- Add to Firebase Console:
- Project Settings → Your apps → SHA certificate fingerprints
- Download new
google-services.jsonand replace inapp/ - Rebuild and reinstall app
References:
- Firebase Setup Guide
- firebase/auth/src/main/kotlin/dev/atick/firebase/auth/data/AuthDataSource.kt
Error (Logcat):
CredentialManager is not available
Solution:
- Ensure device/emulator runs Android 14+ or has Google Play Services
- For devices below Android 14, add Jetpack library:
(Already included in template)
implementation(libs.androidx.credentials) implementation(libs.credentials.play.services.auth)
- Verify Google Play Services is up-to-date on device
References:
- firebase/auth/src/main/kotlin/dev/atick/firebase/auth/data/AuthDataSource.kt
- gradle/libs.versions.toml:160-162
Error (Logcat):
PERMISSION_DENIED: Missing or insufficient permissions
Solution:
- Check Firestore Security Rules in Firebase Console
- For development, use permissive rules (
⚠️ not for production):rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } } - For production, implement proper security rules
- Ensure user is authenticated before accessing Firestore
References:
- firebase/firestore/src/main/kotlin/dev/atick/firebase/firestore/data/FirebaseDataSource.kt
- Firebase Setup Guide
Problem: Events not appearing in Firebase Analytics console
Solution:
- Verify Firebase Analytics is initialized (happens automatically with Firebase SDK)
- Check if Analytics logging is enabled:
// In debug builds, enable verbose logging Firebase.analytics.setAnalyticsCollectionEnabled(true)
- For debug testing, enable debug mode via ADB:
# Enable Analytics debug mode adb shell setprop debug.firebase.analytics.app dev.atick.compose # View events in real-time adb logcat -s FA
- Check if events are being logged correctly:
Firebase.analytics.logEvent("button_click") { param("button_name", "login") param("screen_name", "auth") }
- Events may take 24 hours to appear in console (use DebugView for immediate feedback)
- Verify
google-services.jsonhas correct Analytics project configuration
References:
- firebase/analytics/src/main/kotlin/dev/atick/firebase/analytics/AnalyticsLogger.kt
- Firebase Setup Guide
Problem: Crashes not appearing in Firebase Crashlytics console
Solution:
- Ensure Crashlytics is enabled in
build.gradle.kts:buildTypes { release { firebaseCrashlytics { mappingFileUploadEnabled = true } } } - Verify Firebase Crashlytics plugin is applied (happens via
FirebaseConventionPlugin) - Check if Crashlytics is initialized:
// Crashlytics initializes automatically with Firebase SDK Firebase.crashlytics.setCrashlyticsCollectionEnabled(true)
- For testing, force a crash:
Firebase.crashlytics.log("Test crash triggered") throw RuntimeException("Test crash")
- Crashes may take a few minutes to appear in console
- For release builds, ensure ProGuard mapping files are uploaded:
./gradlew assembleRelease # Mapping files automatically uploaded if mappingFileUploadEnabled = true - Check logcat for Crashlytics errors:
adb logcat -s FirebaseCrashlytics
References:
- firebase/analytics/src/main/kotlin/dev/atick/firebase/analytics/AnalyticsLogger.kt
- build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt
- Firebase Setup Guide
Problem: Firebase not initializing properly, causing crashes or missing functionality
Solution:
- See detailed Firebase initialization troubleshooting in Runtime Errors → Application Crashes on Startup → Firebase Initialization Failure (line 195)
- Quick checklist:
- ✅
google-services.jsonexists inapp/directory - ✅ Firebase plugin applied via convention plugin
- ✅ Package name matches
applicationId - ✅ Firebase project properly configured in console
- ✅
- For emulator testing, use Firebase Emulator Suite:
firebase emulators:start
- Check Firebase SDK versions in
gradle/libs.versions.toml:[versions] firebase-bom = "34.4.0"
References:
- app/build.gradle.kts:30
- build-logic/convention/src/main/kotlin/FirebaseConventionPlugin.kt
- Firebase Setup Guide
- See also: Runtime Errors → Firebase Initialization Failure (line 195)
Problem: UI doesn't update when state changes
Solution:
- Ensure using
collectAsStateWithLifecycle()for Flow collection:// Wrong - doesn't observe lifecycle val uiState by viewModel.uiState.collectAsState() // Correct - lifecycle-aware val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- Verify state is immutable and creates new instances:
// Wrong - mutating state data.items.add(newItem) // Correct - creating new instance _uiState.updateState { copy(items = items + newItem) }
- Check if using
remembercorrectly:// Wrong - doesn't recompose on state change val items = remember { mutableStateListOf<Item>() } // Correct - observes ViewModel state val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- For derived state, use
derivedStateOf:val filteredItems by remember { derivedStateOf { items.filter { it.isActive } } }
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/extensions/LifecycleExtensions.kt
- State Management Guide
- See also: Lifecycle Issues → Compose Recomposition Not Triggering (line 613)
Problem: UI stutters or battery drains due to too frequent recomposition
Solution:
- Use stable parameters in composables:
// Wrong - lambda recreated on every recomposition Button(onClick = { viewModel.loadData() }) // Correct - stable reference Button(onClick = viewModel::loadData)
- Mark data classes as stable when appropriate:
@Immutable data class ScreenData(val items: List<Item>)
- Use
keyparameter in lists to prevent unnecessary recomposition:LazyColumn { items(items = jetpacks, key = { it.id }) { jetpack -> JetpackCard(jetpack) } }
- Use
derivedStateOffor computed values:val filteredItems by remember { derivedStateOf { items.filter { it.isActive } } } - Avoid reading state that doesn't affect UI:
// Wrong - unnecessary recomposition on timestamp change Text("Updated: ${state.lastUpdateTimestamp}") // Better - only show meaningful state Text("Items: ${state.items.size}")
- Use Layout Inspector to identify recomposition hotspots:
- Android Studio → View → Tool Windows → Layout Inspector
- Enable "Show Recomposition Counts"
References:
- Performance Guide
- See also: Memory Issues → Compose Recomposing Too Often (line 1295)
Problem: Compose previews don't render or show errors
Solution:
- Ensure using Android Studio Hedgehog (2023.1.1) or newer
- Enable Compose Preview features:
- Settings → Experimental → Compose
- Enable "Live Edit of Literals"
- Verify preview annotations are correct:
@PreviewDevices @PreviewThemes @Composable private fun HomeScreenPreview() { JetpackTheme { HomeScreen( screenData = HomeScreenData(), onAction = {} ) } }
- Ensure preview composables are private (not public)
- Refresh preview (toolbar icon or Ctrl+Shift+F5 / Cmd+Shift+F5)
- If still failing, try:
- Build → Refresh All Previews
- File → Invalidate Caches / Restart
- Clean and rebuild project
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/PreviewDevices.kt
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/PreviewThemes.kt
- See also: Development Environment Issues → Compose Preview Not Working (line 1020)
Problem: Preview doesn't reflect light/dark theme correctly
Solution:
- Use
@PreviewThemesannotation (includes both light and dark):@PreviewThemes @Composable private fun MyScreenPreview() { JetpackTheme { // Theme wrapper required MyScreen(...) } }
- For manual theme control, use
uiModeparameter:@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun MyScreenDarkPreview() { JetpackTheme { MyScreen(...) } }
- Always wrap preview content in
JetpackTheme { } - Check if using correct
@PreviewThemesannotation:// Correct - custom multi-preview annotation @PreviewThemes // Defined in core/ui // Not - standard Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/PreviewThemes.kt
- core/ui/src/main/kotlin/dev/atick/core/ui/theme/Theme.kt
Problem: Preview shows placeholder data, not actual ViewModel state
Solution:
- This is expected behavior - previews should use fake data
- For preview data, create sample data objects:
@PreviewDevices @PreviewThemes @Composable private fun HomeScreenPreview() { JetpackTheme { HomeScreen( screenData = HomeScreenData( items = listOf( Item(id = "1", name = "Preview Item 1"), Item(id = "2", name = "Preview Item 2") ) ), onAction = {} // No-op for preview ) } }
- For complex preview data, create preview data factories:
object PreviewData { val sampleItems = listOf( Item(id = "1", name = "Item 1"), Item(id = "2", name = "Item 2") ) } @PreviewThemes @Composable private fun HomeScreenPreview() { JetpackTheme { HomeScreen( screenData = HomeScreenData(items = PreviewData.sampleItems), onAction = {} ) } }
- Never access ViewModel in preview composables
- This is why Screen composables are separated from Route composables
References:
- State Management Guide
- feature/home/src/main/kotlin/dev/atick/feature/home/ui/home/HomeScreen.kt
Problem: Scrolling through lists is janky or slow
Solution:
- Always provide
keyparameter:LazyColumn { items(items = jetpacks, key = { it.id }) { jetpack -> JetpackCard(jetpack) } }
- Use
contentTypefor heterogeneous lists:items( items = items, key = { it.id }, contentType = { it.type } // Helps Compose reuse compositions ) { item -> when (item.type) { "header" -> HeaderItem(item) "content" -> ContentItem(item) } } - Avoid heavy computations in item composables:
// Wrong - computation in composable JetpackCard( jetpack = jetpack, formattedDate = formatDate(jetpack.timestamp) // Recomputed on every scroll ) // Correct - computation in data layer data class Jetpack( val id: String, val timestamp: Long, val formattedDate: String // Pre-computed )
- Use
Modifier.drawWithCachefor custom drawing:Modifier.drawWithCache { val path = Path() // Cached between recompositions onDrawBehind { drawPath(path, color) } }
- Check for image loading issues (see Memory Issues → Image Loading)
References:
- feature/home/src/main/kotlin/dev/atick/feature/home/ui/home/HomeScreen.kt:104
- Performance Guide
- See also: Memory Issues → Large List Performance Issues (line 1250)
Problem: UI animation stutters or drops frames
Solution:
- Use
animateFloatAsStatefor smooth animations:val scale by animateFloatAsState( targetValue = if (isPressed) 0.95f else 1f, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) )
- Avoid heavy operations during composition:
// Wrong - heavy operation in composition val result = heavyComputation(data) // Correct - use LaunchedEffect var result by remember { mutableStateOf<Result?>(null) } LaunchedEffect(data) { result = withContext(Dispatchers.Default) { heavyComputation(data) } }
- Profile with Android Studio Profiler:
- View → Tool Windows → Profiler
- Check CPU usage during jank
- Identify slow composables
- Use Layout Inspector to check composition counts:
- Enable "Show Recomposition Counts"
- Identify composables recomposing too frequently
- Consider using
Modifier.graphicsLayerfor transform animations:Modifier.graphicsLayer { scaleX = scale scaleY = scale }
References:
Problem:
State stored with remember resets unexpectedly
Solution:
- For configuration changes (rotation), use
rememberSaveable:// Wrong - lost on rotation var searchQuery by remember { mutableStateOf("") } // Correct - survives rotation var searchQuery by rememberSaveable { mutableStateOf("") }
- For complex objects, provide custom Saver:
val customSaver = Saver<CustomState, Bundle>( save = { state -> Bundle().apply { putString("key", state.value) } }, restore = { bundle -> CustomState(bundle.getString("key") ?: "") } ) val state by rememberSaveable(stateSaver = customSaver) { mutableStateOf(CustomState()) }
- For screen-level state, use ViewModel instead:
@HiltViewModel class MyViewModel @Inject constructor() : ViewModel() { private val _uiState = MutableStateFlow(UiState(ScreenData())) val uiState = _uiState.asStateFlow() }
References:
Problem:
LaunchedEffect executes more than expected
Solution:
- Check key parameters - effect relaunches when keys change:
// Runs on every recomposition (Unit key never changes after first run) LaunchedEffect(Unit) { // Runs once } // Runs every time userId changes LaunchedEffect(userId) { loadUserData(userId) }
- For one-time effects, use
Unitortrueas key:LaunchedEffect(Unit) { // Runs only once analytics.logScreenView("home") }
- For multiple dependencies, use multiple keys:
LaunchedEffect(userId, categoryId) { // Runs when either userId or categoryId changes loadData(userId, categoryId) }
- Avoid using mutable state as keys unless intended:
// Wrong - relaunches on every state change LaunchedEffect(uiState) { // This is almost never what you want } // Correct - specific property LaunchedEffect(uiState.userId) { loadUserData(uiState.userId) }
References:
Error:
Step 'licenseHeaderFile' found problem in 'src/main/kotlin/MyFile.kt':
License header mismatch
Solution:
- Run Spotless Apply to auto-fix:
./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache
- Manually add copyright header from
spotless/copyright.kt:/* * Copyright 2023 Atick Faisal * * Licensed under the Apache License, Version 2.0 (the "License"); * ... */
- For custom copyright, modify files in
spotless/directory
References:
- gradle/init.gradle.kts:47
- spotless/copyright.kt
- Spotless Setup Guide
Error:
Step 'ktlint' found problem in 'MyFile.kt':
Exceeded max line length (120)
Solution:
- Run Spotless Apply to auto-fix most issues:
./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache
- For line length violations, break lines appropriately:
// Too long fun myFunction(param1: String, param2: String, param3: String, param4: String): Result<Data> // Fixed fun myFunction( param1: String, param2: String, param3: String, param4: String ): Result<Data>
- For Compose-specific violations, follow custom rules from
io.nlopez.compose.rules:ktlint
References:
- gradle/init.gradle.kts:38-46
- .editorconfig
- Spotless Setup Guide
Error (GitHub Actions):
Task :spotlessCheck FAILED
Solution:
- Run Spotless Check locally before pushing:
./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
- Fix issues with Spotless Apply:
./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache
- Commit and push fixes
- Best Practice: Set up pre-commit hook to run
spotlessApply
References:
- .github/workflows/ci.yml:35-36
- Spotless Setup Guide
Problem: Compose previews don't render or show errors
Solution:
- Ensure using Android Studio Hedgehog or newer
- Enable Compose Preview:
- Settings → Experimental → Compose
- Enable "Live Edit of Literals"
- Verify preview annotations are correct:
@PreviewDevices @PreviewThemes @Composable private fun MyScreenPreview() { JetpackTheme { MyScreen(...) } }
- Refresh preview (toolbar icon or Ctrl+Shift+F5)
- If still failing, invalidate caches and restart
References:
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/PreviewDevices.kt
- core/ui/src/main/kotlin/dev/atick/core/ui/utils/PreviewThemes.kt
Problem: Gradle builds take too long
Solution:
- Verify Gradle daemon settings in
gradle.properties:org.gradle.jvmargs=-Xmx8g -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true
- Enable build cache (already configured in template)
- Use
--no-configuration-cacheflag only when necessary - Close unnecessary background processes
- Consider increasing heap size in
gradle.propertiesif you have more RAM
References:
- gradle.properties:10-28
Problem: Annotation processing slow during builds
Solution:
- Use KSP instead of Kapt (template already uses KSP for Hilt and Room)
- Verify KSP is being used:
dependencies { "ksp"(libs.dagger.hilt.compiler) // Not "kapt" } - Increase Gradle heap size if needed
- Close other IDEs/applications consuming memory
References:
- build-logic/convention/src/main/kotlin/DaggerHiltConventionPlugin.kt:35
Problem: Installation fails or emulator not detected
Solution:
- Verify emulator is running:
adb devices
- If no devices listed, restart emulator
- If multiple devices, specify target:
./gradlew installDebug -Pandroid.device=emulator-5554
- Clear app data and reinstall:
adb uninstall dev.atick.compose ./gradlew installDebug
- Check min SDK version matches emulator API level (minSdk: 24)
References:
- gradle/libs.versions.toml:68
Error:
keystore.properties file not found. Using debug key.
Solution:
- This is expected for debug builds and template usage
- For release builds, create
keystore.propertiesin project root:storePassword=your-store-password keyPassword=your-key-password keyAlias=your-key-alias storeFile=your-keystore-file.jks
- Generate keystore if needed:
- Android Studio: Build → Generate Signed Bundle/APK
- Or use command line:
keytool -genkey -v -keystore release-keystore.jks \ -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
- Place keystore in
app/directory
References:
- app/build.gradle.kts:25, 84-92
- Getting Started Guide
Error:
Missing class com.google.firebase.FirebaseApp
Solution:
- Add ProGuard rules in
app/proguard-rules.pro:-keep class com.google.firebase.** { *; } -keep class com.google.android.gms.** { *; } - For serialization issues, add:
-keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.AnnotationsKt - Test release builds thoroughly
- Check R8 full mode documentation if using
References:
- app/proguard-rules.pro
- app/build.gradle.kts:94-97
Problem: Repository errors crash app instead of showing in UI
Solution:
- Use
suspendRunCatchingin repositories:override suspend fun getData(): Result<Data> = suspendRunCatching { networkDataSource.getData() }
- Use
updateStateWithorupdateWithin ViewModels:fun loadData() { _uiState.updateStateWith { repository.getData() } }
StatefulComposablewill automatically show errors via snackbar
References:
- core/android/src/main/kotlin/dev/atick/core/android/utils/CoroutineUtils.kt
- State Management Guide
- Data Flow Guide
Error (Logcat):
java.lang.IllegalStateException: Room cannot verify the data integrity
Solution:
- For development, use destructive migration:
Room.databaseBuilder(context, AppDatabase::class.java, "database-name") .fallbackToDestructiveMigration() // Development only .build()
- For production, implement proper migrations
- Bump database version number when schema changes
- Clear app data and reinstall for testing
References:
- core/room/src/main/kotlin/dev/atick/core/room/di/DatabaseModule.kt
Problem: Sync operations don't execute
Solution:
- Verify WorkManager is initialized in
Application.onCreate():@HiltAndroidApp class JetpackApplication : Application() { override fun onCreate() { super.onCreate() Sync.initialize(context = this) } }
- Check WorkManager constraints are satisfied (network, battery, etc.)
- Verify worker is using
@HiltWorkerand@AssistedInject:@HiltWorker class SyncWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, ... ) : CoroutineWorker(appContext, workerParams)
- Check logs for WorkManager errors:
adb logcat -s WM-WorkerWrapper
References:
- sync/src/main/kotlin/dev/atick/sync/utils/Sync.kt
- sync/src/main/kotlin/dev/atick/sync/workers/SyncWorker.kt
- app/src/main/kotlin/dev/atick/compose/JetpackApplication.kt
Problem: LeakCanary reports memory leaks
Solution:
- Check ViewModel lifecycle - ensure not storing Activity/Context
- Verify Flow collection uses lifecycle-aware collectors:
val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Cancel coroutines properly in repositories
- Don't hold references to composables in ViewModel
- For known library leaks, suppress in LeakCanary config
- Disable LeakCanary in release builds (already configured)
References:
- app/build.gradle.kts:138
- core/ui/src/main/kotlin/dev/atick/core/ui/extensions/LifecycleExtensions.kt
Error (Logcat):
java.lang.OutOfMemoryError: Failed to allocate
Solution:
- Check for image loading issues - ensure using Coil properly:
// Use DynamicAsyncImage component (handles memory efficiently) DynamicAsyncImage( imageUrl = imageUrl, contentDescription = "Image", modifier = Modifier.size(200.dp) )
- Verify Coil configuration uses memory cache (already configured):
// In CoilModule.kt .memoryCache { MemoryCache.Builder(context) .maxSizePercent(0.25) // Use 25% of app memory .build() }
- For large lists, ensure using
LazyColumn/LazyRow(not regular Column/Row) - Check if loading too many high-resolution images simultaneously
- Limit image dimensions:
AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data(imageUrl) .size(800) // Limit dimensions .build() )
References:
- core/network/src/main/kotlin/dev/atick/core/network/di/CoilModule.kt
- core/ui/src/main/kotlin/dev/atick/core/ui/image/DynamicAsyncImage.kt
- Performance Guide
Problem: App lags or crashes when scrolling through large lists
Solution:
- Always use
LazyColumn/LazyRowfor lists (not Column/Row):// Wrong - loads all items at once Column { items.forEach { item -> ItemCard(item) } } // Correct - lazy loading LazyColumn { items(items) { item -> ItemCard(item) } }
- Provide
keyparameter for stable list items:LazyColumn { items(items = jetpacks, key = { it.id }) { jetpack -> JetpackCard(jetpack) } }
- Use
contentTypefor heterogeneous lists:items(items, key = { it.id }, contentType = { it.type }) { item -> // Compose can reuse layouts for same content type }
- Avoid heavy computations in list items
- Consider using
StaggeredGridfor varying item sizes
References:
- feature/home/src/main/kotlin/dev/atick/feature/home/ui/home/HomeScreen.kt:104
- Performance Guide
Problem: UI stutters or battery drains due to excessive recomposition
Solution:
- Use
derivedStateOffor computed state:val filteredItems by remember { derivedStateOf { items.filter { it.isActive } } } - Pass stable parameters to composables:
// Wrong - lambda creates new instance every recomposition Button(onClick = { viewModel.loadData() }) // Correct - stable reference Button(onClick = viewModel::loadData)
- Mark data classes as
@Stableor@Immutablewhen appropriate:@Immutable data class ScreenData(val items: List<Item>)
- Use
keyparameter in loops to prevent unnecessary recomposition - Avoid reading state in composition that doesn't affect UI
References:
Problem: Test infrastructure not yet implemented
Solution:
- Testing infrastructure is marked as Upcoming 🚧 in this template
- For now, manual testing is required
- Future updates will include:
- Unit test setup for ViewModels
- Repository tests
- UI tests with Compose Test
- You can add your own testing framework following standard Android practices
References:
- docs/guide.md:343-351
If you encounter issues not covered in this guide:
-
Check Related Guides:
- Getting Started - Setup and initial configuration
- Architecture Overview - Understanding the app structure
- State Management - State-related issues
- Navigation Deep Dive - Navigation problems
- Dependency Injection - DI issues
- Firebase Setup - Firebase-specific problems
- Spotless Setup - Code formatting issues
-
Search GitHub Issues:
- Check existing issues: GitHub Issues
- Search closed issues for solutions
-
Enable Debug Logging:
- Timber is included in this template
- Add logging to identify issues:
Timber.d("Debug message: $variable") Timber.e(throwable, "Error occurred")
-
Clean Build:
- Often resolves mysterious build issues:
./gradlew clean ./gradlew build --refresh-dependencies
- Often resolves mysterious build issues:
-
Invalidate Caches:
- Android Studio: File → Invalidate Caches / Restart
-
Report a Bug:
- If you've found a genuine issue with the template, please report it on GitHub with:
- Android Studio version
- Gradle version
- Error logs
- Steps to reproduce
- If you've found a genuine issue with the template, please report it on GitHub with: