Skip to content

Commit ab92cfb

Browse files
committed
Add modular/scalable recipe
1 parent 93a479a commit ab92cfb

File tree

8 files changed

+287
-2
lines changed

8 files changed

+287
-2
lines changed

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

0 commit comments

Comments
 (0)