Przepis na modułową nawigację (Hilt)
Z tego przepisu dowiesz się, jak podzielić aplikację na moduły przy użyciu Navigation 3 i Daggera/Hilta do wstrzykiwania zależności. Celem jest stworzenie rozdzielonej architektury, w której nawigacja jest definiowana i implementowana w osobnych modułach funkcji.
Jak to działa
Aplikacja jest podzielona na kilka modułów:
-
Moduł
app: jest to główny moduł aplikacji. ZawieraHiltDeepLinkActivity(punkt wejścia, który pokazuje uruchamianie intencji zwykłych i precyzyjnych linków),HiltModularActivity(który hostujeNavDisplayi przekazuje intencje precyzyjnych linków doNavigator.handleDeepLink(intent)), powiązanie miejsca docelowego początkowego i tworzy końcowyentryProvider. -
Moduł
common: ten moduł zawiera podstawową logikę nawigacji, w tym:- Klasę
Navigator, która zarządza stosem wstecznym, rozwiązuje przychodzące intencje precyzyjnych linków i tworzy syntetyczne stosy wsteczne za pomocąhandleDeepLink(intent). startDestination: NavKeyużywany do inicjowania stosu wstecznego.- Typ
EntryProviderInstaller, czyli funkcja, której moduły funkcji używają do dodawania swoich wpisów nawigacyjnych doentryProvideraplikacji. BackStackMatcher<*, NavKey>, którego moduły funkcji używają do dodawania swoich dopasowań precyzyjnych linków z syntetycznymi stosami wstecznymi.
- Klasę
-
Moduły funkcji (np.
conversation,profile): każda funkcja jest podzielona na 2 podmoduły:- Moduł
api: definiuje publiczny interfejs API funkcji, w tym jej trasy nawigacyjne i wzorce identyfikatorów URI precyzyjnych linków. Dzięki temu inne moduły mogą przechodzić do tej funkcji bez konieczności poznawania szczegółów jej implementacji. implmoduł: zawiera implementację funkcji, w tym jej elementy kompozycyjne,EntryProviderInstaller, który mapuje trasy funkcji na jej elementy kompozycyjne, orazBackStackMatcher<*, NavKey>skonfigurowany za pomocą.withBackStackw celu utworzenia odpowiedniego hierarchicznego stosu wstecznego (np.[ConversationList, ConversationDetail(id)]). Oba są udostępniane za pomocą multibindings Daggera/Hilta (@IntoSet).
- Moduł
To modułowe podejście pozwala na wyraźne rozdzielenie zadań, co sprawia, że baza kodu jest bardziej skalowalna i łatwiejsza w utrzymaniu. Każda funkcja jest odpowiedzialna za własną logikę nawigacji, a moduł app tylko łączy te elementy.
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 androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.deeplink.BackStackMatcher import androidx.navigation3.runtime.deeplink.DeepLinkUri import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher import androidx.navigation3.runtime.deeplink.withBackStack import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet import kotlinx.serialization.Serializable // API @Serializable object Profile : NavKey const val PROFILE_URI = "https://www.nav3recipes.com/profile" // IMPLEMENTATION @Module @InstallIn(ActivityRetainedComponent::class) object ProfileModule { @IntoSet @Provides fun provideEntryProviderInstaller() : EntryProviderInstaller = { entry<Profile>{ ProfileScreen() } } @IntoSet @Provides fun provideDeepLinkMatcher(): BackStackMatcher<*, NavKey> = UriDeepLinkMatcher( uriPattern = DeepLinkUri(PROFILE_URI), serializer = Profile.serializer(), ).withBackStack { matchResult -> listOf(ConversationList, matchResult.key) } } @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() navigator.handleDeepLink(intent) 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 androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.deeplink.BackStackMatcher import androidx.navigation3.runtime.deeplink.DeepLinkUri import androidx.navigation3.runtime.deeplink.UriDeepLinkMatcher import androidx.navigation3.runtime.deeplink.withBackStack 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 import kotlinx.serialization.Serializable // API object ConversationList : NavKey @Serializable data class ConversationDetail(val id: Int) : NavKey { val color: Color get() = colors[id % colors.size] } const val CONVERSATION_URI = "https://www.nav3recipes.com/conversation" // 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) } } } @IntoSet @Provides fun provideDeepLinkMatcher(): BackStackMatcher<*, NavKey> = UriDeepLinkMatcher( uriPattern = DeepLinkUri("$CONVERSATION_URI/{id}"), serializer = ConversationDetail.serializer(), ).withBackStack { matchResult -> listOf(ConversationList, matchResult.key) } } @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 android.content.Intent import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.deeplink.BackStackMatcher import androidx.navigation3.runtime.deeplink.DeepLinkRequest import androidx.navigation3.runtime.deeplink.invoke import dagger.hilt.android.scopes.ActivityRetainedScoped import javax.inject.Inject typealias EntryProviderInstaller = EntryProviderScope<NavKey>.() -> Unit @ActivityRetainedScoped class Navigator @Inject constructor( startDestination: NavKey, private val deepLinkMatchers: Set<@JvmSuppressWildcards BackStackMatcher<*, NavKey>>, ) { val backStack: SnapshotStateList<NavKey> = mutableStateListOf(startDestination) fun goTo(destination: NavKey) { backStack.add(destination) } fun handleDeepLink(intent: Intent) { if (intent.data == null && intent.extras == null) return val request = DeepLinkRequest(intent) val matchResult = deepLinkMatchers.firstNotNullOfOrNull { matcher -> matcher.match(request) } ?: return backStack.clear() backStack.addAll(matchResult.backStack) } fun goBack() { backStack.removeLastOrNull() } }
package com.example.nav3recipes.modular.hilt import androidx.navigation3.runtime.NavKey import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent @Module @InstallIn(ActivityRetainedComponent::class) object AppModule { @Provides fun provideStartDestination(): NavKey = ConversationList }