Skip to content

Commit

Permalink
Fix navController lifecycle after configuration change, fix #1459
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed Feb 2, 2025
1 parent 8bf251a commit bf86d88
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package me.him188.ani.app.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
Expand All @@ -26,7 +28,7 @@ import me.him188.ani.datasources.api.source.FactoryId
/**
* Supports navigation to any page in the app.
*
* 应当总是使用 [AniNavigator], 而不要访问 [navigator].
* 应当总是使用 [AniNavigator], 而不要访问 [currentNavigator].
*
* @see LocalNavigator
*/
Expand All @@ -39,14 +41,18 @@ interface AniNavigator {

suspend fun awaitNavController(): NavHostController

val navigator: NavHostController
// Not @Stable
val currentNavigator: NavHostController

@Composable
fun collectNavigatorAsState(): State<NavHostController>

fun popBackStack() {
navigator.popBackStack()
currentNavigator.popBackStack()
}

fun popBackStack(route: NavRoutes, inclusive: Boolean, saveState: Boolean = false) {
navigator.popBackStack(route, inclusive, saveState)
currentNavigator.popBackStack(route, inclusive, saveState)
}

// fun popBackStack(
Expand All @@ -57,54 +63,54 @@ interface AniNavigator {
// }

fun popUntilNotWelcome() {
navigator.popBackStack(NavRoutes.Welcome, inclusive = true)
currentNavigator.popBackStack(NavRoutes.Welcome, inclusive = true)
}

fun popUntilNotAuth() {
navigator.popBackStack(NavRoutes.BangumiTokenAuth, inclusive = true)
navigator.popBackStack(NavRoutes.BangumiOAuth, inclusive = true)
currentNavigator.popBackStack(NavRoutes.BangumiTokenAuth, inclusive = true)
currentNavigator.popBackStack(NavRoutes.BangumiOAuth, inclusive = true)
}

fun navigateSubjectDetails(
subjectId: Int,
placeholder: SubjectDetailPlaceholder?,
) {
navigator.navigate(
currentNavigator.navigate(
NavRoutes.SubjectDetail(subjectId, placeholder),
)
}

fun navigateSubjectCaches(subjectId: Int) {
navigator.navigate(NavRoutes.SubjectCaches(subjectId))
currentNavigator.navigate(NavRoutes.SubjectCaches(subjectId))
}

fun navigateEpisodeDetails(subjectId: Int, episodeId: Int, fullscreen: Boolean = false) {
navigator.popBackStack(NavRoutes.EpisodeDetail(subjectId, episodeId), inclusive = true)
navigator.navigate(NavRoutes.EpisodeDetail(subjectId, episodeId))
currentNavigator.popBackStack(NavRoutes.EpisodeDetail(subjectId, episodeId), inclusive = true)
currentNavigator.navigate(NavRoutes.EpisodeDetail(subjectId, episodeId))
}

fun navigateWelcome() {
navigator.navigate(NavRoutes.Welcome)
currentNavigator.navigate(NavRoutes.Welcome)
}

fun navigateMain(
page: MainScreenPage,
requestFocus: Boolean = false
) {
navigator.popBackStack<NavRoutes.Main>(inclusive = false)
currentNavigator.popBackStack<NavRoutes.Main>(inclusive = false)
}

/**
* 登录页面
*/
fun navigateBangumiOAuthOrTokenAuth() {
navigator.navigate(NavRoutes.BangumiOAuth) {
currentNavigator.navigate(NavRoutes.BangumiOAuth) {
launchSingleTop = true
}
}

fun navigateBangumiTokenAuth() {
navigator.navigate(
currentNavigator.navigate(
NavRoutes.BangumiTokenAuth,
) {
launchSingleTop = true
Expand All @@ -115,32 +121,32 @@ interface AniNavigator {
}

fun navigateSettings(tab: SettingsTab? = null) {
navigator.navigate(NavRoutes.Settings(tab))
currentNavigator.navigate(NavRoutes.Settings(tab))
}

fun navigateEditMediaSource(
factoryId: FactoryId,
mediaSourceInstanceId: String,
) {
navigator.navigate(
currentNavigator.navigate(
NavRoutes.EditMediaSource(factoryId.value, mediaSourceInstanceId),
)
}

fun navigateTorrentPeerSettings() {
navigator.navigate(NavRoutes.TorrentPeerSettings)
currentNavigator.navigate(NavRoutes.TorrentPeerSettings)
}

fun navigateCaches() {
navigator.navigate(NavRoutes.Caches)
currentNavigator.navigate(NavRoutes.Caches)
}

fun navigateCacheDetails(cacheId: String) {
navigator.navigate(NavRoutes.CacheDetail(cacheId))
currentNavigator.navigate(NavRoutes.CacheDetail(cacheId))
}

fun navigateSchedule() {
navigator.navigate(NavRoutes.Schedule)
currentNavigator.navigate(NavRoutes.Schedule)
}
}

Expand All @@ -153,11 +159,16 @@ private class AniNavigatorImpl : AniNavigator {
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)

override val navigator: NavHostController
override val currentNavigator: NavHostController
get() = _navigator.replayCache.firstOrNull() ?: error("Navigator is not yet set")

@Composable
override fun collectNavigatorAsState(): State<NavHostController> = _navigator.collectAsState(currentNavigator)

override fun setNavController(controller: NavHostController) {
this._navigator.tryEmit(controller)
check(this._navigator.tryEmit(controller)) {
"Failed to set NavController"
}
}

override fun isNavControllerReady(): Boolean = _navigator.replayCache.isNotEmpty()
Expand All @@ -179,11 +190,11 @@ inline fun OverrideNavigation(
noinline newNavigator: @DisallowComposableCalls (AniNavigator) -> AniNavigator,
crossinline content: @Composable () -> Unit
) {
val current by rememberUpdatedState(LocalNavigator.current)
val newNavigatorUpdated by rememberUpdatedState(newNavigator)
val currentState = rememberUpdatedState(LocalNavigator.current)
val newNavigatorState = rememberUpdatedState(newNavigator)
val new by remember {
derivedStateOf {
newNavigatorUpdated(current)
newNavigatorState.value(currentState.value)
}
}
CompositionLocalProvider(LocalNavigator provides new) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ internal fun createTestCacheEpisode(
onPause = { state.value = CacheEpisodePaused.PAUSED },
onResume = { state.value = CacheEpisodePaused.IN_PROGRESS },
onDelete = {},
onPlay = {},
backgroundScope = GlobalScope,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fun PreviewCacheGroupCardMissingTotalSize() = ProvideCompositionLocalsForPreview
downloadSpeed = 233.megaBytes,
totalSize = Unspecified,
),
onPlay = { _, _ -> },
)
}
}
Expand All @@ -54,6 +55,7 @@ fun PreviewCacheGroupCardMissingProgress() = ProvideCompositionLocalsForPreview
downloadSpeed = 233.megaBytes,
totalSize = 888.megaBytes,
),
onPlay = { _, _ -> },
)
}
}
Expand All @@ -70,6 +72,7 @@ fun PreviewCacheGroupCardMissingDownloadSpeed() = ProvideCompositionLocalsForPre
downloadSpeed = Unspecified,
totalSize = 888.megaBytes,
),
onPlay = { _, _ -> },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import me.him188.ani.app.domain.media.cache.MediaCacheManager
import me.him188.ani.app.domain.media.cache.MediaCacheState
import me.him188.ani.app.domain.media.cache.engine.MediaStats
import me.him188.ani.app.domain.media.cache.engine.sum
import me.him188.ani.app.navigation.AniNavigator
import me.him188.ani.app.torrent.api.files.averageRate
import me.him188.ani.app.ui.adaptive.AniTopAppBar
import me.him188.ani.app.ui.adaptive.AniTopAppBarDefaults
Expand Down Expand Up @@ -85,9 +84,7 @@ import kotlin.time.Duration.Companion.seconds
typealias CacheGroupGridLayoutState = LazyStaggeredGridState

@Stable
class CacheManagementViewModel(
private val navigator: AniNavigator,
) : AbstractViewModel(), KoinComponent {
class CacheManagementViewModel : AbstractViewModel(), KoinComponent {
private val cacheManager: MediaCacheManager by inject()
private val subjectCollectionRepository: SubjectCollectionRepository by inject()

Expand Down Expand Up @@ -211,12 +208,6 @@ class CacheManagementViewModel(
onPause = { mediaCache.pause() },
onResume = { mediaCache.resume() },
onDelete = { cacheManager.deleteCache(mediaCache) },
onPlay = {
navigator.navigateEpisodeDetails(
mediaCache.metadata.subjectIdInt,
mediaCache.metadata.episodeIdInt,
)
},
backgroundScope = this + CoroutineName("CacheEpisode-${mediaCache.metadata.episodeIdInt}"),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
* Copyright (C) 2024-2025 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
Expand Down Expand Up @@ -90,7 +90,6 @@ class CacheEpisodeState(
private val onPause: suspend () -> Unit, // background scope
private val onResume: suspend () -> Unit, // background scope
private val onDelete: suspend () -> Unit, // background scope
private val onPlay: () -> Unit, // ui scope
backgroundScope: CoroutineScope,
) {
@Immutable
Expand Down Expand Up @@ -173,10 +172,6 @@ class CacheEpisodeState(
}
}

fun play() {
onPlay()
}

companion object {
/**
* @sample me.him188.ani.app.ui.cache.components.CacheEpisodeStateTest.CalculateSizeTextTest
Expand Down Expand Up @@ -214,6 +209,7 @@ enum class CacheEpisodePaused {
@Composable
fun CacheEpisodeItem(
state: CacheEpisodeState,
onPlay: (subjectId: Int, episodeId: Int) -> Unit,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surface,
) {
Expand Down Expand Up @@ -352,6 +348,7 @@ fun CacheEpisodeItem(
Dropdown(
showDropdown, { showDropdown = false },
state,
onPlay = { onPlay(state.subjectId, state.episodeId) },
)
},
colors = listItemColors,
Expand All @@ -363,6 +360,7 @@ private fun Dropdown(
showDropdown: Boolean,
onDismissRequest: () -> Unit,
state: CacheEpisodeState,
onPlay: () -> Unit,
modifier: Modifier = Modifier,
) {
var showConfirm by rememberSaveable { mutableStateOf(false) }
Expand Down Expand Up @@ -422,7 +420,7 @@ private fun Dropdown(
}
},
onClick = {
state.play()
onPlay()
onDismissRequest()
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ fun CacheGroupCard(
}
}
}
val navigator = LocalNavigator.current

AniAnimatedVisibility(state.expanded) {
Column(
Expand All @@ -296,7 +297,13 @@ fun CacheGroupCard(
verticalArrangement = Arrangement.spacedBy(layoutProperties.episodeItemSpacing), // each item already has inner paddings
) {
for (episode in state.episodes) {
CacheEpisodeItem(episode, containerColor = outerCardColors.containerColor)
CacheEpisodeItem(
episode,
containerColor = outerCardColors.containerColor,
onPlay = { subjectId, episodeId ->
navigator.navigateEpisodeDetails(subjectId, episodeId)
},
)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions app/shared/src/commonMain/kotlin/ui/main/AniAppContent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private fun AniAppContentImpl(
initialRoute: NavRoutes,
modifier: Modifier = Modifier,
) {
val navController = aniNavigator.navigator
val navController by aniNavigator.collectNavigatorAsState()
// 必须传给所有 Scaffold 和 TopAppBar. 注意, 如果你不传, 你的 UI 很可能会在 macOS 不工作.
val windowInsetsWithoutTitleBar = ScaffoldDefaults.contentWindowInsets
val windowInsets = ScaffoldDefaults.contentWindowInsets
Expand Down Expand Up @@ -306,7 +306,7 @@ private fun AniAppContentImpl(
) { backStackEntry ->
val route = backStackEntry.toRoute<NavRoutes.Caches>()
CacheManagementScreen(
viewModel { CacheManagementViewModel(aniNavigator) },
viewModel { CacheManagementViewModel() },
navigationIcon = {
BackNavigationIconButton(
{
Expand Down Expand Up @@ -467,7 +467,7 @@ private fun AniAppContentImpl(
}
}

LaunchedEffect(true) {
LaunchedEffect(true, navController) {
navController.currentBackStack.collect { list ->
if (list.isEmpty()) { // workaround for 快速点击左上角返回键会白屏.
navController.navigate(initialRoute)
Expand Down
5 changes: 3 additions & 2 deletions app/shared/src/commonMain/kotlin/ui/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ private fun MainScreenContent(
layoutType = navigationLayoutType,
) {
val coroutineScope = rememberCoroutineScope()
val navigator by rememberUpdatedState(LocalNavigator.current)
val navigatorState = rememberUpdatedState(LocalNavigator.current)
val navigator by navigatorState
TabContent(
layoutType = navigationLayoutType,
Modifier.ifThen(navigationLayoutType != NavigationSuiteType.NavigationBar) {
Expand Down Expand Up @@ -206,7 +207,7 @@ private fun MainScreenContent(
}

MainScreenPage.CacheManagement -> CacheManagementScreen(
viewModel { CacheManagementViewModel(navigator) },
viewModel { CacheManagementViewModel() },
navigationIcon = { },
Modifier.fillMaxSize(),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2024-2025 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.ui.cache.components

import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -79,7 +88,6 @@ class CacheEpisodeStateTest {
onPause = { state.value = CacheEpisodePaused.PAUSED },
onResume = { state.value = CacheEpisodePaused.IN_PROGRESS },
onDelete = {},
onPlay = {},
backgroundScope = GlobalScope,
)
}
Expand Down
Loading

0 comments on commit bf86d88

Please sign in to comment.