英文:
Hold NavigationBottomItem in memory instead of destroying it in Jetpack Compose navigation when changing tabs
问题
# 背景
我正在创建一个聊天应用程序,在用户离开聊天时需要执行逻辑(聊天只是一个`@Composable fun`,我正在使用`LocalLifecycleOwner.current`结合一个观察`onDestroy`方法的ViewModel以取消用户订阅)。现在,当用户切换选项卡时,也会执行这个逻辑,这不应该发生。
# 问题
我正在使用一个带有`BottomNavigation`的`Scaffold`。当我切换选项卡时,旧选项卡被销毁。我不希望发生这种行为,旧选项卡应该保留在内存中。当返回到选项卡时,`remember`块也会重新执行,我不希望发生这种情况。我应该使用多个导航宿主或其他方法吗?
# 目标
在选项卡之间导航时,不重新执行`remember`块(同时`LocalLifecycleOwner.current`也不应发布`onDestroy`)。
# 示例代码
整个项目可以在此处找到:https://github.com/Jasperav/JetpackComposeNavigation。您可以看到在切换选项卡时,`remember`块会重新执行,并且`VM`会被销毁(请参阅日志记录)。我不希望发生这种行为,应该保留在内存中。以下是相关代码:
@Composable
fun Screen() {
val items = listOf(
Triple("a", Icons.Default.Person, Icons.Filled.Person),
Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
)
var selectedTab = items[0]
val navHostController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
val isSelected = index == items.indexOf(selectedTab)
BottomNavigationItem(
icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
label = { Text(text = item.first) },
selected = isSelected,
onClick = {
navHostController.navigate(item.first) {
popUpTo(navHostController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
}
}
) {
NavHost(
navHostController,
startDestination = items[0].first,
Modifier.padding(it)
) {
composable(items[0].first) {
selectedTab = items[0]
val lifecycle = LocalLifecycleOwner.current
val viewModel: ModelDontDestory = viewModel(factory = viewModelFactory {
ModelDontDestory(lifecycle)
})
remember {
println("Recomposed first")
""
}
Text("first")
}
composable(items[1].first) {
selectedTab = items[1]
val lifecycle = LocalLifecycleOwner.current
val viewModel: ModelDontDestory = viewModel(factory = viewModelFactory {
ModelDontDestory(lifecycle)
})
remember {
println("Recomposed second")
""
}
Text("Second")
}
}
}
}
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
}
class ModelDontDestory(val lifecycle: LifecycleOwner): ViewModel(), DefaultLifecycleObserver {
init {
lifecycle.lifecycle.addObserver(this)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
println("This should never happen, this should be kept in memory")
}
}
英文:
Background
I am creating a chat application where I need to execute logic when the user leaves a chat (a chat is just a @Composable fun
and I am using the LocalLifecycleOwner.current
combined with a ViewModel which watches the onDestroy
method to unsubscribe the user). Now that logic is also executed when the user changes tab, this should not happen.
Problem
I am using a Scaffold
with a BottomNavigation
. When I switch tabs, the old tab is destroyed. I don't want this behavior, the old tab should remain in memory. The remember
blocks are also re-executed when coming back to the tab, I don't want this. Should I use multiple navigation hosts or something?
Goal
Navigating between tabs without remember
blocks being re-executed (and also LocalLifecycleOwner.current
should not publish onDestroy
).
Sample code
The whole project can be found here: https://github.com/Jasperav/JetpackComposeNavigation. You can see when switching tabs the remember
blocks are re-executed and that the VM
is destroyed (see logging). I don't want this behavior, it should be kept in memory. This is the relevant code:
@Composable
fun Screen() {
val items = listOf(
Triple("a", Icons.Default.Person, Icons.Filled.Person),
Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
)
var selectedTab = items[0]
val navHostController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
val isSelected = index == items.indexOf(selectedTab)
BottomNavigationItem(
icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
label = { Text(text = item.first) },
selected = isSelected,
onClick = {
navHostController.navigate(item.first) {
popUpTo(navHostController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
}
}
) {
NavHost(
navHostController,
startDestination = items[0].first,
Modifier.padding(it)
) {
composable(items[0].first) {
selectedTab = items[0]
val lifecycle = LocalLifecycleOwner.current
val viewModel: ModelDontDestory = viewModel(factory = viewModelFactory {
ModelDontDestory(lifecycle)
})
remember {
println("Recomposed first")
""
}
Text("first")
}
composable(items[1].first) {
selectedTab = items[1]
val lifecycle = LocalLifecycleOwner.current
val viewModel: ModelDontDestory = viewModel(factory = viewModelFactory {
ModelDontDestory(lifecycle)
})
remember {
println("Recomposed second")
""
}
Text("Second")
}
}
}
}
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
}
class ModelDontDestory(val lifecycle: LifecycleOwner): ViewModel(), DefaultLifecycleObserver {
init {
lifecycle.lifecycle.addObserver(this)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
println("This should never happen, this should be kept in memory")
}
}
答案1
得分: 3
您已经有一个留在内存中的对象 - ViewModel 实例本身。您不应该关注生命周期实例的销毁,因为当您的应用程序经历配置更改时,这也会发生。
相反,您应该查看 ViewModel 的 onCleared
方法 - 只有在实例实际从返回栈中移除时才会调用该方法(例如,用户离开聊天)。
类似地,您不应该使用 remember
来保存需要保存的状态(再次提醒,remember
变量会因多种原因而被清除,包括进行配置更改时)。您应该使用 rememberSaveable
来保存需要在屏幕保留在返回栈上的值。
英文:
You already have an object that remains in memory - the ViewModel instances themselves. You shouldn't ever be looking at the destruction of the Lifecycle instance, since that also happens when your app goes through a configuration change.
Instead, you should be looking at the onCleared
method of the ViewModel - that's what is called only when the instance is actually removed from the back stack (e.g., the user leaves the chat).
Similarly, you shouldn't be using remember
for state that needs to be saved (again, remember
variables are wiped for many reasons, including when you do a config change). You should be using rememberSaveable
for values that need to be retained for the entire time the screen remains on the back stack.
答案2
得分: 2
从我理解的“组件的生命周期”以及像Ziv Kesten的“在Jetpack Compose中实现底部导航栏”这样的示例中,remember
块(在这里解释)和生命周期事件与@Composable
函数的生命周期密切相关。当您从屏幕导航离开时,其@Composable
函数将被丢弃,当您返回时,它们将被重新组合。
在Jetpack Compose中,remember
函数用于在重新组合期间保留状态。
然而,使用remember
保存的状态在配置更改或进程终止时无法幸存。如果您需要在配置更改或进程终止时保留状态,应改用rememberSaveable
函数。
要使用rememberSaveable
,您需要确保要保存的数据是可序列化的,因为它将需要放入Bundle中。
如下所示:
@Composable
fun MyComposable() {
val myState = rememberSaveable { mutableStateOf(MyState()) }
// 在您的组合中使用myState
}
MyState
必须是一个具有所有可序列化属性的数据类。rememberSaveable
函数确保在配置更改和进程终止时保存和恢复状态。
但是,在您提供的代码中,似乎您正在使用remember
来触发println
,当组件重新组合时。通常情况下,remember
(和rememberSaveable
)不用于此目的。如果您只想在重新组合时打印日志消息,可以直接使用println
,无需使用remember
。
如果您想保持一些状态在内存中,可以同时使用ViewModel
和rememberSaveable
,其中rememberSaveable
保存UI状态,而ViewModel
保存更持久的数据状态。
这意味着这将是一个更简单的实现:
@Composable
fun Screen() {
val items = listOf(
Triple("a", Icons.Default.Person, Icons.Filled.Person),
Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
)
// 使用rememberSaveable来在配置更改时保持选定的标签。
val selectedTabIndex = rememberSaveable { mutableStateOf(0) }
val navHostController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
val isSelected = index == selectedTabIndex.value
BottomNavigationItem(
icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
label = { Text(text = item.first) },
selected = isSelected,
onClick = {
// 当点击选项卡时更新选定的标签索引。
selectedTabIndex.value = index
navHostController.navigate(item.first) {
popUpTo(navHostController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
}
}
) {
NavHost(
navHostController,
startDestination = items[0].first,
Modifier.padding(it)
) {
composable(items[0].first) {
val viewModel: ModelDontDestroy = viewModel()
Text("first")
}
composable(items[1].first) {
val viewModel: ModelDontDestroy = viewModel()
Text("Second")
}
}
}
}
class ModelDontDestroy: ViewModel() {
// 覆盖onCleared以在不再需要此ViewModel时执行清理。
override fun onCleared() {
super.onCleared()
println("This should never happen, this should be kept in memory")
}
}
通过这些更改:
-
从ViewModel中删除了
LifecycleOwner
。ViewModel不需要对LifecycleOwner的引用。相反,应在不再需要ViewModel时使用ViewModel的onCleared
方法执行清理。 -
将选定的标签索引从
remember
更改为rememberSaveable
。这确保了即使发生配置更改(例如屏幕旋转),选定的标签也会被记住。 -
删除了用于在重新组合时打印日志消息的
remember
块。如果需要在重新组合时执行操作,可以在组合中直接调用该操作。不需要remember
函数。
英文:
From what I understand of "Lifecycle of composables" and from an example such as "Implement Bottom Bar Navigation in Jetpack Compose" by Ziv Kesten, the remember
block (explained here) and lifecycle events are inherently tied to the lifecycle of the @Composable
function. When you navigate away from a screen, its @Composable
functions are disposed of, and when you return, they are recomposed.
In Jetpack Compose, the remember
function is used to retain state across recompositions.
However, state saved using remember
does not survive configuration changes or process death. If you need to retain state across configuration changes or process death, you should use the rememberSaveable
function instead.
To use rememberSaveable
, you will need to ensure the data you're saving is serializable, as it will need to be put into a Bundle
.
As in:
@Composable
fun MyComposable() {
val myState = rememberSaveable { mutableStateOf(MyState()) }
// Use myState in your composable
}
MyState
would need to be a data class with properties that are all serializable. The rememberSaveable
function ensures that the state is saved and restored across configuration changes and process death.
However, in your provided code, it seems like you are using remember
to trigger a println
when the composable is recomposed. remember
(and rememberSaveable
) is not usually used for this purpose. Instead, remember
and rememberSaveable
are used to retain state across recompositions or configuration changes.
If you just want to print a log message when the composable is recomposed, you can just use println
directly, without remember
.
If you want to keep some state in memory, you can use ViewModel
and rememberSaveable
together, where rememberSaveable
holds UI state and ViewModel
holds more permanent data state.
That means this would be a more straightforward implementation:
@Composable
fun Screen() {
val items = listOf(
Triple("a", Icons.Default.Person, Icons.Filled.Person),
Triple("b", Icons.Default.Notifications, Icons.Filled.Notifications),
)
// Use rememberSaveable to persist the selected tab across configuration changes.
val selectedTabIndex = rememberSaveable { mutableStateOf(0) }
val navHostController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
val isSelected = index == selectedTabIndex.value
BottomNavigationItem(
icon = { Icon(if (isSelected) item.second else item.third, contentDescription = null) },
label = { Text(text = item.first) },
selected = isSelected,
onClick = {
// Update the selected tab index when a tab is clicked.
selectedTabIndex.value = index
navHostController.navigate(item.first) {
popUpTo(navHostController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
}
}
) {
NavHost(
navHostController,
startDestination = items[0].first,
Modifier.padding(it)
) {
composable(items[0].first) {
val viewModel: ModelDontDestroy = viewModel()
Text("first")
}
composable(items[1].first) {
val viewModel: ModelDontDestroy = viewModel()
Text("Second")
}
}
}
}
class ModelDontDestroy: ViewModel() {
// Override onCleared to perform clean-up when this ViewModel is no longer needed.
override fun onCleared() {
super.onCleared()
println("This should never happen, this should be kept in memory")
}
}
With the changes:
-
Removed the
LifecycleOwner
from theViewModel
. TheViewModel
does not need a reference to theLifecycleOwner
. Instead, theViewModel
'sonCleared
method should be used to perform clean-up when theViewModel
is no longer needed. -
Changed
remember
torememberSaveable
for the selected tab index. This ensures that the selected tab is remembered even when a configuration change occurs (e.g., screen rotation). -
Removed the
remember
blocks that were used to print a log message when the composable is recomposed. If you need to perform an action when a Composable is recomposed, you can just call that action directly in the composable. Theremember
function is not needed for this.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论