Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Examples showing how to use the layouts provided by the [Compose Material3 Adapt
- **[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.

### Architecture
- **[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).
- **[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).
- **[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).

### Passing navigation arguments to ViewModels
- **[Basic ViewModel](app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/basic)**: Navigation arguments are passed to a ViewModel constructed using `viewModel()`
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.navigation3)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@
android:exported="true"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".modular.hilt.ModularActivity"
android:name=".modular.hilt.HiltModularActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
<activity
android:name=".modular.koin.KoinModularActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nav3Recipes"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import com.example.nav3recipes.bottomsheet.BottomSheetActivity
import com.example.nav3recipes.commonui.CommonUiActivity
import com.example.nav3recipes.conditional.ConditionalActivity
import com.example.nav3recipes.dialog.DialogActivity
import com.example.nav3recipes.modular.hilt.ModularActivity
import com.example.nav3recipes.modular.hilt.HiltModularActivity
import com.example.nav3recipes.modular.koin.KoinModularActivity
import com.example.nav3recipes.passingarguments.viewmodels.basic.BasicViewModelsActivity
import com.example.nav3recipes.passingarguments.viewmodels.hilt.HiltViewModelsActivity
import com.example.nav3recipes.passingarguments.viewmodels.koin.KoinViewModelsActivity
Expand Down Expand Up @@ -74,7 +75,8 @@ private val recipes = listOf(
Recipe("Conditional navigation", ConditionalActivity::class.java),

Heading("Architecture"),
Recipe("Modular Navigation", ModularActivity::class.java),
Recipe("Hilt - Modular Navigation", HiltModularActivity::class.java),
Recipe("Koin - Modular Navigation", KoinModularActivity::class.java),

Heading("Passing navigation arguments using ViewModels"),
Recipe("Basic", BasicViewModelsActivity::class.java),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import javax.inject.Inject
* to the rest of the app module (i.e. MainActivity) and the feature modules.
*/
@AndroidEntryPoint
class ModularActivity : ComponentActivity() {
class HiltModularActivity : ComponentActivity() {

@Inject
lateinit var navigator: Navigator
Expand Down
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.
Copy link

@danysantiago danysantiago Nov 6, 2025

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 an EntryProvider from's Koin feature extension koin-compose-navigation3 (i.e. the getEntryProvider Koin function).

*
*/
@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Initializing Koin within an Activity's onCreate method, especially with stopKoin(), is not a recommended practice for production applications. It can lead to performance issues and unpredictable behavior if multiple components rely on a stable Koin container. While this approach may be necessary for the isolated nature of these recipe examples, it's crucial to highlight that in a real-world app, startKoin should be called only once, typically in your Application class's onCreate method. Please consider adding a more prominent warning in the KDoc or a code comment to prevent developers from copying this pattern into production code.

Copy link
Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the other Koin activity, re-initializing Koin in onCreate by calling stopKoin() and startKoin() is an anti-pattern. This can cause performance degradation and unexpected behavior in a real application where a consistent dependency graph is expected. It's a best practice to initialize Koin once in the Application class. Please add a comment to warn developers against using this pattern in production code.


setEdgeToEdgeConfig()
Expand Down
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ lifecycleViewmodel = "1.0.0-alpha04"
activityCompose = "1.12.0-alpha09"
composeBom = "2025.09.01"
navigation2 = "2.9.1"
navigation3 = "1.0.0-alpha11"
navigation3 = "1.0.0-beta01"
material3 = "1.4.0"
nav3Material = "1.3.0-alpha01"
ksp = "2.2.10-2.0.2"
hilt = "2.57.1"
hiltNavigationCompose = "1.3.0"
koin = "4.1.1"
koin = "4.2.0-alpha1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand Down Expand Up @@ -63,6 +63,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-material3-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "nav3Material" }
koin-compose-viewmodel = {group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koin"}
koin-navigation3 = {group = "io.insert-koin", name = "koin-compose-navigation3", version.ref = "koin"}

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down