先说说为什么会出现Compose?
传统的命令式UI,如view这套组件构成的布局界面树,我们需要定义xml布局,最后还需要xmlpareser转换为java对象,这个过程需要遍历布局,布局嵌套多这个转换过程也会和很久;命令式UI通过findViewById遍历界面数后,set设置新值;
上面过程会引出几个问题:
- 随着UI嵌套加深,遍历耗时多
- onMeasure/onLayout存在多次测量和绘制
- setValue时有可能设置一个已经不存在的view,极易出错
那Compose又有哪些提高呢?
首先Compose声明式UI,通过声明@Compose注解表示一个可组合函数,也就是一个视图组件,使用简单:
@Compose
fun Text() {
Text{"this is a Text"}
}
Compose构成的视图,也存在的嵌套布局,但是无论是嵌套布局还是单层布局,Compose只会测量一次;
如上的Text组件的文字是写死的,也可以根据外部传入,Compose原理也是如此,当传入参数发生变化,会触发Compose将所有组合函数重组,也就是从新绘制界面,重组会导致所有界面数更新,但是Compose智能的选择只更新发生变化的组合函数
状态
Compose声明式UI不像以前我们setText会自动更新UI,它需要往可组合函数传入更新值来触发重组,Compose提供了remember来监控State参数状态,当参数状态发生改变时,自动传递到组合函数,重新重组(重组未完成之前,如果状态发生改变,会立即取消此次重组)
组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。
Compose状态管理之remember机制保存重组状态
组合函数可以通过remember记住单个对象,初始化的时候会将将remember的值存储到组合中去显示;当重组完成后又把组合中的值返回给remmber,当组合撤销不使用时,remember也会从组合中移除,remember的对象如下:
val value by remember {mutableStateOf("")}
官方示例:
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
name作为remember的观察返回值
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
输入组件
OutlinedTextField(
name变化将更新到value上
value = name,
输入时,会改变name的值,而name状态变化会更新到OutlinedTextField组件发生重组事件
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
- 以上这种组合函数内部也保存了name状态,这种叫做有状态组合项,这种方式对后期不适合调试的;另一种状态叫无状态,组合函数内部不保存状态值,可以重复使用,如下:
@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)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
- 使用和原理如上,这个对更新数据很又帮助,而常用的MVVM模式会用到livedata,compose也适配了该部分,但是livedata在使用时,需要将观察对象进行状态转换为state,如:
class HelloViewModel : ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
}
val name: String by helloViewModel.name.observeAsState("")
- 对于恢复数据,如activity、fragment重建则可以使用rememberSaveable :
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
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() {
remember对象时会传入saver对象
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
异常销毁,mapSaver保存,重建恢复mapSaver恢复,rememberSaveable使用
附带效应
可组合函数内部理应只做视图相关的事情,而不应该做函数返回之外的事情,如访问文件等,如果有,那这就叫做附带效应,也就是在可组合函数范围之外发生的应用状态变化。
切勿依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组。如果您这样做,用户可能会在您的应用中遇到奇怪且不可预测的行为。附带效应是指对应用的其余部分可见的任何更改。例如,以下操作全部都是危险的附带效应:
- 写入共享对象的属性
- 更新 ViewModel 中的可观察项
- 更新共享偏好设置
Effect附带效应事件
如果你需要在可组合函数中产生附带效应,需要使用EffectAPI,其原理是在Compose在API级别引入的kotlin的协程api,其提供的有:
LaunchedEffect、rememberCoroutineScope和DisposableEffect、SideEffect等操作的API
Effect原理实质是观测传入的key值发生变化,将执行第二个高阶函数,如下定义:
fun LunchedEffect(
key: Any?
block: suspend CoroutiScope.() -> Unit) {
remember(key1) { LaunchedEffectImpl(applyContext, block)}
}
对于LucnhedEffect观测的key值发生的变化时,效应也就是LaunchedEffect会重启,重启会带来资源的消耗,所以可以使用rememberUpdatedState来跟踪观测值,就不会发生重启,带来效率的提升和资源的节省;
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
/* Landing screen content */
}
LaunchedEffect只能在挂起函数中使用,而rememberCoroutineScope则可以在其外面使用,如下:
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler
// to show a snackbar
scope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
生命周期
可组合项的生命周期通过以下事件定义:进入组合,执行 0 次或多次重组,然后退出组合。
重组通常由对 State 对象的更改触发。Compose 会跟踪这些操作,并运行组合中读取该特定 State 的所有可组合项以及这些操作调用的无法跳过的所有可组合项。
在重组期间,可组合项调用的可组合项与上个组合期间调用的可组合项不同,Compose 将确定调用或未调用的可组合项,对于在两次组合中均调用的可组合项,如果其输入未更改,Compose 将避免重组这些可组合项。
部分实操用法
点击事件
带onClick事件的组件和不带两种;
带的,如Button(οnclick= onclick()) 指定即可
不带,则可以使用组件的modifier提供点击,如Image(modifier = Modifier.clickable{ xxx })
重组会跳过尽可能多的内容
如下,当入参只有header发生变化,则names对应的LazyColumn则可能会跳过执行,只更新发生变化的部分
@Composable
fun NamePicker(
header: String,
names: List<String>,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumn is the Compose version of a RecyclerView.
// The lambda passed to items() is similar to a RecyclerView.ViewHolder.
LazyColumn {
items(names) { name ->
// When an item's [name] updates, the adapter for that item
// will recompose. This will not recompose when [header] changes
NamePickerItem(name, onNameClicked)
}
}
}
}
重组是乐观的操作,它会预计参数发生改变后,完成重组,更新界面树;如果重组还未完成而此时数据发生或依赖项发生变化,则会取消此次重组;
重组可能会很频繁的运行,比如某个动画每一帧数据会运行一个可组合函数,所以尽可能不要在可重组函数中运行一些耗时的操作
如何居中布局?
居中一般在排列组件如Column、Row或Box的入参中指定,如Column的参数:
inline fun Column(
modifier: Modifier //Modifer决定可视图布局大小 位置以及pading等
verticalArrangement: Arrangemennt.Vertical, //纵向位置排列方式
horizontalAlignmnet: Alignment.Horizontal //横向排列,如居中等
)
如上要居中布局就改变horizontalAlignmnet参数即可
如何为组件添加背景效果?
比例布局属性
比如组件占据屏幕一半的属性,compose也又weight属性,比如我们要横向两个button,总共占据屏幕总宽度,两个button分别占一半,如下:
@Composable
private fun columnWeightChild() {
父占据总宽度
Column(modifier = Modifier.fillMaxSize()) {
Text(
text = "the first text",
fontSize = 30.sp,
color = Color.Black,
子属性weight为1f
modifier = Modifier.weight(1f)
.background(color = Color.LightGray)
)
Text(
text = "the second test",
fontSize = 12.sp,
color = Color.Red,
子属性weight为1f
modifier = Modifier.weight(1f)
.background(color = Color.DarkGray)
)
}
}
动画
AnimatedVisibility动画组件的定义:
fun ColumnScope.AnimatedVisibility(
visibleState : MutableTransitionState<Boolean> 观察对象
modifer: Modifer
enter: EnterTransaction 进入动画
exit: ExitTransition 退出动画
content: 内容有哪些组件
)
动画除以上之外Modifier也又提供:
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)