diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 8cbabc247..4c8232a26 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -16,6 +16,7 @@ package com.google.samples.apps.nowinandroid.ui +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -199,15 +200,7 @@ internal fun NiaApp( ) } - NiaNavHost( - appState = appState, - onShowSnackbar = { message, action -> - snackbarHostState.showSnackbar( - message = message, - actionLabel = action, - duration = Short, - ) == ActionPerformed - }, + Box( modifier = if (shouldShowTopAppBar) { Modifier.consumeWindowInsets( WindowInsets.safeDrawing.only(WindowInsetsSides.Top), @@ -215,7 +208,18 @@ internal fun NiaApp( } else { Modifier }, - ) + ) { + NiaNavHost( + appState = appState, + onShowSnackbar = { message, action -> + snackbarHostState.showSnackbar( + message = message, + actionLabel = action, + duration = Short, + ) == ActionPerformed + }, + ) + } } // TODO: We may want to add padding or spacer when the snackbar is shown so that diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt index d618c2d47..40ce9c116 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/Interests2PaneViewModel.kt @@ -27,7 +27,8 @@ import javax.inject.Inject class Interests2PaneViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ) : ViewModel() { - val selectedTopicId: StateFlow = savedStateHandle.getStateFlow(TOPIC_ID_ARG, null) + val selectedTopicId: StateFlow = + savedStateHandle.getStateFlow(TOPIC_ID_ARG, savedStateHandle[TOPIC_ID_ARG]) fun onTopicClick(topicId: String?) { savedStateHandle[TOPIC_ID_ARG] = topicId diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt index 4cc4345ef..ada4e49d1 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt @@ -18,14 +18,21 @@ package com.google.samples.apps.nowinandroid.ui.interests2pane import androidx.activity.compose.BackHandler import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder @@ -39,8 +46,10 @@ import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERES import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ROUTE +import com.google.samples.apps.nowinandroid.feature.topic.navigation.createTopicRoute import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen +import java.util.UUID private const val DETAIL_PANE_NAVHOST_ROUTE = "detail_pane_route" @@ -76,17 +85,42 @@ internal fun InterestsListDetailScreen( selectedTopicId: String?, onTopicClick: (String) -> Unit, ) { - val listDetailNavigator = rememberListDetailPaneScaffoldNavigator() + val listDetailNavigator = rememberListDetailPaneScaffoldNavigator( + initialDestinationHistory = listOfNotNull( + ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List), + ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.Detail).takeIf { + selectedTopicId != null + }, + ), + ) BackHandler(listDetailNavigator.canNavigateBack()) { listDetailNavigator.navigateBack() } - val nestedNavController = rememberNavController() + var nestedNavHostStartDestination by remember { + mutableStateOf(selectedTopicId?.let(::createTopicRoute) ?: TOPIC_ROUTE) + } + var nestedNavKey by rememberSaveable( + stateSaver = Saver({ it.toString() }, UUID::fromString), + ) { + mutableStateOf(UUID.randomUUID()) + } + val nestedNavController = key(nestedNavKey) { + rememberNavController() + } fun onTopicClickShowDetailPane(topicId: String) { onTopicClick(topicId) - nestedNavController.navigateToTopic(topicId) { - popUpTo(DETAIL_PANE_NAVHOST_ROUTE) + if (listDetailNavigator.isDetailPaneVisible()) { + // If the detail pane was visible, then use the nestedNavController navigate call + // directly + nestedNavController.navigateToTopic(topicId) { + popUpTo(DETAIL_PANE_NAVHOST_ROUTE) + } + } else { + // Otherwise, recreate the NavHost entirely, and start at the new destination + nestedNavHostStartDestination = createTopicRoute(topicId) + nestedNavKey = UUID.randomUUID() } listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail) } @@ -95,34 +129,34 @@ internal fun InterestsListDetailScreen( value = listDetailNavigator.scaffoldValue, directive = listDetailNavigator.scaffoldDirective, listPane = { - InterestsRoute( - onTopicClick = ::onTopicClickShowDetailPane, - highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), - ) - }, - detailPane = { - NavHost( - navController = nestedNavController, - startDestination = TOPIC_ROUTE, - route = DETAIL_PANE_NAVHOST_ROUTE, - ) { - topicScreen( - showBackButton = !listDetailNavigator.isListPaneVisible(), - onBackClick = listDetailNavigator::navigateBack, + AnimatedPane { + InterestsRoute( onTopicClick = ::onTopicClickShowDetailPane, + highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(), ) - composable(route = TOPIC_ROUTE) { - TopicDetailPlaceholder() + } + }, + detailPane = { + AnimatedPane { + key(nestedNavKey) { + NavHost( + navController = nestedNavController, + startDestination = nestedNavHostStartDestination, + route = DETAIL_PANE_NAVHOST_ROUTE, + ) { + topicScreen( + showBackButton = !listDetailNavigator.isListPaneVisible(), + onBackClick = listDetailNavigator::navigateBack, + onTopicClick = ::onTopicClickShowDetailPane, + ) + composable(route = TOPIC_ROUTE) { + TopicDetailPlaceholder() + } + } } } }, ) - LaunchedEffect(Unit) { - if (selectedTopicId != null) { - // Initial topic ID was provided when navigating to Interests, so show its details. - onTopicClickShowDetailPane(selectedTopicId) - } - } } @OptIn(ExperimentalMaterial3AdaptiveApi::class) diff --git a/core/data/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index bb653dd40..db9c7bb1d 100644 --- a/core/data/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -26,15 +26,18 @@ import android.net.NetworkRequest.Builder import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.content.getSystemService +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn import me.tatarka.inject.annotations.Inject @Inject internal class ConnectivityManagerNetworkMonitor( private val context: Context, + private val ioDispatcher: CoroutineDispatcher, ) : NetworkMonitor { override val isOnline: Flow = callbackFlow { val connectivityManager = context.getSystemService() @@ -62,12 +65,10 @@ internal class ConnectivityManagerNetworkMonitor( channel.trySend(networks.isNotEmpty()) } } - val request = Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, callback) - /** * Sends the latest connectivity status to the underlying channel. */ @@ -77,6 +78,7 @@ internal class ConnectivityManagerNetworkMonitor( connectivityManager.unregisterNetworkCallback(callback) } } + .flowOn(ioDispatcher) .conflate() @Suppress("DEPRECATION") diff --git a/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt b/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt index b76397919..b217dbe24 100644 --- a/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt +++ b/core/database/src/androidMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt @@ -46,4 +46,4 @@ internal actual abstract class DriverModule(private val context: Context) { }, ) } -} \ No newline at end of file +} diff --git a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt index c52af3862..656c38a16 100644 --- a/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt +++ b/core/database/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt @@ -26,4 +26,4 @@ internal expect abstract class DriverModule { suspend fun provideDbDriver( schema: SqlSchema>, ): SqlDriver -} \ No newline at end of file +} diff --git a/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt b/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt index b09da154d..43e2b739a 100644 --- a/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt +++ b/core/database/src/jvmMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt @@ -20,7 +20,6 @@ import app.cash.sqldelight.db.QueryResult.AsyncValue import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver -import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver.Companion import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides import java.util.Properties @@ -37,4 +36,4 @@ internal actual abstract class DriverModule { ) .also { schema.create(it).await() } } -} \ No newline at end of file +} diff --git a/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt b/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt index 5986a5a6d..a26fab962 100644 --- a/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt +++ b/core/database/src/nativeMain/kotlin/com/google/samples/apps/nowinandroid/core/database/di/DriverModule.kt @@ -44,4 +44,4 @@ internal actual abstract class DriverModule { }, ) } -} \ No newline at end of file +} diff --git a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt index 41804b634..394c53303 100644 --- a/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt +++ b/feature/topic/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/topic/navigation/TopicNavigation.kt @@ -41,13 +41,16 @@ internal class TopicArgs(val topicId: String) { } fun NavController.navigateToTopic(topicId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { - val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) - val newRoute = "$TOPIC_ROUTE/$encodedId" - navigate(newRoute) { + navigate(createTopicRoute(topicId)) { navOptions() } } +fun createTopicRoute(topicId: String): String { + val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) + return "$TOPIC_ROUTE/$encodedId" +} + fun NavGraphBuilder.topicScreen( showBackButton: Boolean, onBackClick: () -> Unit, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 321b974f0..941dce564 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,15 +12,15 @@ androidxComposeCompiler = "1.5.13" androidxComposeUiTest = "1.7.0-alpha08" androidxComposeMaterial3Adaptive = "1.0.0-alpha12" androidxComposeRuntimeTracing = "1.0.0-beta01" -androidxCore = "1.13.0" +androidxCore = "1.13.1" androidxCoreSplashscreen = "1.0.1" -androidxDataStore = "1.1.0" +androidxDataStore = "1.1.1" androidxEspresso = "3.5.1" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.4" androidxMetrics = "1.0.0-beta01" -androidxNavigation = "2.7.7" +androidxNavigation = "2.8.0-alpha08" androidxProfileinstaller = "1.3.1" androidxTestCore = "1.5.0" androidxTestExt = "1.1.5" @@ -28,7 +28,7 @@ androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" androidxTracing = "1.3.0-alpha02" androidxUiAutomator = "2.3.0" -androidxWindowManager = "1.3.0-beta01" +androidxWindowManager = "1.3.0-beta02" androidxWork = "2.9.0" coil = "3.0.0-alpha06" dependencyGuard = "0.5.0" @@ -54,7 +54,7 @@ protobufPlugin = "0.9.4" retrofit = "2.11.0" retrofitKotlinxSerializationJson = "1.0.0" robolectric = "4.12.1" -roborazzi = "1.13.0" +roborazzi = "1.14.0" room = "2.6.1" secrets = "2.0.1" truth = "1.4.2" @@ -68,10 +68,10 @@ androidx-appcompat = "1.6.1" androidx-constraintlayout = "2.1.4" androidx-core-ktx = "1.12.0" androidx-espresso-core = "3.5.1" -androidx-material = "1.11.0" +androidx-material = "1.12.0" androidx-test-junit = "1.1.5" compose-ui-tooling = "1.6.7" -compose-plugin = "1.6.10-beta03" +compose-plugin = "1.6.10-rc01" junit = "4.13.2" sqldelight = "2.0.2" kotlinInject = '0.6.3'