Công thức điều hướng theo mô-đun (Hilt)
Công thức này minh hoạ cách cấu trúc một ứng dụng nhiều mô-đun bằng Navigation 3 và Dagger/Hilt để chèn phần phụ thuộc. Mục tiêu là tạo một kiến trúc tách biệt, trong đó quá trình điều hướng được xác định và triển khai trong các mô-đun tính năng riêng biệt.
Cách hoạt động
Ứng dụng được chia thành một số mô-đun:
-
Mô-đun
app: Đây là mô-đun ứng dụng chính. Mô-đun này cung cấpHiltDeepLinkActivity(điểm truy cập minh hoạ việc khởi chạy ý định liên kết sâu và ý định thông thường),HiltModularActivity(lưu trữNavDisplayvà uỷ quyền ý định liên kết sâu choNavigator.handleDeepLink(intent)), liên kết đích đến bắt đầu và tạoentryProvidercuối cùng. -
Mô-đun
common: Mô-đun này chứa logic điều hướng cốt lõi, bao gồm:- Lớp
Navigatorquản lý ngăn xếp lui, phân giải ý định liên kết sâu đến và tạo ngăn xếp lui tổng hợp thông quahandleDeepLink(intent). startDestination: NavKeydùng để khởi chạy ngăn xếp lui.- Loại
EntryProviderInstaller. Đây là một hàm mà các mô-đun tính năng dùng để đóng góp các mục điều hướng của chúng vàoentryProvidercủa ứng dụng. BackStackMatcher<*, NavKey>mà các mô-đun tính năng dùng để đóng góp các trình so khớp liên kết sâu của chúng với ngăn xếp lui tổng hợp.
- Lớp
-
Mô-đun tính năng (ví dụ:
conversation,profile): Mỗi tính năng được chia thành 2 mô-đun con:- Mô-đun
api: Xác định API công khai cho tính năng, bao gồm cả các tuyến điều hướng và mẫu URI liên kết sâu. Nhờ đó, các mô-đun khác có thể chuyển đến tính năng này mà không cần biết thông tin triển khai của tính năng đó. implmô-đun: Cung cấp thông tin triển khai của tính năng, bao gồm cả các thành phần kết hợp,EntryProviderInstalleránh xạ các tuyến của tính năng đến các thành phần kết hợp vàBackStackMatcher<*, NavKey>được định cấu hình bằng.withBackStackđể thiết lập ngăn xếp lui phân cấp thích hợp (ví dụ:[ConversationList, ConversationDetail(id)]). Cả hai đều được cung cấp bằng cách sử dụng nhiều liên kết Dagger/Hilt (@IntoSet).
- Mô-đun
Phương pháp theo mô-đun này cho phép tách biệt rõ ràng các mối quan tâm, giúp toàn bộ mã nguồn có khả năng mở rộng và bảo trì hơn. Mỗi tính năng chịu trách nhiệm về logic điều hướng riêng và mô-đun app chỉ kết hợp các phần này lại với nhau.
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 }