Skip to content

Commit 149c1d8

Browse files
setup base usage + fix start
1 parent e14eaed commit 149c1d8

File tree

9 files changed

+334
-13
lines changed

9 files changed

+334
-13
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@
9898
android:exported="true"
9999
android:theme="@style/Theme.Nav3Recipes"/>
100100
<activity
101-
android:name=".modular.hilt.ModularActivity"
101+
android:name=".modular.hilt.HiltModularActivity"
102+
android:exported="true"
103+
android:label="@string/app_name"
104+
android:theme="@style/Theme.Nav3Recipes"/>
105+
<activity
106+
android:name=".modular.koin.KoinModularActivity"
102107
android:exported="true"
103108
android:label="@string/app_name"
104109
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
@@ -33,7 +33,8 @@ import com.example.nav3recipes.bottomsheet.BottomSheetActivity
3333
import com.example.nav3recipes.commonui.CommonUiActivity
3434
import com.example.nav3recipes.conditional.ConditionalActivity
3535
import com.example.nav3recipes.dialog.DialogActivity
36-
import com.example.nav3recipes.modular.hilt.ModularActivity
36+
import com.example.nav3recipes.modular.hilt.HiltModularActivity
37+
import com.example.nav3recipes.modular.koin.KoinModularActivity
3738
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
3839
import com.example.nav3recipes.passingarguments.viewmodels.hilt.HiltViewModelsActivity
3940
import com.example.nav3recipes.passingarguments.viewmodels.koin.KoinViewModelsActivity
@@ -74,7 +75,8 @@ private val recipes = listOf(
7475
Recipe("Conditional navigation", ConditionalActivity::class.java),
7576

7677
Heading("Architecture"),
77-
Recipe("Modular Navigation", ModularActivity::class.java),
78+
Recipe("Hilt - Modular Navigation", HiltModularActivity::class.java),
79+
Recipe("Koin - Modular Navigation", KoinModularActivity::class.java),
7880

7981
Heading("Passing navigation arguments using ViewModels"),
8082
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
@@ -27,7 +27,7 @@ import javax.inject.Inject
2727
* to the rest of the app module (i.e. MainActivity) and the feature modules.
2828
*/
2929
@AndroidEntryPoint
30-
class ModularActivity : ComponentActivity() {
30+
class HiltModularActivity : ComponentActivity() {
3131

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

app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/koin/KoinViewModelsActivity.kt

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import com.example.nav3recipes.ui.setEdgeToEdgeConfig
2020
import org.koin.android.ext.koin.androidContext
2121
import org.koin.compose.viewmodel.koinViewModel
2222
import org.koin.core.context.GlobalContext
23+
import org.koin.core.context.startKoin
24+
import org.koin.core.context.stopKoin
2325
import org.koin.core.module.dsl.viewModelOf
2426
import org.koin.core.parameter.parametersOf
2527
import org.koin.dsl.module
28+
import org.koin.mp.KoinPlatform
2629

2730
/**
2831
* Passing navigation arguments to a Koin injected ViewModel
@@ -37,16 +40,18 @@ class KoinViewModelsActivity : ComponentActivity() {
3740

3841
override fun onCreate(savedInstanceState: Bundle?) {
3942

43+
//prevent any already launched Koin instance with other config
44+
if (KoinPlatform.getKoinOrNull() != null) {
45+
stopKoin()
46+
}
4047
// The startKoin block should be placed in Application.onCreate.
41-
if (GlobalContext.getOrNull() == null) {
42-
GlobalContext.startKoin {
43-
androidContext(this@KoinViewModelsActivity)
44-
modules(
45-
module {
46-
viewModelOf(::RouteBViewModel)
47-
}
48-
)
49-
}
48+
startKoin {
49+
androidContext(this@KoinViewModelsActivity)
50+
modules(
51+
module {
52+
viewModelOf(::RouteBViewModel)
53+
}
54+
)
5055
}
5156

5257
setEdgeToEdgeConfig()

0 commit comments

Comments
 (0)