Android: Navigation, Coroutines & Dependency Injection
Navigation Component (Compose)
// implementation("androidx.navigation:navigation-compose:2.7.7")
// Define destinations
sealed class Screen(val route: String) {
object ArticleList : Screen("articles")
object ArticleDetail : Screen("articles/{id}") {
fun createRoute(id: Int) = "articles/$id"
}
object Settings : Screen("settings")
}
// NavHost — root of the navigation graph
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.ArticleList.route) {
composable(Screen.ArticleList.route) {
ArticlesScreen(
onArticleClick = { id ->
navController.navigate(Screen.ArticleDetail.createRoute(id))
}
)
}
composable(
route = Screen.ArticleDetail.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
val id = backStackEntry.arguments?.getInt("id") ?: return@composable
ArticleDetailScreen(articleId = id, onBack = { navController.popBackStack() })
}
}
}
// Navigate and pass data back
navController.navigate(route) {
popUpTo(Screen.ArticleList.route) { inclusive = false } // clear back stack
launchSingleTop = true // avoid duplicate screens
}Coroutines
import kotlinx.coroutines.*
// viewModelScope — tied to ViewModel lifecycle
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
repository.fetchData() // blocking I/O off main thread
}
_uiState.update { it.copy(data = result) }
}
// Launch concurrent work
viewModelScope.launch {
val deferredA = async { api.fetchUsers() }
val deferredB = async { api.fetchPosts() }
val (users, posts) = deferredA.await() to deferredB.await()
}
// Flow operators
repository.articles
.filter { it.isNotEmpty() }
.map { articles -> articles.sortedByDescending { it.createdAt } }
.distinctUntilChanged()
.onEach { Log.d("TAG", "Articles updated: ${it.size}") }
.collect { articles -> _uiState.update { it.copy(articles = articles) } }
// Dispatchers:
// Dispatchers.Main — UI thread
// Dispatchers.IO — I/O (network, disk)
// Dispatchers.Default — CPU-intensive (parsing, sorting)Hilt Dependency Injection
// plugins: id("com.google.dagger.hilt.android")
// implementation("com.google.dagger:hilt-android:2.51")
// kapt("com.google.dagger:hilt-android-compiler:2.51")
@HiltAndroidApp
class MyApplication : Application()
// Provide dependencies
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
}
@Provides @Singleton
fun provideArticleDao(database: AppDatabase): ArticleDao = database.articleDao()
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// Inject into ViewModel
@HiltViewModel
class ArticlesViewModel @Inject constructor(
private val repository: ArticleRepository
) : ViewModel()
// Inject into Activity/Fragment
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var analytics: Analytics
}Key Architecture Patterns
MVVM + Clean Architecture: UI layer (Compose) → ViewModel → Repository → Data sources (Room, Retrofit).
Single source of truth: Room DB as the local truth; network data flows through DB before reaching UI.
UiState sealed class or data class: represent Loading/Success/Error states explicitly.
collectAsStateWithLifecycle: preferred over collectAsState — stops collecting when screen is in background.
SavedStateHandle: persist ViewModel state across process death. Inject via @HiltViewModel.
Use sealed interfaces for UI events (one-shot actions like navigation, snackbars) separately from UI state.
Preview parameters: use @PreviewParameter to show composables with sample data in Android Studio.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free