-
Notifications
You must be signed in to change notification settings - Fork 27
[#617][Part 1/3] Migrate to navigation 3 - Template compose project #630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
eeeeaa
wants to merge
4
commits into
develop
Choose a base branch
from
chore/#617-migrate-to-navigation-3-template-compose
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2407831
[#617] Migrate template compose to navigation 3
eeeeaa 00cdba1
[#617] Resolve codeRabbit comments
eeeeaa 0e6b26d
[#617] Extract navigation component out of main activity and implemen…
eeeeaa 60edc91
[#617] Move fake navigator, resolve more comments
eeeeaa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 22 additions & 3 deletions
25
...pose/app/src/main/java/co/nimblehq/template/compose/di/modules/main/MainActivityModule.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,28 @@ | ||
| package co.nimblehq.template.compose.di.modules.main | ||
|
|
||
| import co.nimblehq.template.compose.navigation.EntryProviderInstaller | ||
| import co.nimblehq.template.compose.navigation.Navigator | ||
| import co.nimblehq.template.compose.navigation.NavigatorImpl | ||
| import co.nimblehq.template.compose.navigation.entries.Home | ||
| import co.nimblehq.template.compose.navigation.entries.homeScreenEntry | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.components.ActivityComponent | ||
| import dagger.hilt.android.components.ActivityRetainedComponent | ||
| import dagger.hilt.android.scopes.ActivityRetainedScoped | ||
| import dagger.multibindings.IntoSet | ||
|
|
||
| @Module | ||
| @InstallIn(ActivityComponent::class) | ||
| class MainActivityModule | ||
| @InstallIn(ActivityRetainedComponent::class) | ||
| object MainActivityModule { | ||
|
|
||
| @Provides | ||
| @ActivityRetainedScoped | ||
| fun provideNavigator(): Navigator = NavigatorImpl(startDestination = Home) | ||
|
|
||
| @IntoSet | ||
| @Provides | ||
| fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { | ||
| homeScreenEntry(navigator) | ||
| } | ||
| } | ||
14 changes: 14 additions & 0 deletions
14
...compose/app/src/main/java/co/nimblehq/template/compose/extensions/ComponentActivityExt.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package co.nimblehq.template.compose.extensions | ||
|
|
||
| import android.os.Build | ||
| import androidx.activity.ComponentActivity | ||
| import androidx.activity.enableEdgeToEdge | ||
|
|
||
| fun ComponentActivity.setEdgeToEdgeConfig() { | ||
| enableEdgeToEdge() | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
| // Force the 3-button navigation bar to be transparent | ||
| // See: https://developer.android.com/develop/ui/views/layout/edge-to-edge#create-transparent | ||
| window.isNavigationBarContrastEnforced = false | ||
| } | ||
| } |
11 changes: 0 additions & 11 deletions
11
...-compose/app/src/main/java/co/nimblehq/template/compose/extensions/SavedStateHandleExt.kt
This file was deleted.
Oops, something went wrong.
53 changes: 53 additions & 0 deletions
53
template-compose/app/src/main/java/co/nimblehq/template/compose/navigation/AppNavigation.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package co.nimblehq.template.compose.navigation | ||
|
|
||
| import androidx.compose.animation.ContentTransform | ||
| import androidx.compose.animation.core.tween | ||
| import androidx.compose.animation.slideInHorizontally | ||
| import androidx.compose.animation.slideOutHorizontally | ||
| import androidx.compose.animation.togetherWith | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.CompositionLocalProvider | ||
| import androidx.compose.runtime.remember | ||
| import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator | ||
| import androidx.navigation3.runtime.entryProvider | ||
| import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator | ||
| import androidx.navigation3.ui.NavDisplay | ||
| import co.nimblehq.template.compose.util.LocalResultEventBus | ||
| import co.nimblehq.template.compose.util.ResultEventBus | ||
| import kotlinx.collections.immutable.ImmutableSet | ||
|
|
||
| private const val TWEEN_DURATION_IN_MILLIS = 500 | ||
|
|
||
| @Composable | ||
| fun AppNavigation( | ||
| navigator: Navigator, | ||
| entryProviderScopes: ImmutableSet<EntryProviderInstaller>, | ||
| ) { | ||
| val eventBus = remember { ResultEventBus() } | ||
| CompositionLocalProvider(LocalResultEventBus.provides(eventBus)) { | ||
| NavDisplay( | ||
| backStack = navigator.backStack, | ||
| onBack = { navigator.goBack() }, | ||
| entryDecorators = listOf( | ||
| rememberSaveableStateHolderNavEntryDecorator(), | ||
| rememberViewModelStoreNavEntryDecorator() | ||
| ), | ||
| entryProvider = entryProvider { | ||
| entryProviderScopes.forEach { builder -> this.builder() } | ||
| }, | ||
| transitionSpec = { horizontalSlideTransition(isPop = false) }, | ||
| popTransitionSpec = { horizontalSlideTransition(isPop = true) }, | ||
| predictivePopTransitionSpec = { horizontalSlideTransition(isPop = true) } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun horizontalSlideTransition(isPop: Boolean): ContentTransform = | ||
| slideInHorizontally( | ||
| initialOffsetX = { if (isPop) -it else it }, | ||
| animationSpec = tween(TWEEN_DURATION_IN_MILLIS) | ||
| ) togetherWith slideOutHorizontally( | ||
| targetOffsetX = { if (isPop) it else -it }, | ||
| animationSpec = tween(TWEEN_DURATION_IN_MILLIS) | ||
| ) | ||
|
|
18 changes: 18 additions & 0 deletions
18
template-compose/app/src/main/java/co/nimblehq/template/compose/navigation/Navigator.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package co.nimblehq.template.compose.navigation | ||
|
|
||
| import androidx.compose.runtime.snapshots.SnapshotStateList | ||
| import androidx.navigation3.runtime.EntryProviderScope | ||
| import kotlin.reflect.KClass | ||
|
|
||
| typealias EntryProviderInstaller = EntryProviderScope<Any>.() -> Unit | ||
|
|
||
| interface Navigator { | ||
|
|
||
| val backStack: SnapshotStateList<Any> | ||
|
|
||
| fun goTo(destination: Any) | ||
|
|
||
| fun goBack() | ||
|
|
||
| fun goBackToLast(destinationClass: KClass<*>) | ||
| } |
29 changes: 29 additions & 0 deletions
29
template-compose/app/src/main/java/co/nimblehq/template/compose/navigation/NavigatorImpl.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package co.nimblehq.template.compose.navigation | ||
|
|
||
| import androidx.compose.runtime.mutableStateListOf | ||
| import androidx.compose.runtime.snapshots.SnapshotStateList | ||
| import dagger.hilt.android.scopes.ActivityRetainedScoped | ||
| import kotlin.reflect.KClass | ||
|
|
||
| @ActivityRetainedScoped | ||
| class NavigatorImpl(startDestination: Any) : Navigator { | ||
| override val backStack: SnapshotStateList<Any> = mutableStateListOf(startDestination) | ||
|
|
||
| override fun goTo(destination: Any) { | ||
| backStack.add(destination) | ||
| } | ||
|
|
||
| override fun goBack() { | ||
| backStack.removeLastOrNull() | ||
| } | ||
|
|
||
| override fun goBackToLast(destinationClass: KClass<*>) { | ||
| val index = backStack.indexOfLast { | ||
| destinationClass.isInstance(it) | ||
| } | ||
|
|
||
| if (index in backStack.indices) { | ||
| backStack.removeRange(index + 1, backStack.size) | ||
| } | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
...e/app/src/main/java/co/nimblehq/template/compose/navigation/entries/HomeScreenNavEntry.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| @file:Suppress("MatchingDeclarationName") | ||
|
|
||
| package co.nimblehq.template.compose.navigation.entries | ||
|
|
||
| import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel | ||
| import androidx.navigation3.runtime.EntryProviderScope | ||
| import androidx.navigation3.runtime.NavKey | ||
| import co.nimblehq.template.compose.navigation.Navigator | ||
| import co.nimblehq.template.compose.ui.screens.main.home.HomeScreen | ||
|
|
||
| /** | ||
| * **Template note:** Annotate this NavKey with `@Serializable` if you need to reference | ||
| * its serializer in a [DeepLinkPattern] (e.g. `DeepLinkPattern(Home.serializer(), uri)`). | ||
| */ | ||
| data object Home : NavKey | ||
|
|
||
| fun EntryProviderScope<Any>.homeScreenEntry(navigator: Navigator) { | ||
| entry<Home> { | ||
| HomeScreen(viewModel = hiltViewModel(), navigator = navigator) | ||
| } | ||
| } |
10 changes: 0 additions & 10 deletions
10
template-compose/app/src/main/java/co/nimblehq/template/compose/ui/AppDestination.kt
This file was deleted.
Oops, something went wrong.
52 changes: 0 additions & 52 deletions
52
template-compose/app/src/main/java/co/nimblehq/template/compose/ui/AppNavGraph.kt
This file was deleted.
Oops, something went wrong.
19 changes: 0 additions & 19 deletions
19
template-compose/app/src/main/java/co/nimblehq/template/compose/ui/base/BaseDestination.kt
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 59 additions & 3 deletions
62
template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/MainActivity.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,78 @@ | ||
| package co.nimblehq.template.compose.ui.screens | ||
|
|
||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import android.os.Bundle | ||
| import androidx.activity.ComponentActivity | ||
| import androidx.activity.compose.setContent | ||
| import androidx.navigation.compose.rememberNavController | ||
| import co.nimblehq.template.compose.ui.AppNavGraph | ||
| import co.nimblehq.template.compose.extensions.setEdgeToEdgeConfig | ||
| import co.nimblehq.template.compose.navigation.AppNavigation | ||
| import co.nimblehq.template.compose.navigation.EntryProviderInstaller | ||
| import co.nimblehq.template.compose.navigation.Navigator | ||
| import co.nimblehq.template.compose.ui.theme.ComposeTheme | ||
| import co.nimblehq.template.compose.util.DeepLinkMatcher | ||
| import co.nimblehq.template.compose.util.DeepLinkPattern | ||
| import co.nimblehq.template.compose.util.DeepLinkRequest | ||
| import co.nimblehq.template.compose.util.KeyDecoder | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import kotlinx.collections.immutable.toImmutableSet | ||
| import javax.inject.Inject | ||
|
|
||
| @AndroidEntryPoint | ||
| class MainActivity : ComponentActivity() { | ||
|
|
||
| @Inject | ||
| lateinit var navigator: Navigator | ||
|
|
||
| @Inject | ||
| lateinit var entryProviderScopes: Set<@JvmSuppressWildcards EntryProviderInstaller> | ||
|
|
||
| /** | ||
| * List of deep link patterns supported by the app. | ||
| * | ||
| * **Template note:** Navigation 3 does not natively support deep links. Add [DeepLinkPattern] | ||
| * instances here for each deep link your app should handle. | ||
| * | ||
| * Example: | ||
| * ``` | ||
| * DeepLinkPattern(Home.serializer(), Uri.parse("https://example.com/home")) | ||
| * ``` | ||
| */ | ||
| internal val deepLinkPatterns: List<DeepLinkPattern<out Any>> = listOf() | ||
|
eeeeaa marked this conversation as resolved.
|
||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| setEdgeToEdgeConfig() | ||
| handleNewIntent(intent) | ||
| setContent { | ||
| ComposeTheme { | ||
| AppNavGraph(navController = rememberNavController()) | ||
| AppNavigation( | ||
| navigator = navigator, | ||
| entryProviderScopes = entryProviderScopes.toImmutableSet() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onNewIntent(intent: Intent) { | ||
| super.onNewIntent(intent) | ||
| handleNewIntent(intent) | ||
| } | ||
|
|
||
| private fun handleNewIntent(intent: Intent) { | ||
| val uri = intent.data ?: return | ||
| val deepLinkNavKey = resolveDeepLinkNavKey(uri) | ||
| intent.data = null | ||
| if (deepLinkNavKey != null) navigator.goTo(deepLinkNavKey) | ||
| } | ||
|
|
||
| private fun resolveDeepLinkNavKey(uri: Uri): Any? { | ||
| val request = DeepLinkRequest(uri) | ||
| val match = deepLinkPatterns.firstNotNullOfOrNull { pattern -> | ||
| DeepLinkMatcher(request, pattern).match() | ||
| } ?: return null | ||
| return try { | ||
| KeyDecoder(match.args).decodeSerializableValue(match.serializer) | ||
| } catch (_: Exception) { null } | ||
| } | ||
| } | ||
8 changes: 0 additions & 8 deletions
8
...compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/MainDestination.kt
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: nimblehq/android-templates
Length of output: 671
🏁 Script executed:
Repository: nimblehq/android-templates
Length of output: 355
🌐 Web query:
Hilt ActivityRetainedComponent lifecycle process death configuration changes💡 Result:
ActivityRetainedComponentin Hilt: configuration change vs. process deathWhat it is
ActivityRetainedComponentis a Hilt component whose lifetime matches a configuration-surviving Activity (i.e., the logical Activity across rotations). It’s the backing scope for@ActivityRetainedScoped. [1], [2]Configuration changes (rotation, locale, UI mode, etc.)
Activity.onCreate()in that logical Activity lifetime). [1], [3]ActivityRetainedComponentinstance. [2], [3]Activity.onDestroy()(not the intermediateonDestroy()triggered by rotation). [1], [3]Process death (app process killed, then later recreated)
ActivityRetainedComponentis in-memory only. If the OS kills your app process, the component is gone.ActivityRetainedComponent. (Nothing “retained” survives because the prior process memory is wiped.) This matches the general rule thatViewModelsurvives configuration changes but does not survive process death unless you persist/restore state separately. [4], [5]Practical implication
@ActivityRetainedScopedfor objects you want to reuse across rotations (common examples: “repo-like” instances tied to a screen flow).SavedStateHandlefor small UI state, or database/files for durable data); don’t rely on retained scopes for that. [5]Sources: Hilt component lifetimes and note about creation/destruction across config changes [1], [3]; definition of
ActivityRetainedComponentas configuration-surviving [2]; ViewModel/process-death + state restoration guidance (SavedStateHandle) [4], [5].🌐 Web query:
Android Navigation 3 save state process death back stack persistence💡 Result:
To survive process death with Jetpack Navigation 3, you must persist your back stack (because Nav3 makes the back stack app-owned). The intended path is:
Use a saveable back stack primitive
NavBackStackis@Serializable, so it can be saved/restored via saved-state automatically when used with the provided “remember + serialization” helpers. This is specifically called out as enabling restore across process death + config change. [1]Ensure everything on the back stack is serializable
kotlinx.serialization). [1][3]If you keep per-destination state in ViewModels, scope it to the back stack entry
androidx.lifecycle:lifecycle-viewmodel-navigation3) with aNavEntryDecoratorto scope/clearViewModelstate only while thatNavEntryremains on the back stack, and clear it when popped—while still allowing restoration after process recreation when backed by saved state. [2]Key takeaway: in Nav3, back stack persistence across process death happens by serializing
NavBackStackinto saved state (rather than NavController doing it implicitly like Nav2). [1][2]Sources:
[1] AndroidX Navigation3 release notes (NavBackStack serializable; process-death restore)
[2] Android Developers: “Save and manage navigation state” (Navigation 3)
[3] Android Developers: Navigation 3 recipes showing saveable back stack patterns
🏁 Script executed:
Repository: nimblehq/android-templates
Length of output: 1128
🏁 Script executed:
Repository: nimblehq/android-templates
Length of output: 2644
🏁 Script executed:
Repository: nimblehq/android-templates
Length of output: 1231
This navigator loses the back stack after process death.
provideNavigator()bindsNavigatortoNavigatorImpl, which stores the back stack only in memory usingSnapshotStateList.ActivityRetainedComponentsurvives configuration changes but not process death. When the app is killed and recreated, a newNavigatorImplis instantiated with onlyHomeas the initial entry. Navigation 3 requires back stacks to be persisted explicitly (via serializableNavBackStackand saved state) to survive process death. The decorators inAppNavigation(lines 32–33) handle per-entry state but do not persist the stack structure itself. Either persist the back stack keys via saved state or switch to Nav3's built-in serializable back stack primitives.🤖 Prompt for AI Agents