文章目录
通过状态变化,导致的 @Composable 函数重组,我们可以得到新的 UI 界面。
应用中的状态是指可以随时间变化的任何值。这是一个非常宽泛的定义,从 Room 数据库到类的变量,全部涵盖在内。
所有 Android 应用都会向用户显示状态。下面是 Android 应用中的一些状态示例:
- 在无法建立网络连接时显示的信息提示控件。
- 博文和相关评论。
- 在用户点击按钮时播放的涟漪效果。
- 用户可以在图片上绘制的贴纸。
一、state 和组合
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
如果运行此代码,您将不会看到任何反应。这是因为,TextField 不会自行更新,但会在其 value 参数更改时更新。这是因 Compose 中组合和重组的工作原理造成的。
为了让 OutlinedTextField 可以输入,我们需要传入参数,并赋值给 value 属性。当参数改变时,OutlinedTextField 就会自动重组,我们改写代码如下:
@Composable
@Preview
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name: String by remember { mutableStateOf("") }
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") })
}
}
运行后,效果如下:
二、可组合项中的 state
可以用 remember 将对象存储在内存中。在可组合项中声明 MutableState 对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
可以用 if 对 state 判断,如下例中的 if (name.isNotEmpty())
,代码如下:
@Composable
@Preview
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name: String by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") })
}
}
运行后,效果如下:
rememberSaveable 会保存 Bundle 中的值。
2.1 state 提升
Compose 中的状态提升是一种,将状态移至可组合项的调用方,以使可组合项无状态的模式。需要2个参数:
- value: T:要显示的当前值
- onValueChange: (T) -> Unit:请求更改值的事件,其中 T 是建议的新值
例如下例中,把 name变量和 onNameChange() 函数,抽出来放到 HelloScreen() 中,使 HelloContent() 变为无状态,更方便复用,代码如下:
@Composable
@Preview
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)) {
if (name.isNotEmpty()) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") })
}
}
整体框架是数据向下,事件向上的单向数据流,框架如下:
2.2 用 rememberSaveable 恢复状态
在重新创建 activity 或进程后,可以使用 rememberSaveable 恢复界面状态, rememberSaveable 可以在重组、重建 activity 和进程后保持状态。
添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择。
2.2.1 Parcelize
最简单的解决方案是向对象添加 @Parcelize 注解。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
2.2.2 MapSaver
如果某种原因导致 @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"))
}
}
2.2.3 ListSaver
为了避免需要为映射定义键,也可以使用 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"))
}
}
2.3 管理 state
下图所示为 Compose 状态管理所涉及的各实体之间的关系概览。本部分的其余内容详细介绍了每个实体:
- 可组合项可以依赖于 0 个或多个状态容器(可以是普通的对象、ViewModel 或二者皆有),具体取决于其复杂性。
- 如果普通的状态容器需要访问业务逻辑或屏幕状态,则可能需要依赖于 ViewModel。
- ViewModel 依赖于业务层或数据层。
2.3.1 state 的类别
- 界面元素状态:是界面元素的提升状态。例如,ScaffoldState 用于处理 Scaffold 可组合项的状态。
- 屏幕或界面状态:是屏幕上需要显示的内容。例如,CartUiState 类可以包含购物车中的商品信息、向用户显示的消息或加载标记。该状态通常会与层次结构中的其他层相关联,原因是其包含应用数据。
2.3.2 逻辑的类别
- 界面行为逻辑或界面逻辑:与如何在屏幕上显示状态变化相关。例如,导航逻辑决定着接下来显示哪个屏幕,界面逻辑决定着如何在可能会使用信息提示控件或消息框的屏幕上显示用户消息。界面行为逻辑应始终位于组合中。
- 业务逻辑:决定着如何处理状态变化,例如如何付款或存储用户偏好设置。该逻辑通常位于业务层或数据层,但绝不会位于界面层。
2.3.3 将可组合项作为可信来源
如果状态和逻辑比较简单,在可组合项中使用界面逻辑和界面元素状态是一种不错的方法。例如,以下是处理 ScaffoldState 和 CoroutineScope 的 MyApp 可组合项:
@Composable
fun MyApp() {
MyTheme {
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
MyContent(
showSnackbar = { message ->
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(message)
}
}
)
}
}
}
ScaffoldState 包含可变属性,因此,与之相关的所有交互都应在 MyApp 可组合项中进行。但是,如果我们将其传递给其他可组合项,这些可组合项可能会改变其状态,这不符合单一可信来源原则,而且会使对错误的跟踪变得更加困难。
2.3.4 将状态容器作为可信来源
当可组合项包含涉及多个界面元素状态的复杂界面逻辑时,应将相应事务委派给状态容器。这样更解耦:可组合项负责发出界面元素,而状态容器包含界面逻辑和界面元素的状态。
如果将可组合项作为可信来源部分中的 MyApp 可组合项的责任增加,我们就可以创建一个 MyAppState 状态容器来管理其复杂性,代码如下:
// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources,
/* ... */
) {
val bottomBarTabs = /* State */
// Logic to decide when to show the bottom bar
val shouldShowBottomBar: Boolean
get() = /* ... */
// Navigation logic, which is a type of UI logic
fun navigateToBottomBarRoute(route: String) { /* ... */ }
// Show snackbar using Resources
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, /* ... */)
}
其中,MyAppState 采用的是依赖项,因此最好提供可记住组合中 MyAppState 实例的方法。在上面的示例中为 rememberMyAppState 函数。
现在,MyApp 侧重于发出界面元素,并将所有界面逻辑和界面元素的状态委派给 MyAppState,代码如下:
@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") { /* ... */ }
}
}
}
如您所见,增加可组合项的责任会增加对状态容器的需求。这些责任可能存在于界面逻辑中,也可能仅与要跟踪的状态数相关。
2.3.5 将 ViewModel 作为可信来源
屏幕级可组合项
使用 ViewModel 实例,来提供对业务逻辑的访问权限,并作为界面状态的可信来源。
不应将 ViewModel 实例向下传递到其他可组合项。
ViewModel 的使用示例如下:
data class ExampleUiState(
val dataToDisplayOnScreen: List<Example> = emptyList(),
val userMessages: List<Message> = emptyList(),
val loading: Boolean = false
)
class ExampleViewModel(
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf(ExampleUiState())
private set
// Business logic
fun somethingRelatedToBusinessLogic() { /* ... */ }
}
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
/* ... */
ExampleReusableComponent(
someData = uiState.dataToDisplayOnScreen,
onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
)
}
@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
/* ... */
Button(onClick = onDoSomething) {
Text("Do something")
}
}
由于状态容器可组合,且 ViewModel 与普通状态容器的责任不同,因此屏幕级可组合项可以既有一个 ViewModel 来提供对业务逻辑的访问权限,又有一个状态容器来管理其界面逻辑和界面元素状态。由于 ViewModel 的生命周期比状态容器长,因此状态容器可以根据需要将 ViewModel 视为依赖项。
class ExampleState(
val lazyListState: LazyListState,
private val resources: Resources,
private val expandedItems: List<Item> = emptyList()
) {
fun isExpandedItem(item: Item): Boolean = TODO()
/* ... */
}
@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
val exampleState = rememberExampleState()
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item)) {
/* ... */
}
/* ... */
}
}
}