Skip to content

Commit bb208ed

Browse files
author
Josh
committed
more progress on nav3 migration
1 parent d2d9f2c commit bb208ed

29 files changed

+785
-699
lines changed

app/.editorconfig

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
root = true
22

33
[*.{kt,kts}]
4-
ktlint_code_style = ktlint_official
5-
ktlint_function_naming_ignore_when_annotated_with=Composable
64
max_line_length = 160
7-
8-
## Testing rules
9-
[**/{test,androidTest,commonTest}/**.kt]
10-
max_line_length = off
11-
spacing-between-declarations-with-comments = off
5+
ktlint_code_style = ktlint_official
6+
ktlint_standard_chain-method-continuation = disabled
7+
ktlint_standard_multiline-expression-wrapping = disabled
8+
ktlint_standard_no-wildcard-imports = disabled
9+
ktlint_standard_no-single-line-block-comment = disabled
10+
ktlint_standard_parameter-list-wrapping = enabled
11+
ktlint_standard_function-expression-body = disabled
12+
ktlint_function_naming_ignore_when_annotated_with = Composable,Preview,PreviewLightDark
13+
ktlint_function_signature_body_expression_wrapping = default

app/build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,16 @@ dependencies {
120120
implementation(libs.androidx.material3)
121121
implementation(libs.androidx.material.icons)
122122
implementation(libs.androidx.material.icons.extended)
123-
// implementation(libs.androidx.navigation)
124-
// implementation(libs.androidx.navigation.common.ktx)
125123
implementation(libs.androidx.nav3.runtime)
126124
implementation(libs.androidx.nav3.ui)
127125
implementation(libs.androidx.lifecycle.runtime.compose.android)
126+
implementation(libs.androidx.lifecycle.viewmodel)
128127
implementation(libs.androidx.splashscreen)
129128

130129
implementation(platform(libs.koin.bom))
131130
implementation(libs.koin.android)
132131
implementation(libs.koin.compose)
133132
implementation(libs.koin.workmanager)
134-
// implementation(libs.koin.compose.navigation)
135133
implementation(libs.koin.nav3)
136134

137135
implementation(libs.androidx.adaptive.android)

app/src/main/java/net/frozendevelopment/openletters/MainActivity.kt

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import android.os.Bundle
55
import androidx.activity.ComponentActivity
66
import androidx.activity.compose.setContent
77
import androidx.activity.enableEdgeToEdge
8-
import androidx.compose.animation.core.tween
9-
import androidx.compose.animation.slideInHorizontally
10-
import androidx.compose.animation.slideOutHorizontally
11-
import androidx.compose.animation.togetherWith
128
import androidx.compose.foundation.layout.Box
139
import androidx.compose.foundation.layout.WindowInsets
1410
import androidx.compose.foundation.layout.WindowInsetsSides
@@ -28,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
2824
import androidx.compose.ui.Modifier
2925
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
3026
import androidx.lifecycle.compose.collectAsStateWithLifecycle
27+
import androidx.navigation3.runtime.entryProvider
3128
import androidx.navigation3.ui.NavDisplay
3229
import androidx.window.core.layout.WindowWidthSizeClass
3330
import kotlinx.coroutines.launch
@@ -38,7 +35,8 @@ import net.frozendevelopment.openletters.feature.letter.list.LetterListDestinati
3835
import net.frozendevelopment.openletters.feature.reminder.form.ReminderFormDestination
3936
import net.frozendevelopment.openletters.feature.reminder.list.ReminderListDestination
4037
import net.frozendevelopment.openletters.feature.settings.SettingsDestination
41-
import net.frozendevelopment.openletters.ui.animation.navigationEnterTransition
38+
import net.frozendevelopment.openletters.ui.animation.popTransitionSpec
39+
import net.frozendevelopment.openletters.ui.animation.pushTransitionSpec
4240
import net.frozendevelopment.openletters.ui.navigation.EntryProvider
4341
import net.frozendevelopment.openletters.ui.navigation.LettersNavDrawer
4442
import net.frozendevelopment.openletters.ui.navigation.LocalDrawerState
@@ -74,11 +72,14 @@ class MainActivity : ComponentActivity() {
7472
) {
7573
val coroutineScope = rememberCoroutineScope()
7674
val drawerState = rememberDrawerState(DrawerValue.Closed)
77-
val navigationState =
78-
rememberNavigationState(
75+
val navigationState = rememberNavigationState(
76+
LetterListDestination,
77+
setOf(
7978
LetterListDestination,
80-
setOf(LetterListDestination, ManageCategoryDestination, ReminderListDestination),
81-
)
79+
ManageCategoryDestination,
80+
ReminderListDestination,
81+
),
82+
)
8283
val navigator = remember { Navigator(navigationState) }
8384
val entryProvider: EntryProvider = koinEntryProvider()
8485

@@ -133,30 +134,14 @@ class MainActivity : ComponentActivity() {
133134
) {
134135
NavDisplay(
135136
entries = navigationState.toEntries(entryProvider),
137+
// entryDecorators = listOf(
138+
// rememberSaveableStateHolderNavEntryDecorator(),
139+
// rememberViewModelStoreNavEntryDecorator()
140+
// ),
136141
onBack = { navigator.pop() },
137-
transitionSpec = { navigationEnterTransition() },
138-
popTransitionSpec = {
139-
// Slide in from left when navigating back
140-
slideInHorizontally(
141-
initialOffsetX = { -it },
142-
animationSpec = tween(400),
143-
) togetherWith
144-
slideOutHorizontally(
145-
targetOffsetX = { it },
146-
animationSpec = tween(400),
147-
)
148-
},
149-
predictivePopTransitionSpec = {
150-
// Slide in from left when navigating back
151-
slideInHorizontally(
152-
initialOffsetX = { -it },
153-
animationSpec = tween(400),
154-
) togetherWith
155-
slideOutHorizontally(
156-
targetOffsetX = { it },
157-
animationSpec = tween(400),
158-
)
159-
},
142+
transitionSpec = { pushTransitionSpec() },
143+
popTransitionSpec = { popTransitionSpec() },
144+
predictivePopTransitionSpec = { popTransitionSpec() },
160145
)
161146
}
162147
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package net.frozendevelopment.openletters.extensions
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.navigation3.runtime.EntryProviderScope
5+
import androidx.navigation3.runtime.NavEntry
6+
import androidx.navigation3.runtime.NavKey
7+
import androidx.navigation3.runtime.entryProvider
8+
import org.koin.compose.LocalKoinScopeContext
9+
import org.koin.compose.navigation3.EntryProviderInstaller
10+
import org.koin.core.annotation.KoinExperimentalAPI
11+
import org.koin.core.annotation.KoinInternalApi
12+
import org.koin.core.definition.KoinDefinition
13+
import org.koin.core.module.KoinDslMarker
14+
import org.koin.core.module.Module
15+
import org.koin.core.module._scopedInstanceFactory
16+
import org.koin.core.module._singleInstanceFactory
17+
import org.koin.core.qualifier.named
18+
import org.koin.core.scope.Scope
19+
import org.koin.dsl.ScopeDSL
20+
import org.koin.dsl.navigation3.navigation
21+
22+
/**
23+
* Declares a scoped navigation entry within a Koin scope DSL.
24+
*
25+
* This function registers a composable navigation destination that is scoped to a specific Koin scope,
26+
* allowing access to scoped dependencies within the composable. The route type [T] is used as both
27+
* the navigation destination identifier and a qualifier for the entry provider.
28+
*
29+
* Example usage:
30+
* ```kotlin
31+
* activityScope {
32+
* viewModel { MyViewModel() }
33+
* navigation<MyRoute> { route ->
34+
* MyScreen(viewModel = koinViewModel())
35+
* }
36+
* }
37+
* ```
38+
*
39+
* @param T The type representing the navigation route/destination
40+
* @param definition A composable function that receives the [Scope] and route instance [T] to render the destination
41+
* @return A [KoinDefinition] for the created [EntryProviderInstaller]
42+
*
43+
* @see Module.navigation for module-level navigation entries
44+
*/
45+
@KoinExperimentalAPI
46+
@KoinDslMarker
47+
@OptIn(KoinInternalApi::class)
48+
inline fun <reified T : NavKey> ScopeDSL.navigation(
49+
metadata: Map<String, Any> = emptyMap(),
50+
noinline definition: @Composable Scope.(T) -> Unit,
51+
): KoinDefinition<EntryProviderInstaller> {
52+
val def = _scopedInstanceFactory<EntryProviderInstaller>(named<T>(), {
53+
val scope = this {
54+
entry<T>(
55+
metadata = metadata,
56+
content = { t -> definition(scope, t) },
57+
)
58+
}
59+
}, scopeQualifier)
60+
module.indexPrimaryType(def)
61+
return KoinDefinition(module, def)
62+
}
63+
64+
/**
65+
* Declares a singleton navigation entry within a Koin module.
66+
*
67+
* This function registers a composable navigation destination as a singleton in the Koin module,
68+
* allowing access to module-level dependencies within the composable. The route type [T] is used
69+
* as both the navigation destination identifier and a qualifier for the entry provider.
70+
*
71+
* Example usage:
72+
* ```kotlin
73+
* module {
74+
* viewModel { MyViewModel() }
75+
* navigation<HomeRoute> { route ->
76+
* HomeScreen(myViewModel = koinViewModel())
77+
* }
78+
* }
79+
* ```
80+
*
81+
* @param T The type representing the navigation route/destination
82+
* @param definition A composable function that receives the [Scope] and route instance [T] to render the destination
83+
* @return A [KoinDefinition] for the created [EntryProviderInstaller]
84+
*
85+
* @see ScopeDSL.navigation for scope-level navigation entries
86+
*/
87+
@KoinExperimentalAPI
88+
@KoinDslMarker
89+
@OptIn(KoinInternalApi::class)
90+
inline fun <reified T : Any> Module.navigation(
91+
metadata: Map<String, Any> = emptyMap(),
92+
noinline definition: @Composable Scope.(T) -> Unit,
93+
): KoinDefinition<EntryProviderInstaller> {
94+
val def = _singleInstanceFactory<EntryProviderInstaller>(named<T>(), {
95+
val scope = this {
96+
entry<T>(
97+
metadata = metadata,
98+
content = { t -> definition(scope, t) },
99+
)
100+
}
101+
})
102+
indexPrimaryType(def)
103+
return KoinDefinition(this, def)
104+
}
105+
106+
typealias EntryProvider = (NavKey) -> NavEntry<NavKey>
107+
108+
@OptIn(KoinExperimentalAPI::class, KoinInternalApi::class)
109+
@KoinExperimentalAPI
110+
@Composable
111+
fun koinEntryProvider(scope: Scope = LocalKoinScopeContext.current.getValue()): EntryProvider {
112+
val entries: List<EntryProviderScope<NavKey>.() -> Unit> = scope.getAll()
113+
val entryProvider: (NavKey) -> NavEntry<NavKey> =
114+
entryProvider {
115+
entries.forEach { builder -> this.builder() }
116+
}
117+
return entryProvider
118+
}

app/src/main/java/net/frozendevelopment/openletters/feature/category/CategoryKoinModule.kt

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,21 @@ import org.koin.dsl.module
3636
// }
3737
// }
3838

39-
val categoryKoinModule =
40-
module {
41-
categoryFormNavigation()
42-
manageCategoryNavigation()
43-
viewModel { (mode: CategoryFormDestination.Mode) ->
44-
CategoryFormViewModel(
45-
mode = mode,
46-
upsertCategoryUseCase = get(),
47-
categoryQueries = get(),
48-
)
49-
}
39+
val categoryKoinModule = module {
40+
categoryFormNavigation()
41+
manageCategoryNavigation()
42+
viewModel { (mode: CategoryFormDestination.Mode) ->
43+
CategoryFormViewModel(
44+
mode = mode,
45+
upsertCategoryUseCase = get(),
46+
categoryQueries = get(),
47+
)
48+
}
5049

51-
viewModel {
52-
ManageCategoryViewModel(
53-
saveCategoryOrder = get(),
54-
categoryQueries = get(),
55-
)
56-
}
50+
viewModel {
51+
ManageCategoryViewModel(
52+
saveCategoryOrder = get(),
53+
categoryQueries = get(),
54+
)
5755
}
56+
}

app/src/main/java/net/frozendevelopment/openletters/feature/category/form/CategoryFormView.kt

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,28 +69,27 @@ data class CategoryFormDestination(
6969
}
7070

7171
@OptIn(KoinExperimentalAPI::class)
72-
fun Module.categoryFormNavigation() =
73-
navigation<CategoryFormDestination> { route ->
74-
val navigator = LocalNavigator.current
75-
val viewModel = koinViewModel<CategoryFormViewModel> { parametersOf(route.mode) }
76-
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
77-
val coroutineScope = rememberCoroutineScope()
72+
fun Module.categoryFormNavigation() = navigation<CategoryFormDestination> { route ->
73+
val navigator = LocalNavigator.current
74+
val viewModel = koinViewModel<CategoryFormViewModel> { parametersOf(route.mode) }
75+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
76+
val coroutineScope = rememberCoroutineScope()
7877

79-
Surface {
80-
CategoryFormView(
81-
state = state,
82-
onLabelChanged = viewModel::setLabel,
83-
onColorChanged = viewModel::setColor,
84-
onBackClicked = navigator::pop,
85-
onSaveClicked = {
86-
coroutineScope.launch {
87-
viewModel.save()
88-
navigator.pop()
89-
}
90-
},
91-
)
92-
}
78+
Surface {
79+
CategoryFormView(
80+
state = state,
81+
onLabelChanged = viewModel::setLabel,
82+
onColorChanged = viewModel::setColor,
83+
onBackClicked = navigator::pop,
84+
onSaveClicked = {
85+
coroutineScope.launch {
86+
viewModel.save()
87+
navigator.pop()
88+
}
89+
},
90+
)
9391
}
92+
}
9493

9594
@OptIn(ExperimentalMaterial3Api::class)
9695
@Composable

app/src/main/java/net/frozendevelopment/openletters/feature/category/form/CategoryFormViewModel.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,13 @@ class CategoryFormViewModel(
5656
}
5757
}
5858

59-
fun setLabel(label: String) =
60-
viewModelScope.launch {
61-
update { copy(label = label) }
62-
}
59+
fun setLabel(label: String) = viewModelScope.launch {
60+
update { copy(label = label) }
61+
}
6362

64-
fun setColor(color: Color) =
65-
viewModelScope.launch {
66-
update { copy(color = color) }
67-
}
63+
fun setColor(color: Color) = viewModelScope.launch {
64+
update { copy(color = color) }
65+
}
6866

6967
suspend fun save() {
7068
upsertCategoryUseCase(

0 commit comments

Comments
 (0)