From 6b35cbb328ee230e3af14b77c32012f003083ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Wed, 10 Apr 2024 11:48:08 +0200 Subject: [PATCH 1/5] Offload connectivity monitor to a background thread Change-Id: I9a2ef7766ae6abc6d8a7c86a4b49ef3c795e446c --- .../util/ConnectivityManagerNetworkMonitor.kt | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index e9599c555..a3cad57f9 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -26,57 +26,71 @@ import android.net.NetworkRequest.Builder import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.content.getSystemService +import androidx.tracing.Trace +import androidx.tracing.trace +import com.google.samples.apps.nowinandroid.core.network.Dispatcher +import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import dagger.hilt.android.qualifiers.ApplicationContext +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 javax.inject.Inject internal class ConnectivityManagerNetworkMonitor @Inject constructor( @ApplicationContext private val context: Context, + @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, ) : NetworkMonitor { override val isOnline: Flow = callbackFlow { - val connectivityManager = context.getSystemService() - if (connectivityManager == null) { - channel.trySend(false) - channel.close() - return@callbackFlow - } + trace("NetworkMonitor.callbackFlow") { + val connectivityManager = context.getSystemService() + if (connectivityManager == null) { + channel.trySend(false) + channel.close() + return@callbackFlow + } + + /** + * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], + * not just the active network. So we can simply track the presence (or absence) of such [Network]. + */ + val callback = object : NetworkCallback() { - /** - * The callback's methods are invoked on changes to *any* network matching the [NetworkRequest], - * not just the active network. So we can simply track the presence (or absence) of such [Network]. - */ - val callback = object : NetworkCallback() { + private val networks = mutableSetOf() - private val networks = mutableSetOf() + override fun onAvailable(network: Network) { + networks += network + channel.trySend(true) + } - override fun onAvailable(network: Network) { - networks += network - channel.trySend(true) + override fun onLost(network: Network) { + networks -= network + channel.trySend(networks.isNotEmpty()) + } } - override fun onLost(network: Network) { - networks -= network - channel.trySend(networks.isNotEmpty()) + trace("NetworkMonitor.registerNetworkCallback") { + val request = Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(request, callback) } - } - val request = Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - connectivityManager.registerNetworkCallback(request, callback) + /** + * Sends the latest connectivity status to the underlying channel. + */ + channel.trySend(connectivityManager.isCurrentlyConnected()) - /** - * Sends the latest connectivity status to the underlying channel. - */ - channel.trySend(connectivityManager.isCurrentlyConnected()) + Trace.endSection() - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } } } + .flowOn(ioDispatcher) .conflate() @Suppress("DEPRECATION") From c396352d83f9bbf1d86dae3c52c89fb66a9bcd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Wed, 10 Apr 2024 15:23:20 +0200 Subject: [PATCH 2/5] Remove forgotten Trace.endSection() Change-Id: Ib6f7678f5f1c4b0f93f1736b6453c649d6f7dc97 --- .../core/data/util/ConnectivityManagerNetworkMonitor.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt index a3cad57f9..b2a642cf9 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -26,7 +26,6 @@ import android.net.NetworkRequest.Builder import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.core.content.getSystemService -import androidx.tracing.Trace import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO @@ -83,8 +82,6 @@ internal class ConnectivityManagerNetworkMonitor @Inject constructor( */ channel.trySend(connectivityManager.isCurrentlyConnected()) - Trace.endSection() - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } From 1c0508a6781be6e53cec07e5ccc51eb401ea488f Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Mon, 15 Apr 2024 17:03:04 -0700 Subject: [PATCH 3/5] Recreate nested nav to work with AnimatedPane Change-Id: I6b526331b7fc62b968ac39e91753a8a1e5343023 --- .../InterestsListDetailScreen.kt | 82 +++++++++++++------ .../topic/navigation/TopicNavigation.kt | 9 +- gradle/libs.versions.toml | 2 +- 3 files changed, 62 insertions(+), 31 deletions(-) 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..87a6f48fa 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,19 @@ 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.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder @@ -39,8 +44,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 +83,38 @@ 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 remember { 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 +123,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/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 ef84555fa..6bc85e10e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.7.0" androidxMacroBenchmark = "1.2.2" androidxMetrics = "1.0.0-alpha04" -androidxNavigation = "2.7.7" +androidxNavigation = "2.8.0-alpha06" androidxProfileinstaller = "1.3.1" androidxTestCore = "1.5.0" androidxTestExt = "1.1.5" From 1f9097950994b3eac408f38b04ce99ce3c138a14 Mon Sep 17 00:00:00 2001 From: alexvanyo Date: Tue, 16 Apr 2024 00:34:30 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A4=96=20Updates=20baselines=20for=20?= =?UTF-8?q?Dependency=20Guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prodReleaseRuntimeClasspath.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/dependencies/prodReleaseRuntimeClasspath.txt b/app/dependencies/prodReleaseRuntimeClasspath.txt index 71e0ab289..1703b0c36 100644 --- a/app/dependencies/prodReleaseRuntimeClasspath.txt +++ b/app/dependencies/prodReleaseRuntimeClasspath.txt @@ -17,10 +17,10 @@ androidx.compose.animation:animation-android:1.7.0-alpha06 androidx.compose.animation:animation-core-android:1.7.0-alpha06 androidx.compose.animation:animation-core:1.7.0-alpha06 androidx.compose.animation:animation:1.7.0-alpha06 -androidx.compose.foundation:foundation-android:1.6.3 -androidx.compose.foundation:foundation-layout-android:1.6.3 -androidx.compose.foundation:foundation-layout:1.6.3 -androidx.compose.foundation:foundation:1.6.3 +androidx.compose.foundation:foundation-android:1.7.0-alpha06 +androidx.compose.foundation:foundation-layout-android:1.7.0-alpha06 +androidx.compose.foundation:foundation-layout:1.7.0-alpha06 +androidx.compose.foundation:foundation:1.7.0-alpha06 androidx.compose.material3.adaptive:adaptive-android:1.0.0-alpha10 androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-alpha10 androidx.compose.material3.adaptive:adaptive-layout:1.0.0-alpha10 @@ -103,11 +103,11 @@ androidx.lifecycle:lifecycle-viewmodel:2.8.0-alpha04 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-alpha04 -androidx.navigation:navigation-common-ktx:2.7.7 -androidx.navigation:navigation-common:2.7.7 -androidx.navigation:navigation-compose:2.7.7 -androidx.navigation:navigation-runtime-ktx:2.7.7 -androidx.navigation:navigation-runtime:2.7.7 +androidx.navigation:navigation-common-ktx:2.8.0-alpha06 +androidx.navigation:navigation-common:2.8.0-alpha06 +androidx.navigation:navigation-compose:2.8.0-alpha06 +androidx.navigation:navigation-runtime-ktx:2.8.0-alpha06 +androidx.navigation:navigation-runtime:2.8.0-alpha06 androidx.print:print:1.0.0 androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 From c2fc34c76182c7353f794063364eb2c76afde03c Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 16 Apr 2024 10:45:45 -0700 Subject: [PATCH 5/5] Save nested nav key in instance state Change-Id: If1155bfbe080eb4df3c59faaec0fb4cd4da3821d --- .../ui/interests2pane/Interests2PaneViewModel.kt | 3 ++- .../ui/interests2pane/InterestsListDetailScreen.kt | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) 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 87a6f48fa..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 @@ -30,6 +30,8 @@ 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 @@ -98,7 +100,11 @@ internal fun InterestsListDetailScreen( var nestedNavHostStartDestination by remember { mutableStateOf(selectedTopicId?.let(::createTopicRoute) ?: TOPIC_ROUTE) } - var nestedNavKey by remember { mutableStateOf(UUID.randomUUID()) } + var nestedNavKey by rememberSaveable( + stateSaver = Saver({ it.toString() }, UUID::fromString), + ) { + mutableStateOf(UUID.randomUUID()) + } val nestedNavController = key(nestedNavKey) { rememberNavController() }