Jetpack Compose是Google最新提出的一个可以用声明式来绘制UI的框架。这个框架可以有效的提高UI的重复使用率,编程速度,以及UI的绘制效率。
现在Jetpack Compose是beta版本,API终于变得稳定了一点,我们用也可以认真地,全面地开始学习这个框架了。
这一篇我们首先学习一下Jetpack Compose的编程思想和一些名词。
大部分Android开发者都知道Jetpack Comopse是声明式UI编程。那到底什么是声明式编程呢。
命令式编程:用代码告诉系统一步步具体的步骤。
声明式编程:告诉系统最后需要实现的结果,具体实现的过程全部交给系统,不过问细节。
像Flutter和Compose等框架中,我们需要告诉系统我们想要构建怎样的UI,但是具体如何高效的渲染,如何管理UI的更新等问题全部交给系统,开发者则不必关心这些问题。
这样可以大幅度减小开发难度,提高开发速度,因为这些原因声明式编程逐渐成为了前端的主流。
@Composable是Compose中的注释,用于告知Compose编译器此函数是用于显示界面UI的函数。所有用于构建UI的函数都应该加上@Composable注释。
还有一点需要注意的是可组合函数不应该返回任何数据。因为它们描述所需的屏幕状态,而不是构造界面微件。
这里有一点需要注意的是,我们习惯性的把可组合函数写在文件的最顶层,这样就可以方便全局调用。
为了让可组合函数能被重复的利用,我们要考虑如何动态的展示内容和动态的设置布局。
听起来很唬人,但是具体做法是相当简单的。
我们在创建可组合函数时考虑哪些数据要动态的显示,这些数据将作为该函数的传入参数。
@ComposablefunGreeting(name: String){
Text("Hello $name")
}
复制代码
如上面代码可以知道,根据传入的name的不同,展示的内容也会不同。
接下来是关于动态的设置布局。
@ComposablefunGreeting(
name: String,
modifier: Modifier = Modifier
){
Text(
text = "Hello $name",
modifier = modifier.clickable { /*do something */}
)
}
复制代码
在上面代码中可以看出,我在传入参数中加入了Modifier。
Modifier是在Compose中为UI组件设置布局和点击事件的重要的类,每一个UI组件都可以设置。以后会详细介绍Modifier。
除了在Text中引用了Modifier以外还在Text内部加入了clickable。
因为可组合函数内部的布局是不需要改变,在这里所谓的动态布局指的是该布局在整体界面中的布局。
所以在传入参数中需要传入的是设置了padding等外部布局相关的Modifier。
如下代码。
Greeting(
name = "MOON",
modifier = Modifier.padding(2.dp)
)
复制代码
这里有两个注意点:
传入参数Modifier应该设置在内部的最顶层UI组件中。上面的例子只有Text,所以设置在Text即可。如果在Text的上层有Column或者Row的时候应该传入该组件中。
尽量不要在传入参数中直接传入数字来设置内部的布局,这个时候应该更进一步的切碎UI,设置成多个可组合函数。
@ComposablefunButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
复制代码
当我们看到上述代码时第一时间认为上述的代码是按顺序执行的。
因为每个函数的组成不同,优先级不同,所以实际上并不是按顺序执行的。
所以我们不能把StartScreen()设置某个全局变量并让MiddleScreen()的布局产生变化。(这里的全局变量会产生附带效应,下面会有讲到)
所以我们应该要做的是让三个布局互相保持独立。
Compose会用并行运行的方式提高构建UI的速度。
所以可组合函数可能在后台线程池中执行,如果某个可组合函数调用ViewModel中的函数,则Compose可能会同时从多个线程中调用该函数。
如下代码,Column和Text("Count: ${myList.size}")可能会并行运行。
@ComposablefunListComposable(myList: List) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
复制代码
首先我们先了解一下以下三个名词:
状态(state): 任何可以随时间变化的值。
事件(event): 通知程序发生了什么事情。
单向数据流模式(unidirectional data flow): 指的是向下传递状态,向上传递事件的设计模式。
具体在Compose中分析,就是把Activity中产生的event传给ViewModel,ViewModel再把值传回Activity。
如下图。下图所表示的就是unidirectional data flow。
如果在Compose中使用该模式会有以下3个好处:
可测试性(testability): 通过将状态和UI分离的方法,可以更轻松的测试Activity和ViewModel。
状态封装(State encapsulation): 因为状态只能在一个地方(ViewModel)进行更新,所以随着UI的增长,也不容易引入局部状态更新错误。
用户界面一致性(UI consistency): 通过使用Observable state holders(LiveData),所有状态更新都会立即反映在UI中。
好了,理论说的有点多了,我们尝试分析一下下面的例子。
可以看出Activity中event的改变传递给了ViewModel,如EditText中的文本发生变化的事件传递给ViewModel。
还可以看出ViewModel进行处理后把state传递给了Activity,如Activity观察了ViewModel的name来获得state。
总结如下。
event: 当文本输入更改时,UI会调用onNameChanged。
update stat: onNameChanged进行处理,然后设置_name的状态。
display state: 当name的观察者被调用时,会通知UI去更改状态。
classHelloCodelabViewModel: ViewModel() {
// LiveData holds state which is observed by the UI// (state flows down from ViewModel)privateval _name = MutableLiveData("")
val name: LiveData = _name
// onNameChanged is an event we're defining that the UI can invoke// (events flow up from UI)funonNameChanged(newName: String) {
_name.value = newName
}
}
classHelloCodeLabActivityWithViewModel : AppCompatActivity() {
privateval helloViewModel by viewModels()
overridefunonCreate(savedInstanceState: Bundle?) {
/* ... */
binding.textInput.doAfterTextChanged {
helloViewModel.onNameChanged(it.toString())
}
helloViewModel.name.observe(this) { name ->
binding.helloText.text = "Hello, $name"
}
}
}
复制代码
在Composable函数中如果传入的数据发生了改变,Compose会界面进行更新绘制,这一过程就叫重组(recomposition)。
Compose为了节省电量和提高绘制UI效率,只会重组已经改变了数据的组件。
比如下面的例子中,myList数据发生了改变时Compose只会更新与myList相关的组件,Text("End")并不会被更新。
@ComposablefunListComposable(myList: List) {
Column{
for (person in myList) {
Column{
Text("name: $person.name")
Text("age: $person.age")
}
}
Text("End")
}
}
复制代码
Compose在进行重组时,采用的策略是「跳过尽可能多的内容」。
在上面讲过,Compose只会更新数据发生或者状态发生改变的组件,而不执行其界面树中其上面或者下面与该数据无关的可组合项。
在下面的例子中,如果数据names发生了,Compose会跳过header,只在去更新names相关的组件。
/**
* Display a list of names the user can click with a header
*/@ComposablefunNamePicker(
header: String,
names: List,
onNameClicked: (String) -> Unit
) {
Column {
// this will recompose when [header] changes, but not when [names] changes
Text(header, style = MaterialTheme.typography.h5)
Divider()
// LazyColumnFor is the Compose version of a RecyclerView.// The lambda passed is similar to a RecyclerView.ViewHolder.
LazyColumnFor(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)
}
}
}
复制代码
只要Compose认为可组合项中的数据或状态发生了改变,就会开始重组。
重组是个乐观的操作,所以Compose会预计在下一次改变发生前完成重组。
但是如果在重组完成前再次发生变化时,Compose会取消当前的重组,并使用新数据重新开始重组。
在某些情况,Compose可能会对界面动画的每一帧运行一个可组合函数或者进行重组。如果在该函数中进行高昂的操作,如读取设备信息,可能会造成界面卡顿。(可能会在一秒内读取设备信息数百次,最终导致应用崩溃。)
该问题的解决方法是把相应的数据作为传入参数传给可组合函数,或者把高昂的操作移交给其他线程,在或者mutableStateOf或 LiveData把数据传递给可组合函数。
在上面讲过,Compose的可组合函数是不按顺序执行的。
因为多个线程同时进行访问的原因,像下面代码中的item会造成附带效应。
在Compose中不能有任何的附带效应,附带效应容易让应用产生未知的错误。
@Composable@Deprecated("Example with bug")funListWithBug(myList: List) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}
Android开发核心知识点笔记
Android Framework核心知识点笔记
Android Flutter核心知识点笔记与实战详解
音视频开发笔记,入门到高级进阶
性能优化核心知识点笔记
Android开发高频面试题,25个知识点整合
Android开发核心架构知识点笔记