-
Notifications
You must be signed in to change notification settings - Fork 70
Koin Navigation3 Integration #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
149c1d8
35b26ef
c30e847
4790b17
86830bd
44a59da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 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) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| 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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| 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 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] | ||
| } | ||
|
|
||
| @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 = { 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 = onProfileClicked) { | ||
| Text("View Profile") | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 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.ext.koin.androidContext | ||
| import org.koin.android.scope.AndroidScopeComponent | ||
| import org.koin.androidx.compose.navigation3.getEntryProvider | ||
| import org.koin.androidx.scope.activityRetainedScope | ||
| import org.koin.core.annotation.KoinExperimentalAPI | ||
| import org.koin.core.context.startKoin | ||
| import org.koin.core.context.stopKoin | ||
| import org.koin.core.scope.Scope | ||
| import org.koin.mp.KoinPlatform | ||
|
|
||
| /** | ||
| * 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 defined in their own Koin modules: | ||
| * - `ConversationModule` and `ProfileModule` declare navigation entries for their screens. | ||
| * | ||
| * A shared `Navigator` class manages the backstack. | ||
| * | ||
| * The `appModule` includes the feature modules, creates the `Navigator` with a start destination, | ||
| * and makes it available for injection into the `KoinModularActivity` and feature modules. | ||
| * | ||
| */ | ||
| @OptIn(KoinExperimentalAPI::class) | ||
| class KoinModularActivity : ComponentActivity(), AndroidScopeComponent { | ||
|
|
||
| override val scope : Scope by activityRetainedScope() | ||
| val navigator: Navigator by inject() | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| //prevent any already launched Koin instance with other config | ||
| if (KoinPlatform.getKoinOrNull() != null) { | ||
| stopKoin() | ||
| } | ||
| // The startKoin block should be placed in Application.onCreate. | ||
| startKoin { | ||
| androidContext(this@KoinModularActivity) | ||
| modules(appModule) | ||
| } | ||
|
Comment on lines
+46
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initializing Koin within an Activity's
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this part is to help local sample, of course not advised to to so in normal app setup |
||
|
|
||
| setEdgeToEdgeConfig() | ||
| setContent { | ||
| Scaffold { paddingValues -> | ||
| NavDisplay( | ||
| backStack = navigator.backStack, | ||
| modifier = Modifier.padding(paddingValues), | ||
| onBack = { navigator.goBack() }, | ||
| entryProvider = getEntryProvider() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| 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 | ||
|
|
||
| @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 | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,9 +20,12 @@ import com.example.nav3recipes.ui.setEdgeToEdgeConfig | |
| import org.koin.android.ext.koin.androidContext | ||
| import org.koin.compose.viewmodel.koinViewModel | ||
| import org.koin.core.context.GlobalContext | ||
| import org.koin.core.context.startKoin | ||
| import org.koin.core.context.stopKoin | ||
| import org.koin.core.module.dsl.viewModelOf | ||
| import org.koin.core.parameter.parametersOf | ||
| import org.koin.dsl.module | ||
| import org.koin.mp.KoinPlatform | ||
|
|
||
| /** | ||
| * Passing navigation arguments to a Koin injected ViewModel | ||
|
|
@@ -37,16 +40,18 @@ class KoinViewModelsActivity : ComponentActivity() { | |
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
|
|
||
| //prevent any already launched Koin instance with other config | ||
| if (KoinPlatform.getKoinOrNull() != null) { | ||
| stopKoin() | ||
| } | ||
| // The startKoin block should be placed in Application.onCreate. | ||
| if (GlobalContext.getOrNull() == null) { | ||
| GlobalContext.startKoin { | ||
| androidContext(this@KoinViewModelsActivity) | ||
| modules( | ||
| module { | ||
| viewModelOf(::RouteBViewModel) | ||
| } | ||
| ) | ||
| } | ||
| startKoin { | ||
| androidContext(this@KoinViewModelsActivity) | ||
| modules( | ||
| module { | ||
| viewModelOf(::RouteBViewModel) | ||
| } | ||
| ) | ||
| } | ||
|
Comment on lines
+43
to
55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the other Koin activity, re-initializing Koin in |
||
|
|
||
| setEdgeToEdgeConfig() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also add one more sentence about how the
NavDisplay's is then setup with anEntryProviderfrom's Koin feature extensionkoin-compose-navigation3(i.e. thegetEntryProviderKoin function).