目录
应用中的状态是指可以随时间变化的任何值。这是一个非常宽泛的定义,从 Room 数据库到类的变量,全部涵盖在内。
由于Compose是声明式UI,会根据状态变化来更新UI,因此状态的处理至关重要。这里的状态你可以简单理解为页面上展示的数据,那么状态管理就是处理数据的读写。
1.remember
remember
就是用来保存状态的,下面举一个小例子。
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
比如我们在页面中加了一个输入框,如果只是上面代码中这样处理,那你会发现我们输入的文字不会被记录起来,输入框中始终都是空的。这是因为属性value
被固定成了空字符串。我们使用remember
优化一下:
@Composable fun HelloContent() { val inputValue = remember { mutableStateOf("") } Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = inputValue.value, onValueChange = { inputValue.value = it }, label = { Text("Name") } ) } }
通过onValueChange
更新value,mutableStateOf
会创建可观察的 MutableState<T>
,value 变更时,系统会重组读取 value 的所有Composable
函数,这样就会自动更新UI。
Jetpack Compose 并不强制要求你使用 MutableState 存储状态。Jetpack Compose 支持其他可观察类型。在 Jetpack Compose 中读取其他可观察类型之前,您必须将其转换为 State,以便 Jetpack Compose 可以在状态发生变化时自动重组界面。
LiveData
中可以使用扩展函数observeAsState()
转换为 State。Flow
中可以使用扩展函数collectAsState()
转换为 State。RxJava
中可以使用扩展函数subscribeAsState()
转换为 State。
2.rememberSaveable
虽然 remember
可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveable
。rememberSaveable
会自动保存可保存在 Bundle 中的任何值。
还是上面的例子,如果我们旋转屏幕,就会发现输入框中的文字会丢失。此时就可以使用rememberSaveable
替换remember
来帮助我们恢复界面状态。
由于保存的数据都是在 Bundle
中的,因此可保存的数据类型是有限制的。比如基础类型、String、Parcelable,Serializable等。一般来说需要保存的对象加个 @Parcelize
注解就可以解决问题。
如果某种原因导致无法使用 @Parcelize
,你可以使用 mapSaver
自定义规则,定义如何将对象保存和恢复到 Bundle。
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
如果你觉得定义map的key麻烦,可以使用 listSaver
并将其索引用作键。
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
3.状态提升
对于上面使用到remember
或rememberSaveState
方法来保存状态的Composable
函数,我们称为有状态。有状态的好处是调用方不需要控制状态,并且不必自行管理状态。但是,具有内部状态的Composable
往往不易重复使用,也更难测试。
在开发可重复使用的Composable
时,您通常想要同时提供同一Composable
的有状态和无状态版本。有状态版本对于不关心状态的调用方来说很方便,而无状态版本对于需要控制或提升状态的调用方来说是必要的。
Compose 中的状态提升是一种将状态移至调用方以使可组合项无状态的模式。
举例说明一下状态提升,比如我们实现一个Dialog,为了方便使用我们可以将里面显示的文字,点击事件逻辑写到dialog的内部封装起来,虽然使用简单但不具有通用性。那么为了通用,我们可以将文字,点击事件的回调当参数传入,这样就灵活了起来。
状态提升其实就是这样一个编程思想,只是换了个名词,没有什么特别了。
对于上面输入框的例子,我们用状态提示优化一下:
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = name, onValueChange = onNameChange, label = { Text("Name") } ) } }
这样就实现了Composable
函数HelloContent 与状态的存储方式解耦,便于我们复用。
状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从 HelloScreen 下降为 HelloContent,事件会从 HelloContent 上升为 HelloScreen。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。
4.状态管理
根据可组合项的复杂性,需要考虑不同的备选方案:
将Composable作为可信来源
用于管理简单的界面元素状态。比如上一篇提到的LazyColumn
滚动到指定item,将交互都放在当前的Composable
中进行。
val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() LazyColumn( state = listState, ) { /* ... */ } Button( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } ) { ... }
其实查看rememberLazyListState
的源码,可以看到实现很简单:
@Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset ) } }
将状态容器作为可信来源
当可组合项包含涉及多个界面元素状态的复杂界面逻辑时,应将相应事务委派给状态容器。这样做更易于单独对该逻辑进行测试,还降低了可组合项的复杂性。该方法支持分离关注点原则:可组合项负责发出界面元素,而状态容器包含界面逻辑和界面元素的状态。
@Composable fun MyApp() { MyTheme { val myAppState = rememberMyAppState() Scaffold( scaffoldState = myAppState.scaffoldState, bottomBar = { if (myAppState.shouldShowBottomBar) { BottomBar( tabs = myAppState.bottomBarTabs, navigateToRoute = { myAppState.navigateToBottomBarRoute(it) } ) } } ) { NavHost(navController = myAppState.navController, "initial") { /* ... */ } } } }
rememberMyAppState
代码:
class MyAppState( val scaffoldState: ScaffoldState, val navController: NavHostController, private val resources: Resources, /* ... */ ) { val bottomBarTabs = /* State */ val shouldShowBottomBar: Boolean get() = /* ... */ fun navigateToBottomBarRoute(route: String) { /* ... */ } fun showSnackbar(message: String) { /* ... */ } } @Composable fun rememberMyAppState( scaffoldState: ScaffoldState = rememberScaffoldState(), navController: NavHostController = rememberNavController(), resources: Resources = LocalContext.current.resources, /* ... */ ) = remember(scaffoldState, navController, resources, /* ... */) { MyAppState(scaffoldState, navController, resources, /* ... */) }
其实就是再封装一层,用户处理逻辑。封装的部分就叫状态容器,用于管理Composable的逻辑和状态。
将 ViewModel 作为可信来源
一种特殊的状态容器类型,用于提供对业务逻辑以及屏幕或界面状态的访问权限。
ViewModel 的生命周期比Composable长,因此不应保留对绑定到组合生命周期的状态的长期引用。否则,可能会导致内存泄漏。建议屏幕级Composable使用 ViewModel 来提供对业务逻辑的访问权限并作为其界面状态的可信来源。如需了解 ViewModel 为何适用于这种情况,请参阅 ViewModel 和状态容器部分。
本篇到此结束,帮忙点个赞~ 给我一点鼓励,你也可以收藏本篇以备不时之需。
参考