Compose(7/N) - 状态 State

一、概念

Compose将界面(@Composable组合)和数据(State状态)分开,事件(Event)会引起状态的变化,进而使用了该状态的组合会发生重组。

二、MutableState

Compose内置的专门用来存储状态的容器,与其他观察者模式的容器(LiveData、StateFlow、Flow、Observable)一样可以观察到值的变化。在组合中创建 State 务必对其进行 remember 操作记住该值,否则每次重组都会初始化成默认值丢失数据。注意只适用于纯Compose项目因为不通用。注意:容器持有的数据若是可变对象如 MutableList 数据不会更新,需要的是对象的改变,这是对象内部数据的改变。

//ViewModel中管理
var strState = mutableStateOf("Hello Word!")
//Compose中使用
fun Input(viewModel:MyViewModel) {
    Text(text = viewModel.strState.value)
}
//Activity中各部分全部组合起来
@Composable
fun MainActivityScreen(viewModel: MyViewModel) {
    Input(viewmodel)
}

三、remember

可以为组合提供数据存储的功能,会记住该关键字所修饰的状态值(存储在UI树中),这样在重组后状态不会被初始化。在有添加或移除元素、重组发生在父元素中、屏幕旋转、恢复前台显示的情况下,使用 remember 会造成数据丢失变成初始值,见下方其它方式存储。

inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T

记住Lambda中生成的值(参数不带key,默认key无变化不会重置为始化值)。

inline fun <T> remember(key: Any?,crossinline calculation: @DisallowComposableCalls () -> T): T

当key值改变时,就执行Lambda重置为初始化值。key默认不变,会返回记住的值。

val str = remember{ mutableStateOf("默认值") }

//状态值只读。使用:str.value。

var str: String by remember{ mutableStateOf("默认值") }

//状态值可读可写。使用:str。需要导包androidx.compose.runtime.getValue和setValue。

val (value, setValue) = remember{ mutableStateOf(defaultValue) }

//状态值可读可写。解构的是某个实现类,value是显示的值,setValue是如何处理新值(事件的处理)。

//不需要ViewModel传入了
@Composable
fun Counter() {
    //内部创建了状态,难以复用和测试,见下方状态提升
    //只有在调用方不需要管理状态的时候使用
    val count = remember { mutableStateOf(0) }  //初始值为0
    Button(onClick = { count.value++ }) {
        Text(text = "点击了 ${count.value} 次")
    }
}
其它可观察容器需要转为State使用
LiveData val liveDataState = MutableLiveData("str").observeAsState()
Flow

val flowState = flow.collectAsState("str")

val flowState = flow.collectAsStateWithLifecycle("str")        //仅适用于Android,处于后台不收集。

四、状态提升(让组合复用)

        根据组合中是否有 State 可分为有状态组合和无状态组合。上面的1.4和1.5两个代码例子复用性不高难做测试 (有状态组合处于中间层或底层时,需要组合函数的形参层层传入自定义类型的ViewModel来拿到State对象,或是写到Activity中使用全局变量,或根据自己内部 remember 存储的状态变化自行改变显示内容)。

        有状态组合变为无状态组合需要状态提升,即将状态(数据源和事件) 移出到组合外给调用方处理,具体是将 State 替换为两个形参:要显示的数据(value),对事件的操作 (T) -> Unit,T是新值。事件提升状态下降即单向数据流设计。

  • 状态应至少提升到使用该状态(读取)的所有Composable的最低共同父项。
  • 状态应至少提升到它可以发生变化(写入)的最高级别。
  • 如果两种状态发生变化以响应相同的事件,它们应该一直提升。

//有状态
@Composable
fun InputWithState() {
    val str = remember { mutableStateOf("Hello Word!") }
    Column {
        Text(text = str.value)
        TextField(value = str.value, onValueChange = { str.value = it })
    }
}
//状态提升(中间层或底层不必传入ViewModel)
@Composable
fun InputWithoutState(value: String, onValueChange: (String) -> Unit) {
    Column {
        Text(text = str)
        TextField(value = value, onValueChange = onValueChange)
    }
}
//ViewModel中处理
var strState = mutableStateOf("Hello Word!")
fun onStrChange(newValue: String) {
    strState.value = newValue
}
//Activity中各部分全部组合起来(只在顶层组合传入ViewModel)
@Composable
fun MainActivityScreen(viewMidel:MyViewModel) {
    InputWithoutState(viewMidel.str, viewMidel.onStrChange)
}

五、其它状态存储方式

1.7.1 rememberSaveable( )

与 remember 类似,用于 Activity 或进程重建时恢复界面状态,类似于 onSaveInstanceState( ),任何可以存储在 Bundle 中的数据都可以通过 rememberSaveable( ) 进行存储。

1.7.2 @Parcelize

直接对数据类使用该注解,类似于 Java 中的 Serializable 都是将实例编码成字节流存储,但不会产生大量临时对象、没有反射效率更高,也不用写 Parcelable 模板代码。如果不涉及到本地化存储或者网络传输推荐使用。

// 第一步:添加插件
plugins {
    id 'kotlin-parcelize'    
}
//第二步:添加注解及Parcelable
@Parcelize    
data class City(val name: String, val country: String) : Parcelable
//第三步:保存到状态中
val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}

1.7.3 MapSaver( )

不适合用Parcelize的场景可以使用,定义自己的存储和回复规则,规定如何把实例转为可保存到 Bundle 中的值。通过 save 这个 lambda 可以将 Book 对象转化为一个 Map 进行存储;要使用的时候就通过 restore 这个 lambda 将 Map 又恢复为一个 Book 对象。

data class Book(val name: String, val author: String)

val BookSaver = run {
    val nameKey = "Name"
    val authorKey = "Author"
    mapSaver(
        save = { mapOf(nameKey to it.name, authorKey to it.author) },
        restore = { Book(it[nameKey] as String, it[authorKey] as String) }
    )
}

val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
	mutableStateOf(Book("三体","刘慈欣"))
}

1.7.4 ListSaver( )

List相对于上面的Map不用定义key。

val BookListSaver = listSaver<Book, Any>(
    save = { listOf(it.name, it.author) },
    restore = { Book(it[0] as String, it[1] as String) }
)

val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
	mutableStateOf(Book("三体","刘慈欣"))
}

六、状态持有者

考虑将状态保存在何处的因素有:界面状态(数据还是UI)、逻辑(业务还是UI)

组合 如果状态数量较少和逻辑比较简单,在组合中直接增加逻辑和状态是可以的,与其相关的交互都应该在这个组合进行。但是如果将它传递给其他组合,这就不符合单一可信来源原则,而且会使调试更多困难。
状态容器 当组合涉及多个界面的状态等复杂逻辑时,应将相应事务委派给状态容器。这样更易于单独对该逻辑进行测试,还降低了组合的复杂性。保证组合只是负责展示,而状态容器负责逻辑和状态。
ViewModel ViewModel 的生命周期往往是比较长的,原因是它们在配置发生变化后仍然有效。ViewModel 可以遵循 Activity、Fragment、或导航(如果使用了导航库)的生命周期。正因为 ViewModel 的生命周期较长,因此不应该长期持有和组合密切相关的一些状态,否则,可以会导致内存泄漏。如果 ViewModel 中包含要在进程重建后保留的状态,使用SavedStateHandle。

6.1 组合 作为可信来源

val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
    MyContent(
        showSnackbar = { message ->
            coroutineScope.launch { scaffoldState.snackbarHostState.showSnackbar(message) }
        }
    )
}

6.2 状态容器 作为可信来源

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) {
        //使用资源显示 snackbar
    }
}

//使用MyAppState 的时候需要使用remember来进行信赖
//可以创建一个rememberMyAppState方法来直接返回MyAppState实例。
@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

//使用
val myAppState = rememberMyAppState()
Scaffold(
    scaffoldState = myAppState.scaffoldState,
    bottomBar = {
        if (myAppState.shouldShowBottomBar) {
            BottomBar(
                tabs = myAppState.bottomBarTabs,
                navigateToRoute = { myAppState.navigateToBottomBarRoute(it) }
            )
        }
    }
) {
    NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}

6.3 ViewModel 作为可信来源

猜你喜欢

转载自blog.csdn.net/HugMua/article/details/129943188