mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
feat(nav): put onboarding and chat behind a Navigation 3 back stack
Replaces the when (onboardingState) in MainActivity and the ad-hoc back handling with a Navigation 3 back stack. Navigator, AppNavigator, the EntryProviderInstaller multibinding and the NavDisplay host live in :app for now; they move to a :core:navigation module once there is a second module to justify one. Onboarding is one destination, not eight. Its steps are driven by permission results and adapter state changes rather than by the user navigating, and going back a step has never been supported. Per-step routes would invent a history that does not exist, so the state machine stays and only the crossing into chat is a navigation event. That crossing uses resetTo, so Back cannot re-enter onboarding. Removes the OnBackPressedCallback that was constructed and registered inside a composable body, adding a callback on every recomposition of that branch. NavDisplay owns back now; it drives predictive back through androidx.navigationevent, and a second handler over the same gesture can detach the navigationevent input mid-gesture, so there must not be one. Chat still manages its overlays with booleans, so back consults ChatViewModel first through interceptBack. That parameter exists only until those overlays become routes. The back stack is seeded before setContent rather than from a LaunchedEffect: NavDisplay rejects an empty back stack and an effect does not run until after the first composition. Verified on a Pixel 9a: launch renders without the empty-backstack crash, chat is reached, Back exits to the launcher instead of re-entering onboarding, and relaunch is clean. Plus :app:assembleDebug and 591 unit tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26
This commit is contained in:
parent
480dc38333
commit
c7d551e2af
@ -198,6 +198,10 @@ dependencies {
|
||||
implementation(libs.androidx.exifinterface)
|
||||
|
||||
// Testing
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||
|
||||
testImplementation(libs.bundles.testing)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.bundles.compose.testing)
|
||||
|
||||
@ -116,21 +116,31 @@ androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileCla
|
||||
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation3:navigation3-runtime-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigation3:navigation3-runtime:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigation3:navigation3-ui-android:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigation3:navigation3-ui:1.1.6=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose-android:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent-compose:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.navigationevent:navigationevent:1.1.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath
|
||||
androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
|
||||
@ -44,8 +44,15 @@ import com.bitchat.android.ui.OrientationAwareActivity
|
||||
import com.bitchat.android.ui.theme.BitchatTheme
|
||||
import com.bitchat.android.wifiaware.WifiAwareController
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
import com.bitchat.android.navigation.AppNavigator
|
||||
import com.bitchat.android.navigation.BitchatNavDisplay
|
||||
import com.bitchat.android.navigation.ChatRoute
|
||||
import com.bitchat.android.navigation.EntryProviderInstaller
|
||||
import com.bitchat.android.navigation.OnboardingRoute
|
||||
import com.bitchat.android.navigation.rootRouteFor
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -65,6 +72,11 @@ class MainActivity : OrientationAwareActivity() {
|
||||
private var pendingMeshForegroundServiceStart = false
|
||||
private val chatViewModel: ChatViewModel by viewModels()
|
||||
|
||||
// Held by ActivityRetainedComponent, so the back stack outlives configuration
|
||||
// changes without being rebuilt here.
|
||||
@Inject
|
||||
lateinit var navigator: AppNavigator
|
||||
|
||||
private val forceFinishReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||
if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) {
|
||||
@ -148,16 +160,57 @@ class MainActivity : OrientationAwareActivity() {
|
||||
onOnboardingFailed = ::handleOnboardingFailed
|
||||
)
|
||||
|
||||
// Seed the stack before the first composition. NavDisplay rejects an empty
|
||||
// back stack, and a LaunchedEffect would not run until after it has already
|
||||
// composed once.
|
||||
navigator.setRootIfEmpty(rootRouteFor(mainViewModel.onboardingState.value))
|
||||
|
||||
setContent {
|
||||
BitchatTheme {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
) { innerPadding ->
|
||||
OnboardingFlowScreen(modifier = Modifier
|
||||
val contentModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
val onboardingState by mainViewModel.onboardingState.collectAsState()
|
||||
val root = rootRouteFor(onboardingState)
|
||||
|
||||
// Keyed on root, so this fires only when the app crosses between
|
||||
// onboarding and chat — not on every step within onboarding.
|
||||
// resetTo rather than goTo: onboarding must not be reachable with
|
||||
// Back once the app is in.
|
||||
LaunchedEffect(root) {
|
||||
if (navigator.backStack.lastOrNull() != root) {
|
||||
navigator.resetTo(root)
|
||||
}
|
||||
}
|
||||
|
||||
val entries: EntryProviderInstaller = {
|
||||
entry<OnboardingRoute> { OnboardingFlowScreen(contentModifier) }
|
||||
entry<ChatRoute> { ChatScreen(viewModel = chatViewModel) }
|
||||
}
|
||||
|
||||
BitchatNavDisplay(
|
||||
navigator = navigator,
|
||||
entryInstallers = setOf(entries),
|
||||
onExit = { finish() },
|
||||
modifier = contentModifier,
|
||||
)
|
||||
|
||||
// Chat still drives its overlays with booleans rather than routes,
|
||||
// and NavDisplay enables its own handler only while something sits
|
||||
// beneath the current scene, which at the root destination is never.
|
||||
// So the press has to be claimed here. Composed after NavDisplay:
|
||||
// among enabled handlers the last one composed wins, so overlays
|
||||
// unwind before routes pop. Removed once the overlays become routes.
|
||||
val canHandleBack by chatViewModel.canHandleBack.collectAsState()
|
||||
BackHandler(
|
||||
enabled = navigator.backStack.lastOrNull() == ChatRoute && canHandleBack
|
||||
) {
|
||||
chatViewModel.handleBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -306,17 +359,12 @@ class MainActivity : OrientationAwareActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> {
|
||||
val canHandleBack by chatViewModel.canHandleBack.collectAsState()
|
||||
// Disabled rather than always-on so predictive back can preview
|
||||
// the exit instead of the app claiming every gesture.
|
||||
BackHandler(enabled = canHandleBack) {
|
||||
// enabled trails the state by a dispatch and a recomposition.
|
||||
if (!chatViewModel.handleBackPressed()) finish()
|
||||
}
|
||||
ChatScreen(viewModel = chatViewModel)
|
||||
}
|
||||
|
||||
// CHECKING, INITIALIZING and COMPLETE are handled by ChatRoute, so this
|
||||
// composable is only ever shown for the onboarding steps themselves.
|
||||
OnboardingState.CHECKING,
|
||||
OnboardingState.INITIALIZING,
|
||||
OnboardingState.COMPLETE -> Unit
|
||||
|
||||
OnboardingState.ERROR -> {
|
||||
InitializationErrorScreen(
|
||||
modifier = modifier,
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Single-stack navigator backed by a [SnapshotStateList] that NavDisplay observes.
|
||||
*
|
||||
* Scoped to [dagger.hilt.android.components.ActivityRetainedComponent], which is
|
||||
* what makes the back stack survive configuration changes — there is no
|
||||
* rememberNavBackStack here, and so no requirement that keys be serializable.
|
||||
*
|
||||
* The stack is *not* restored across process death. Nothing needs that yet:
|
||||
* onboarding re-derives its state on launch. When it is needed, the move is
|
||||
* kotlinx-serialization plus rememberNavBackStack, which is why keys are kept as
|
||||
* plain data objects that would be trivial to annotate.
|
||||
*
|
||||
* Deliberately single-stack. The multi-back-stack pattern exists to serve bottom
|
||||
* navigation; this app has no tabs, so it would be a map that only ever holds
|
||||
* one key.
|
||||
*/
|
||||
@Stable
|
||||
@ActivityRetainedScoped
|
||||
class AppNavigator @Inject constructor() : Navigator {
|
||||
|
||||
val backStack: SnapshotStateList<NavKey> = mutableStateListOf()
|
||||
|
||||
/**
|
||||
* Seeds the stack with [root] the first time only.
|
||||
*
|
||||
* Idempotent by design: an Activity recreated after a configuration change
|
||||
* re-runs its setup, and the retained scope still holds the real history.
|
||||
* Seeding again would discard it.
|
||||
*/
|
||||
fun setRootIfEmpty(root: NavKey) {
|
||||
if (backStack.isEmpty()) backStack.add(root)
|
||||
}
|
||||
|
||||
override fun goTo(dest: NavKey) {
|
||||
// A same-frame double tap pushes the same key twice. Dedup at the source
|
||||
// rather than guarding every call site.
|
||||
if (backStack.lastOrNull() != dest) backStack.add(dest)
|
||||
}
|
||||
|
||||
override fun goBack(): Boolean {
|
||||
if (backStack.size <= 1) return false
|
||||
backStack.removeAt(backStack.lastIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun popTo(route: NavKey, inclusive: Boolean): Boolean {
|
||||
val index = backStack.indexOfLast { it == route }
|
||||
if (index < 0) return false
|
||||
val target = if (inclusive) index else index + 1
|
||||
// Already at the requested route with inclusive = false pops nothing and
|
||||
// still succeeded.
|
||||
while (backStack.size > target) {
|
||||
backStack.removeAt(backStack.lastIndex)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun replaceCurrent(dest: NavKey) {
|
||||
if (backStack.isNotEmpty()) backStack.removeAt(backStack.lastIndex)
|
||||
backStack.add(dest)
|
||||
}
|
||||
|
||||
override fun resetTo(dest: NavKey) {
|
||||
backStack.clear()
|
||||
backStack.add(dest)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.bitchat.android.onboarding.OnboardingState
|
||||
|
||||
/**
|
||||
* Top-level destinations.
|
||||
*
|
||||
* Onboarding is one destination, not eight. Its steps are driven by permission
|
||||
* results and adapter state changes rather than by the user navigating, and the
|
||||
* app has never supported going back from one step to the previous one. Giving
|
||||
* each step its own entry would invent a history that does not exist.
|
||||
*
|
||||
* These live in :app because both destinations still need collaborators the
|
||||
* Activity owns. They move to :feature:<name>:api as those features become
|
||||
* modules.
|
||||
*/
|
||||
data object OnboardingRoute : NavKey
|
||||
|
||||
data object ChatRoute : NavKey
|
||||
|
||||
/**
|
||||
* The destination that should be at the root for a given onboarding state.
|
||||
*
|
||||
* CHECKING and INITIALIZING map to chat, matching the behaviour this replaced:
|
||||
* the app shows the chat screen while it verifies its own readiness rather than
|
||||
* flashing an onboarding step.
|
||||
*/
|
||||
fun rootRouteFor(state: OnboardingState): NavKey = when (state) {
|
||||
OnboardingState.CHECKING,
|
||||
OnboardingState.INITIALIZING,
|
||||
OnboardingState.COMPLETE -> ChatRoute
|
||||
|
||||
else -> OnboardingRoute
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
|
||||
/**
|
||||
* Hosts every feature's destinations.
|
||||
*
|
||||
* [entryInstallers] arrives as a Hilt multibinding, so adding a destination
|
||||
* means adding an @IntoSet provider in the owning feature — this function never
|
||||
* changes.
|
||||
*
|
||||
* This handles Back only between destinations, and only while there is one to
|
||||
* go back to: NavDisplay enables its handler on scene.previousEntries being
|
||||
* non-empty, so at the root destination it takes no press at all. A screen with
|
||||
* state the back stack does not model has to claim Back itself, with a handler
|
||||
* registered into the same dispatcher — androidx.activity.compose's BackHandler
|
||||
* is, since activity-compose prefers the NavigationEventDispatcher when one is
|
||||
* present. Among enabled handlers the last one composed wins.
|
||||
*/
|
||||
@Composable
|
||||
fun BitchatNavDisplay(
|
||||
navigator: AppNavigator,
|
||||
entryInstallers: Set<EntryProviderInstaller>,
|
||||
onExit: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
NavDisplay(
|
||||
backStack = navigator.backStack,
|
||||
modifier = modifier,
|
||||
onBack = { if (!navigator.goBack()) onExit() },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator(),
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entryInstallers.forEach { install -> install() }
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
|
||||
/**
|
||||
* One feature's contribution to the navigation graph.
|
||||
*
|
||||
* Features provide these into a Set via Hilt multibindings, so the host
|
||||
* assembles every destination without importing any feature's internals.
|
||||
*/
|
||||
typealias EntryProviderInstaller = EntryProviderScope<NavKey>.() -> Unit
|
||||
@ -0,0 +1,18 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityRetainedComponent::class)
|
||||
abstract class NavigationModule {
|
||||
|
||||
/**
|
||||
* Features inject [Navigator]; only the host injects [AppNavigator] itself,
|
||||
* since reading or seeding the back stack is the host's job.
|
||||
*/
|
||||
@Binds
|
||||
abstract fun bindNavigator(impl: AppNavigator): Navigator
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.bitchat.android.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
|
||||
/**
|
||||
* The navigation surface features are allowed to depend on.
|
||||
*
|
||||
* Features call this rather than touching the back stack directly, so the
|
||||
* guards below live in one place instead of at every call site.
|
||||
*/
|
||||
interface Navigator {
|
||||
|
||||
/** Push [dest], unless it is already on top. */
|
||||
fun goTo(dest: NavKey)
|
||||
|
||||
/** Pop one entry. Returns false when the stack cannot go back any further. */
|
||||
fun goBack(): Boolean
|
||||
|
||||
/** Pop back to [route]. Returns false when [route] is not on the stack. */
|
||||
fun popTo(route: NavKey, inclusive: Boolean = false): Boolean
|
||||
|
||||
/** Swap the current top for [dest], so Back skips the screen being replaced. */
|
||||
fun replaceCurrent(dest: NavKey)
|
||||
|
||||
/** Clear the stack and start again at [dest]. */
|
||||
fun resetTo(dest: NavKey)
|
||||
}
|
||||
@ -22,6 +22,9 @@ compose-icons-extended = "1.7.8"
|
||||
|
||||
# Navigation
|
||||
navigation-compose = "2.9.8"
|
||||
# Latest stable line; the 1.2.0 branch is still alpha. 1.1.6 floors Compose at
|
||||
# 1.10.0, below the 1.11.4 the BOM resolves, so it does not drag Compose forward.
|
||||
navigation3 = "1.1.6"
|
||||
|
||||
# Dependency injection
|
||||
hilt = "2.60.1"
|
||||
@ -83,6 +86,11 @@ mockito-core = "5.23.0"
|
||||
roboelectric = "4.15" # 4.16+ drops the AndroidKeyStore shim; breaks EncryptionServiceTest
|
||||
|
||||
[libraries]
|
||||
# Gradle plugin artifacts, resolved only by build-logic convention plugins
|
||||
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
||||
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
|
||||
|
||||
# AndroidX Core
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
|
||||
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime" }
|
||||
@ -104,6 +112,9 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
|
||||
|
||||
# Navigation
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
|
||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
|
||||
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle-runtime" }
|
||||
|
||||
# Dependency injection
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
|
||||
@ -5465,5 +5465,70 @@
|
||||
<sha256 value="e154a3231cd6cde68aabf274fd18964d49a9c1a5f5e9ed3bf471e9ae7bdf08e6" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-navigation3" version="2.11.0">
|
||||
<artifact name="lifecycle-viewmodel-navigation3-2.11.0.module">
|
||||
<sha256 value="8a4771ae287872a2439033b4b39ab29648989bcdd766d5a381798890b9f0a6b1" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel-navigation3-android" version="2.11.0">
|
||||
<artifact name="lifecycle-viewmodel-navigation3-android-2.11.0.module">
|
||||
<sha256 value="4b4ef039749a6e0ac8a586c0cfff40c1b07e448142e0c48d28e02c704b065247" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="lifecycle-viewmodel-navigation3.aar">
|
||||
<sha256 value="07889cc98a311023c49cff1feabb23adadcf72121e327e7b9431f1d4831a5ab9" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation3" name="navigation3-runtime" version="1.1.6">
|
||||
<artifact name="navigation3-runtime-1.1.6.module">
|
||||
<sha256 value="2fd41f2626eb17ef5690f8886f40d430459cbe9822359637bb027545dd900c5f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation3" name="navigation3-runtime-android" version="1.1.6">
|
||||
<artifact name="navigation3-runtime-android-1.1.6.module">
|
||||
<sha256 value="294a003e4280a3cd0e8ed24ec605aec6b98be5ee378bc0f2ef0ebe7ceac79487" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigation3-runtime.aar">
|
||||
<sha256 value="82145fb70f9c7fd6681e54ed2c9b61787ef428ec62a59d4200a6746394f00336" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation3" name="navigation3-ui" version="1.1.6">
|
||||
<artifact name="navigation3-ui-1.1.6.module">
|
||||
<sha256 value="cc9c3c03f97b10bc8a019f56f01e1cfd668b1753823ca4b4c24357730d4d10d4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigation3" name="navigation3-ui-android" version="1.1.6">
|
||||
<artifact name="navigation3-ui-android-1.1.6.module">
|
||||
<sha256 value="e3b0f7fe9c614dd63ae3fdca51ee962f93136fe3216d87fd42767c91084100b3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigation3-ui.aar">
|
||||
<sha256 value="1b7004e94f69be0e264ee983a0fbde908e74fb5464f976f2a8b5a9444c414f83" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigationevent" name="navigationevent" version="1.1.2">
|
||||
<artifact name="navigationevent-1.1.2.module">
|
||||
<sha256 value="9e8c5ad1c7f80dfec824b7ebe219654888515a82bc26e0d2cc679b70b350ffe9" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigationevent" name="navigationevent-android" version="1.1.2">
|
||||
<artifact name="navigationevent-android-1.1.2.module">
|
||||
<sha256 value="44c6081542bea72773dea5733e5cb9c0cdfab616985c4bcebe087959ee4a93ea" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigationevent.aar">
|
||||
<sha256 value="40a357103ab703762bf434f424a8486be18652b4aa25110f0d635a4cef5dcd63" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigationevent" name="navigationevent-compose" version="1.1.2">
|
||||
<artifact name="navigationevent-compose-1.1.2.module">
|
||||
<sha256 value="3736c69a73fe2b7389adb05529dfe15e9808eb1e1106cfbc2d5525bc15a1f1d3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.navigationevent" name="navigationevent-compose-android" version="1.1.2">
|
||||
<artifact name="navigationevent-compose-android-1.1.2.module">
|
||||
<sha256 value="24a0d539aaa2be6812cfa07cb309a6310538f51a4a5419448b759ed4993f6d98" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="navigationevent-compose.aar">
|
||||
<sha256 value="ac9773dc9076d3b035ccf5494316d78a5f9abc18a8fa73fb211f92957286e1aa" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
</components>
|
||||
</verification-metadata>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user