Skip to content

Commit 60e7d39

Browse files
authored
Merge pull request #30 from android/dt/scalable-recipe
Add modular/scalable recipe
2 parents 278e5f8 + 57fbff1 commit 60e7d39

File tree

9 files changed

+273
-21
lines changed

9 files changed

+273
-21
lines changed

.idea/gradle.xml

Lines changed: 0 additions & 19 deletions
This file was deleted.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ These are the recipes and what they demonstrate.
1919
- **[Common navigation UI](app/src/main/java/com/example/nav3recipes/commonui)**: A common navigation toolbar where each item in the toolbar navigates to a top level destination.
2020
- **[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.
2121

22+
**Architecture**
23+
- **[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).
24+
2225
**Passing navigation arguments to ViewModels**
2326
- **[Basic ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/basicviewmodels)**: Navigation arguments are passed to a ViewModel constructed using `viewModel()`
2427
- **[Hilt injected ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/injectedviewmodels)**: Navigation arguments are passed to a ViewModel constructed using `hiltViewModel()`

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
android:name=".passingarguments.injectedviewmodels.InjectedViewModelsActivity"
8686
android:exported="true"
8787
android:theme="@style/Theme.Nav3Recipes"/>
88+
<activity
89+
android:name=".modular.hilt.ModularActivity"
90+
android:exported="true"
91+
android:label="@string/app_name"
92+
android:theme="@style/Theme.Nav3Recipes"/>
8893
</application>
8994

90-
</manifest>
95+
</manifest>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.example.nav3recipes.basicdsl.BasicDslActivity
2828
import com.example.nav3recipes.basicsaveable.BasicSaveableActivity
2929
import com.example.nav3recipes.commonui.CommonUiActivity
3030
import com.example.nav3recipes.conditional.ConditionalActivity
31+
import com.example.nav3recipes.modular.hilt.ModularActivity
3132
import com.example.nav3recipes.passingarguments.basicviewmodels.BasicViewModelsActivity
3233
import com.example.nav3recipes.passingarguments.injectedviewmodels.InjectedViewModelsActivity
3334
import com.example.nav3recipes.scenes.materiallistdetail.MaterialListDetailActivity
@@ -52,7 +53,8 @@ private val recipes = listOf(
5253
Recipe("Material list-detail layout", MaterialListDetailActivity::class.java),
5354
Recipe("Two pane layout", TwoPaneActivity::class.java),
5455
Recipe("Argument passing to basic ViewModel", BasicViewModelsActivity::class.java),
55-
Recipe("Argument passing to injected ViewModel", InjectedViewModelsActivity::class.java)
56+
Recipe("Argument passing to injected ViewModel", InjectedViewModelsActivity::class.java),
57+
Recipe("Modular Navigation", ModularActivity::class.java),
5658
)
5759

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

0 commit comments

Comments
 (0)