diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 939c1b33..81649153 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/gradle.lockfile b/app/gradle.lockfile index 21a4ec95..62b09af7 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -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 diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 41d95a1e..280e7d5a 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -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 { OnboardingFlowScreen(contentModifier) } + entry { 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, diff --git a/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt new file mode 100644 index 00000000..9ce3158c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/AppNavigator.kt @@ -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 = 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) + } +} diff --git a/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt new file mode 100644 index 00000000..0eae3b97 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/AppRoutes.kt @@ -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::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 +} diff --git a/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt b/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt new file mode 100644 index 00000000..c0009527 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/BitchatNavDisplay.kt @@ -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, + 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() } + }, + ) +} diff --git a/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt b/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt new file mode 100644 index 00000000..18d86803 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/EntryProviderInstaller.kt @@ -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.() -> Unit diff --git a/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt b/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt new file mode 100644 index 00000000..39895808 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/NavigationModule.kt @@ -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 +} diff --git a/app/src/main/java/com/bitchat/android/navigation/Navigator.kt b/app/src/main/java/com/bitchat/android/navigation/Navigator.kt new file mode 100644 index 00000000..1c66b2bb --- /dev/null +++ b/app/src/main/java/com/bitchat/android/navigation/Navigator.kt @@ -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) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92034c89..1034a2f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8685436f..9066a436 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5465,5 +5465,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +