Skip to content

Commit 5bbc211

Browse files
Merge pull request #118 from arnaudgiuliani/dt/koin_nav3
Koin Navigation3 Integration
2 parents d8a6d49 + a5f455c commit 5bbc211

File tree

12 files changed

+288
-15
lines changed

12 files changed

+288
-15
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Examples showing how to use the layouts provided by the [Compose Material3 Adapt
3535
- **[Conditional navigation](app/src/main/java/com/example/nav3recipes/conditional)**: Switch to a different navigation flow when a condition is met. For example, for authentication or first-time user onboarding.
3636

3737
### Architecture
38-
- **[Modularized navigation code](app/src/main/java/com/example/nav3recipes/modular/hilt)**: Demonstrates how to decouple navigation code into separate modules (uses Dagger/Hilt for DI).
38+
- **[Hilt - Modularized navigation code](app/src/main/java/com/example/nav3recipes/modular/hilt)**: Demonstrates how to decouple navigation code into separate modules (uses Dagger/Hilt for DI).
39+
- **[Koin - Modularized navigation code](app/src/main/java/com/example/nav3recipes/modular/koin)**: Demonstrates how to decouple navigation code into separate modules (uses Koin for DI).
3940

4041
### Passing navigation arguments to ViewModels
4142
- **[Basic ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/basic)**: Navigation arguments are passed to a ViewModel constructed using `viewModel()`

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ dependencies {
8585
implementation(libs.hilt.android)
8686
ksp(libs.hilt.compiler)
8787
implementation(libs.koin.compose.viewmodel)
88+
implementation(libs.koin.navigation3)
8889

8990
testImplementation(libs.junit)
9091
androidTestImplementation(libs.androidx.junit)

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@
9494
android:exported="true"
9595
android:theme="@style/Theme.Nav3Recipes"/>
9696
<activity
97-
android:name=".modular.hilt.ModularActivity"
97+
android:name=".modular.hilt.HiltModularActivity"
98+
android:exported="true"
99+
android:label="@string/app_name"
100+
android:theme="@style/Theme.Nav3Recipes"/>
101+
<activity
102+
android:name=".modular.koin.KoinModularActivity"
98103
android:exported="true"
99104
android:label="@string/app_name"
100105
android:theme="@style/Theme.Nav3Recipes"/>

app/src/main/java/com/example/nav3recipes/RecipePickerActivity.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ import com.example.nav3recipes.conditional.ConditionalActivity
5151
import com.example.nav3recipes.dialog.DialogActivity
5252
import com.example.nav3recipes.material.listdetail.MaterialListDetailActivity
5353
import com.example.nav3recipes.material.supportingpane.MaterialSupportingPaneActivity
54-
import com.example.nav3recipes.modular.hilt.ModularActivity
5554
import com.example.nav3recipes.multiplestacks.MultipleStacksActivity
55+
import com.example.nav3recipes.modular.hilt.HiltModularActivity
56+
import com.example.nav3recipes.modular.koin.KoinModularActivity
5657
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
5758
import com.example.nav3recipes.passingarguments.viewmodels.hilt.HiltViewModelsActivity
5859
import com.example.nav3recipes.passingarguments.viewmodels.koin.KoinViewModelsActivity
@@ -97,7 +98,8 @@ private val recipes = listOf(
9798
Recipe("Conditional navigation", ConditionalActivity::class.java),
9899

99100
Heading("Architecture"),
100-
Recipe("Modular Navigation", ModularActivity::class.java),
101+
Recipe("Hilt - Modular Navigation", HiltModularActivity::class.java),
102+
Recipe("Koin - Modular Navigation", KoinModularActivity::class.java),
101103

102104
Heading("Passing navigation arguments using ViewModels"),
103105
Recipe("Basic", BasicViewModelsActivity::class.java),

app/src/main/java/com/example/nav3recipes/modular/hilt/ModularActivity.kt renamed to app/src/main/java/com/example/nav3recipes/modular/hilt/HiltModularActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import dagger.hilt.android.AndroidEntryPoint
1313
import javax.inject.Inject
1414

1515
@AndroidEntryPoint
16-
class ModularActivity : ComponentActivity() {
16+
class HiltModularActivity : ComponentActivity() {
1717

1818
@Inject
1919
lateinit var navigator: Navigator
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.example.nav3recipes.modular.koin
2+
3+
import org.koin.androidx.scope.dsl.activityRetainedScope
4+
import org.koin.dsl.module
5+
6+
val appModule = module {
7+
includes(profileModule,conversationModule)
8+
9+
activityRetainedScope {
10+
scoped {
11+
Navigator(startDestination = ConversationList)
12+
}
13+
}
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.example.nav3recipes.modular.koin
2+
3+
import androidx.compose.runtime.mutableStateListOf
4+
import androidx.compose.runtime.snapshots.SnapshotStateList
5+
6+
class Navigator(startDestination: Any) {
7+
val backStack : SnapshotStateList<Any> = mutableStateListOf(startDestination)
8+
9+
fun goTo(destination: Any){
10+
backStack.add(destination)
11+
}
12+
13+
fun goBack(){
14+
backStack.removeLastOrNull()
15+
}
16+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.example.nav3recipes.modular.koin
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.lazy.LazyColumn
13+
import androidx.compose.material3.Button
14+
import androidx.compose.material3.ListItem
15+
import androidx.compose.material3.ListItemDefaults
16+
import androidx.compose.material3.MaterialTheme
17+
import androidx.compose.material3.Text
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.ui.Alignment
20+
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.Color
22+
import androidx.compose.ui.unit.dp
23+
import com.example.nav3recipes.ui.theme.colors
24+
import org.koin.androidx.scope.dsl.activityRetainedScope
25+
import org.koin.core.annotation.KoinExperimentalAPI
26+
import org.koin.dsl.module
27+
import org.koin.dsl.navigation3.navigation
28+
29+
// API
30+
object ConversationList
31+
data class ConversationDetail(val id: Int) {
32+
val color: Color
33+
get() = colors[id % colors.size]
34+
}
35+
36+
@OptIn(KoinExperimentalAPI::class)
37+
val conversationModule = module {
38+
activityRetainedScope {
39+
navigation<ConversationList> {
40+
ConversationListScreen(
41+
onConversationClicked = { conversationDetail ->
42+
get<Navigator>().goTo(conversationDetail)
43+
}
44+
)
45+
}
46+
47+
navigation<ConversationDetail> { key ->
48+
ConversationDetailScreen(key) {
49+
get<Navigator>().goTo(Profile)
50+
}
51+
}
52+
}
53+
}
54+
55+
@Composable
56+
private fun ConversationListScreen(
57+
onConversationClicked: (ConversationDetail) -> Unit
58+
) {
59+
LazyColumn(
60+
modifier = Modifier.fillMaxSize(),
61+
) {
62+
items(10) { index ->
63+
val conversationId = index + 1
64+
val conversationDetail = ConversationDetail(conversationId)
65+
val backgroundColor = conversationDetail.color
66+
ListItem(
67+
modifier = Modifier
68+
.fillMaxWidth()
69+
.clickable(onClick = { onConversationClicked(conversationDetail) }),
70+
headlineContent = {
71+
Text(
72+
text = "Conversation $conversationId",
73+
style = MaterialTheme.typography.headlineSmall,
74+
color = MaterialTheme.colorScheme.onSurface
75+
)
76+
},
77+
colors = ListItemDefaults.colors(
78+
containerColor = backgroundColor // Set container color directly
79+
)
80+
)
81+
}
82+
}
83+
}
84+
85+
@Composable
86+
private fun ConversationDetailScreen(
87+
conversationDetail: ConversationDetail,
88+
onProfileClicked: () -> Unit
89+
) {
90+
Column(
91+
modifier = Modifier
92+
.fillMaxSize()
93+
.background(conversationDetail.color)
94+
.padding(16.dp),
95+
horizontalAlignment = Alignment.CenterHorizontally,
96+
verticalArrangement = Arrangement.Center
97+
) {
98+
Text(
99+
text = "Conversation Detail Screen: ${conversationDetail.id}",
100+
style = MaterialTheme.typography.headlineMedium,
101+
color = MaterialTheme.colorScheme.onSurface
102+
)
103+
Spacer(modifier = Modifier.height(16.dp))
104+
Button(onClick = onProfileClicked) {
105+
Text("View Profile")
106+
}
107+
}
108+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.example.nav3recipes.modular.koin
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.material3.Scaffold
8+
import androidx.compose.ui.Modifier
9+
import androidx.navigation3.ui.NavDisplay
10+
import com.example.nav3recipes.ui.setEdgeToEdgeConfig
11+
import org.koin.android.ext.android.inject
12+
import org.koin.android.ext.koin.androidContext
13+
import org.koin.android.ext.koin.androidLogger
14+
import org.koin.android.scope.AndroidScopeComponent
15+
import org.koin.androidx.compose.navigation3.getEntryProvider
16+
import org.koin.androidx.scope.activityRetainedScope
17+
import org.koin.androidx.scope.activityScope
18+
import org.koin.core.annotation.KoinExperimentalAPI
19+
import org.koin.core.component.KoinComponent
20+
import org.koin.core.context.startKoin
21+
import org.koin.core.context.stopKoin
22+
import org.koin.core.logger.Level
23+
import org.koin.core.scope.Scope
24+
import org.koin.mp.KoinPlatform
25+
26+
/**
27+
* This recipe demonstrates how to use a modular approach with Navigation 3,
28+
* where different parts of the application are defined in separate modules and injected
29+
* into the main app using Dagger/Hilt.
30+
*
31+
* Features (Conversation and Profile) are split into two modules:
32+
* - api: defines the public facing routes for this feature
33+
* - impl: defines the entryProviders for this feature, these are injected into the app's main activity
34+
* The common module defines:
35+
* - a common navigator class that exposes a back stack and methods to modify that back stack
36+
* - a type that should be used by feature modules to inject entryProviders into the app's main activity
37+
* The app module creates the navigator by supplying a start destination and provides this navigator
38+
* to the rest of the app module (i.e. MainActivity) and the feature modules.
39+
*/
40+
@OptIn(KoinExperimentalAPI::class)
41+
class KoinModularActivity : ComponentActivity(), AndroidScopeComponent {
42+
43+
override val scope : Scope by activityRetainedScope()
44+
val navigator: Navigator by inject()
45+
46+
override fun onCreate(savedInstanceState: Bundle?) {
47+
super.onCreate(savedInstanceState)
48+
49+
initKoin()
50+
51+
setEdgeToEdgeConfig()
52+
setContent {
53+
Scaffold { paddingValues ->
54+
NavDisplay(
55+
backStack = navigator.backStack,
56+
modifier = Modifier.padding(paddingValues),
57+
onBack = { navigator.goBack() },
58+
entryProvider = getEntryProvider()
59+
)
60+
}
61+
}
62+
}
63+
64+
private fun initKoin() {
65+
if (KoinPlatform.getKoinOrNull() == null) {
66+
// The startKoin block should be placed in Application.onCreate.
67+
startKoin {
68+
androidContext(this@KoinModularActivity)
69+
androidLogger(Level.DEBUG)
70+
modules(appModule)
71+
}
72+
}
73+
}
74+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.example.nav3recipes.modular.koin
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.material3.MaterialTheme
9+
import androidx.compose.material3.Text
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Alignment
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.unit.dp
14+
import org.koin.androidx.scope.dsl.activityRetainedScope
15+
import org.koin.core.annotation.KoinExperimentalAPI
16+
import org.koin.dsl.module
17+
import org.koin.dsl.navigation3.navigation
18+
19+
// API
20+
object Profile
21+
22+
@OptIn(KoinExperimentalAPI::class)
23+
val profileModule = module {
24+
activityRetainedScope {
25+
navigation<Profile> { ProfileScreen() }
26+
}
27+
}
28+
29+
@Composable
30+
private fun ProfileScreen() {
31+
val profileColor = MaterialTheme.colorScheme.surfaceVariant
32+
Column(
33+
modifier = Modifier
34+
.fillMaxSize()
35+
.background(profileColor)
36+
.padding(16.dp),
37+
horizontalAlignment = Alignment.CenterHorizontally,
38+
verticalArrangement = Arrangement.Center
39+
) {
40+
Text(
41+
text = "Profile Screen",
42+
style = MaterialTheme.typography.headlineMedium,
43+
color = MaterialTheme.colorScheme.onSurface
44+
)
45+
}
46+
}

0 commit comments

Comments
 (0)