Modular Navigation Recipe (Hilt)
Questa ricetta mostra come strutturare un'applicazione multimodulo utilizzando Navigation 3 e Dagger/Hilt per l'inserimento delle dipendenze. L'obiettivo è creare un'architettura disaccoppiata in cui la navigazione è definita e implementata in moduli delle funzionalità separati.
Come funziona
L'applicazione è suddivisa in diversi moduli:
-
Modulo
app: questo è il modulo principale dell'applicazione. Inizializza unNavigatorcomune e inserisce un insieme diEntryProviderInstallerdai moduli delle funzionalità. Poi utilizza questi programmi di installazione per creare ilentryProviderfinale perNavDisplay. -
Modulo
common: questo modulo contiene la logica di navigazione principale, tra cui:- Una classe
Navigatorche gestisce lo stack delle attività precedenti. - Un tipo
EntryProviderInstaller, ovvero una funzione che i moduli delle funzionalità utilizzano per contribuire con le proprie voci di navigazione all'entryProviderdell'applicazione.
- Una classe
-
Moduli delle funzionalità (ad es.
conversation,profile): ogni funzionalità è suddivisa in due moduli secondari:- Modulo
api: definisce l'API pubblica per la funzionalità, incluse le relative route di navigazione. In questo modo, altri moduli possono passare a questa funzionalità senza dover conoscere i dettagli della sua implementazione. - Modulo
impl: fornisce l'implementazione della funzionalità, inclusi i relativi composable e unEntryProviderInstallerche mappa le route della funzionalità ai relativi composable. Questo programma di installazione viene quindi fornito al moduloapputilizzando Dagger/Hilt.
- Modulo
Questo approccio modulare consente una chiara separazione delle responsabilità, rendendo la base di codice più scalabile e gestibile. Ogni funzionalità è responsabile della propria logica di navigazione e il modulo app combina solo questi elementi.
package com.example.nav3recipes.modular.hilt import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet // API object Profile // IMPLEMENTATION @Module @InstallIn(ActivityRetainedComponent::class) object ProfileModule { @IntoSet @Provides fun provideEntryProviderInstaller() : EntryProviderInstaller = { entry<Profile>{ ProfileScreen() } } } @Composable private fun ProfileScreen() { val profileColor = MaterialTheme.colorScheme.surfaceVariant Column( modifier = Modifier .fillMaxSize() .background(profileColor) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Profile Screen", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface ) } }
package com.example.nav3recipes.modular.hilt import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.ui.setEdgeToEdgeConfig import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class HiltModularActivity : ComponentActivity() { @Inject lateinit var navigator: Navigator @Inject lateinit var entryProviderScopes: Set<@JvmSuppressWildcards EntryProviderInstaller> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setEdgeToEdgeConfig() setContent { Scaffold { paddingValues -> NavDisplay( backStack = navigator.backStack, modifier = Modifier.padding(paddingValues), onBack = { navigator.goBack() }, entryProvider = entryProvider { entryProviderScopes.forEach { builder -> this.builder() } } ) } } } }
package com.example.nav3recipes.modular.hilt import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Button import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import com.example.nav3recipes.ui.theme.colors import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet // API object ConversationList data class ConversationDetail(val id: Int) { val color: Color get() = colors[id % colors.size] } // IMPL @Module @InstallIn(ActivityRetainedComponent::class) object ConversationModule { @IntoSet @Provides fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { entry<ConversationList> { ConversationListScreen( onConversationClicked = { conversationDetail -> navigator.goTo(conversationDetail) } ) } entry<ConversationDetail> { key -> ConversationDetailScreen(key) { navigator.goTo(Profile) } } } } @Composable private fun ConversationListScreen( onConversationClicked: (ConversationDetail) -> Unit ) { LazyColumn( modifier = Modifier.fillMaxSize(), ) { items(10) { index -> val conversationId = index + 1 val conversationDetail = ConversationDetail(conversationId) val backgroundColor = conversationDetail.color ListItem( modifier = Modifier .fillMaxWidth() .clickable(onClick = dropUnlessResumed { onConversationClicked(conversationDetail) }), headlineContent = { Text( text = "Conversation $conversationId", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface ) }, colors = ListItemDefaults.colors( containerColor = backgroundColor // Set container color directly ) ) } } } @Composable private fun ConversationDetailScreen( conversationDetail: ConversationDetail, onProfileClicked: () -> Unit ) { Column( modifier = Modifier .fillMaxSize() .background(conversationDetail.color) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Conversation Detail Screen: ${conversationDetail.id}", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = dropUnlessResumed(block = onProfileClicked)) { Text("View Profile") } } }
package com.example.nav3recipes.modular.hilt import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.navigation3.runtime.EntryProviderScope import dagger.hilt.android.scopes.ActivityRetainedScoped typealias EntryProviderInstaller = EntryProviderScope<Any>.() -> Unit @ActivityRetainedScoped class Navigator(startDestination: Any) { val backStack : SnapshotStateList<Any> = mutableStateListOf(startDestination) fun goTo(destination: Any){ backStack.add(destination) } fun goBack(){ backStack.removeLastOrNull() } }
package com.example.nav3recipes.modular.hilt import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.scopes.ActivityRetainedScoped @Module @InstallIn(ActivityRetainedComponent::class) object AppModule { @Provides @ActivityRetainedScoped fun provideNavigator() : Navigator = Navigator(startDestination = ConversationList) }