Android
03 / 03

Navigation, Coroutines & Dependency Injection

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