模块化导航配方 (Koin)
此方案演示了如何使用 Navigation 3 和 Koin 进行依赖项注入,从而构建多模块应用。目标是创建一个解耦的架构,其中导航在单独的功能模块中定义和实现。它依赖于 koin-compose-navigation3 制品。
工作原理
应用分为多个 Android 模块:
-
app模块:这是主应用模块。它会includes()功能模块并初始化一个通用Navigator。 -
common模块:此模块包含应用模块和功能模块共用的核心导航逻辑。具体而言,它定义了一个用于管理返回堆栈的Navigator类。 -
功能模块(例如,
conversation、profile):每个功能都拆分为两个子模块:api模块:定义了该功能的公共 API,包括其导航路线。这样,其他模块便可导航到此功能,而无需了解其实现细节。impl模块:提供功能实现,包括其可组合项和 KoinModule。Koin 模块使用navigationDSL 为功能模块定义入口提供程序安装程序。
这种模块化方法可以清晰地分离关注点,从而提高代码库的可伸缩性和可维护性。每个功能都负责自己的导航逻辑,而 app 模块只是将这些部分组合在一起。
package com.example.nav3recipes.modular.koin 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 org.koin.androidx.scope.dsl.activityRetainedScope import org.koin.core.annotation.KoinExperimentalAPI import org.koin.dsl.module import org.koin.dsl.navigation3.navigation // API object Profile // IMPL @OptIn(KoinExperimentalAPI::class) val profileModule = module { activityRetainedScope { navigation<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.koin 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 org.koin.androidx.scope.dsl.activityRetainedScope import org.koin.core.annotation.KoinExperimentalAPI import org.koin.dsl.module import org.koin.dsl.navigation3.navigation // API object ConversationList data class ConversationDetail(val id: Int) { val color: Color get() = colors[id % colors.size] } // IMPL @OptIn(KoinExperimentalAPI::class) val conversationModule = module { activityRetainedScope { navigation<ConversationList> { ConversationListScreen( onConversationClicked = { conversationDetail -> get<Navigator>().goTo(conversationDetail) } ) } navigation<ConversationDetail> { key -> ConversationDetailScreen(key) { get<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.koin import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList 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.koin import org.koin.androidx.scope.dsl.activityRetainedScope import org.koin.dsl.module val appModule = module { includes(profileModule,conversationModule) activityRetainedScope { scoped { Navigator(startDestination = ConversationList) } } }
package com.example.nav3recipes.modular.koin 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.ui.NavDisplay import com.example.nav3recipes.ui.setEdgeToEdgeConfig import org.koin.android.ext.android.inject import org.koin.android.scope.AndroidScopeComponent import org.koin.androidx.compose.navigation3.getEntryProvider import org.koin.androidx.scope.activityRetainedScope import org.koin.core.Koin import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.component.KoinComponent import org.koin.core.scope.Scope import org.koin.dsl.koinApplication /** * This recipe demonstrates how to use a modular approach with Navigation 3, * where different parts of the application are defined in separate modules and injected * into the main app using Koin. * * Features (Conversation and Profile) are split into two modules: * - api: defines the public facing routes for this feature * - impl: defines the entryProviders for this feature, these are injected into the app's main activity * The common module defines: * - a common navigator class that exposes a back stack and methods to modify that back stack * - a type that should be used by feature modules to inject entryProviders into the app's main activity * The app module creates the navigator by supplying a start destination and provides this navigator * to the rest of the app module (i.e. MainActivity) and the feature modules. */ @OptIn(KoinExperimentalAPI::class) class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComponent { // Local Koin Context Instance companion object { private val localKoin = koinApplication { modules(appModule) }.koin } // Override default Koin context to use the local one override fun getKoin(): Koin = localKoin override val scope : Scope by activityRetainedScope() val navigator: Navigator by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setEdgeToEdgeConfig() setContent { Scaffold { paddingValues -> NavDisplay( backStack = navigator.backStack, modifier = Modifier.padding(paddingValues), onBack = { navigator.goBack() }, entryProvider = getEntryProvider() ) } } } }