Hold NavigationBottomItem in memory instead of destroying it in Jetpack Compose navigation when changing tabs

huangapple go评论82阅读模式
英文:

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(&quot;a&quot;, Icons.Default.Person, Icons.Filled.Person),
Triple(&quot;b&quot;, Icons.Default.Notifications, Icons.Filled.Notifications),
)
var selectedTab = items[0]
val navHostController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item -&gt;
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(&quot;Recomposed first&quot;)
&quot;&quot;
}
Text(&quot;first&quot;)
}
composable(items[1].first) {
selectedTab = items[1]
val lifecycle = LocalLifecycleOwner.current
val viewModel: ModelDontDestory = viewModel(factory = viewModelFactory {
ModelDontDestory(lifecycle)
})
remember {
println(&quot;Recomposed second&quot;)
&quot;&quot;
}
Text(&quot;Second&quot;)
}
}
}
}
inline fun &lt;VM : ViewModel&gt; viewModelFactory(crossinline f: () -&gt; VM) =
object : ViewModelProvider.Factory {
override fun &lt;T : ViewModel&gt; create(aClass: Class&lt;T&gt;): 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(&quot;This should never happen, this should be kept in memory&quot;)
}
}

答案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
如果您想保持一些状态在内存中,可以同时使用ViewModelrememberSaveable,其中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")
    }
}

通过这些更改:

  1. 从ViewModel中删除了LifecycleOwner。ViewModel不需要对LifecycleOwner的引用。相反,应在不再需要ViewModel时使用ViewModel的onCleared方法执行清理。

  2. 将选定的标签索引从remember更改为rememberSaveable。这确保了即使发生配置更改(例如屏幕旋转),选定的标签也会被记住。

  3. 删除了用于在重新组合时打印日志消息的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.

Hold NavigationBottomItem in memory instead of destroying it in Jetpack Compose navigation when changing tabs

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(&quot;a&quot;, Icons.Default.Person, Icons.Filled.Person),
        Triple(&quot;b&quot;, 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 -&gt;
                    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(&quot;first&quot;)
            }
            composable(items[1].first) {
                val viewModel: ModelDontDestroy = viewModel()

                Text(&quot;Second&quot;)
            }
        }
    }
}

class ModelDontDestroy: ViewModel() {
    // Override onCleared to perform clean-up when this ViewModel is no longer needed.
    override fun onCleared() {
        super.onCleared()

        println(&quot;This should never happen, this should be kept in memory&quot;)
    }
}

With the changes:

  1. Removed the LifecycleOwner from the ViewModel. The ViewModel does not need a reference to the LifecycleOwner. Instead, the ViewModel's onCleared method should be used to perform clean-up when the ViewModel is no longer needed.

  2. Changed remember to rememberSaveable for the selected tab index. This ensures that the selected tab is remembered even when a configuration change occurs (e.g., screen rotation).

  3. 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. The remember function is not needed for this.

huangapple
  • 本文由 发表于 2023年5月28日 17:07:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76350732.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定