阅读 3266
让你易上手的Jetpack Compose教程 - 1. Compose的编程思想

让你易上手的Jetpack Compose教程 - 1. Compose的编程思想

1. 简介

Jetpack Compose是Google最新提出的一个可以用声明式来绘制UI的框架。这个框架可以有效的提高UI的重复使用率,编程速度,以及UI的绘制效率。
现在Jetpack Compose是beta版本,API终于变得稳定了一点,我们用也可以认真地,全面地开始学习这个框架了。
这一篇我们首先学习一下Jetpack Compose的编程思想和一些名词。

2. 编程思想及专有名词

2.1 声明式编程

大部分Android开发者都知道Jetpack Comopse是声明式UI编程。那到底什么是声明式编程呢。
命令式编程:用代码告诉系统一步步具体的步骤。
声明式编程:告诉系统最后需要实现的结果,具体实现的过程全部交给系统,不过问细节。

FlutterCompose等框架中,我们需要告诉系统我们想要构建怎样的UI,但是具体如何高效的渲染,如何管理UI的更新等问题全部交给系统,开发者则不必关心这些问题。
这样可以大幅度减小开发难度,提高开发速度,因为这些原因声明式编程逐渐成为了前端的主流。

2.2 可组合 (Composable)

@ComposableCompose中的注释,用于告知Compose编译器此函数是用于显示界面UI的函数。 所有用于构建UI的函数都应该加上@Composable注释。
还有一点需要注意的是可组合函数不应该返回任何数据。因为它们描述所需的屏幕状态,而不是构造界面微件。
这里有一点需要注意的是,我们习惯性的把可组合函数写在文件的最顶层,这样就可以方便全局调用。

2.3 动态的展示内容和布局

为了让可组合函数能被重复的利用,我们要考虑如何动态的展示内容和动态的设置布局。
听起来很唬人,但是具体做法是相当简单的。
我们在创建可组合函数时考虑哪些数据要动态的显示,这些数据将作为该函数的传入参数。

@Composable
fun Greeting(name: String){
    Text("Hello $name")
}
复制代码

如上面代码可以知道,根据传入的name的不同,展示的内容也会不同。

接下来是关于动态的设置布局。

@Composable
fun Greeting(
    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)
)
复制代码

这里有两个注意点:

  1. 传入参数Modifier应该设置在内部的最顶层UI组件中。上面的例子只有Text,所以设置在Text即可。如果在Text的上层有Column或者Row的时候应该传入该组件中。
  2. 尽量不要在传入参数中直接传入数字来设置内部的布局,这个时候应该更进一步的切碎UI,设置成多个可组合函数。

2.4 可组合函数的按任何顺序执行和并行运行

2.4.1 可组合函数可以按任何顺序执行
@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}
复制代码

当我们看到上述代码时第一时间认为上述的代码是按顺序执行的。
因为每个函数的组成不同,优先级不同,所以实际上并不是按顺序执行的。
所以我们不能把StartScreen()设置某个全局变量并让MiddleScreen()的布局产生变化。(这里的全局变量会产生附带效应,下面会有讲到)
所以我们应该要做的是让三个布局互相保持独立。

2.4.2 可组合函数可以并行运行

Compose会用并行运行的方式提高构建UI的速度。
所以可组合函数可能在后台线程池中执行,如果某个可组合函数调用ViewModel中的函数,则Compose可能会同时从多个线程中调用该函数。
如下代码,ColumnText("Count: ${myList.size}")可能会并行运行。

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}
复制代码

2.3 状态是向下传递,事件是向上传递 (state flows down and events flow up)

首先我们先了解一下以下三个名词:

  1. 状态(state): 任何可以随时间变化的值。
  2. 事件(event): 通知程序发生了什么事情。
  3. 单向数据流模式(unidirectional data flow): 指的是向下传递状态,向上传递事件的设计模式。

具体在Compose中分析,就是把Activity中产生的event传给ViewModel,ViewModel再把值传回Activity。
如下图。下图所表示的就是unidirectional data flow

1bb3728573d00d8d.png

如果在Compose中使用该模式会有以下3个好处:

  1. 可测试性(testability): 通过将状态和UI分离的方法,可以更轻松的测试Activity和ViewModel。
  2. 状态封装(State encapsulation): 因为状态只能在一个地方(ViewModel)进行更新,所以随着UI的增长,也不容易引入局部状态更新错误。
  3. 用户界面一致性(UI consistency): 通过使用Observable state holders(LiveData),所有状态更新都会立即反映在UI中。

好了,理论说的有点多了,我们尝试分析一下下面的例子。

可以看出Activity中event的改变传递给了ViewModel,如EditText中的文本发生变化的事件传递给ViewModel。
还可以看出ViewModel进行处理后把state传递给了Activity,如Activity观察了ViewModel的name来获得state。

总结如下。

  1. event: 当文本输入更改时,UI会调用onNameChanged
  2. update stat: onNameChanged进行处理,然后设置_name的状态。
  3. display state: 当name的观察者被调用时,会通知UI去更改状态。
class HelloCodelabViewModel: ViewModel() {

   // LiveData holds state which is observed by the UI
   // (state flows down from ViewModel)
   private val _name = MutableLiveData("")
   val name: LiveData<String> = _name

   // onNameChanged is an event we're defining that the UI can invoke
   // (events flow up from UI)
   fun onNameChanged(newName: String) {
       _name.value = newName
   }
}

class HelloCodeLabActivityWithViewModel : AppCompatActivity() {
   private val helloViewModel by viewModels<HelloCodelabViewModel>()

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */

       binding.textInput.doAfterTextChanged {
           helloViewModel.onNameChanged(it.toString()) 
       }

       helloViewModel.name.observe(this) { name ->
           binding.helloText.text = "Hello, $name"
       }
   }
}
复制代码

2.4 重组 (recomposition)

在Composable函数中如果传入的数据发生了改变,Compose会界面进行更新绘制,这一过程就叫重组(recomposition)
Compose为了节省电量和提高绘制UI效率,只会重组已经改变了数据的组件。
比如下面的例子中,myList数据发生了改变时Compose只会更新与myList相关的组件,Text("End")并不会被更新。

@Composable
fun ListComposable(myList: List<Person>) {
    Column{
        for (person in myList) {
            Column{
                Text("name: $person.name")
                Text("age: $person.age")
            }    
        }
        Text("End")
    }  
}
复制代码
2.4.1 重组会跳过尽可能多的内容

Compose在进行重组时,采用的策略是「跳过尽可能多的内容」
在上面讲过,Compose只会更新数据发生或者状态发生改变的组件,而不执行其界面树中其上面或者下面与该数据无关的可组合项。
在下面的例子中,如果数据names发生了,Compose会跳过header,只在去更新names相关的组件。

/**
 * Display a list of names the user can click with a header
 */
@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()

        // 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)
        }
    }
}
复制代码
2.4.2 重组是乐观的操作

只要Compose认为可组合项中的数据或状态发生了改变,就会开始重组。
重组是个乐观的操作,所以Compose会预计在下一次改变发生前完成重组。
但是如果在重组完成前再次发生变化时,Compose会取消当前的重组,并使用新数据重新开始重组。

2.4.3 可组合函数可能会非常频繁地运行

在某些情况,Compose可能会对界面动画的每一帧运行一个可组合函数或者进行重组。如果在该函数中进行高昂的操作,如读取设备信息,可能会造成界面卡顿。(可能会在一秒内读取设备信息数百次,最终导致应用崩溃。)

该问题的解决方法是把相应的数据作为传入参数传给可组合函数,或者把高昂的操作移交给其他线程,在或者mutableStateOfLiveData把数据传递给可组合函数。

2.5 附带效应 (side-effect)

在上面讲过,Compose的可组合函数是不按顺序执行的。
因为多个线程同时进行访问的原因,像下面代码中的item会造成附带效应。
在Compose中不能有任何的附带效应,附带效应容易让应用产生未知的错误。

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    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")
    }
}
复制代码

3. 其他

让你易上手的Jetpack Compose教程

1. Compose的编程思想

Jetpack相关教程

1. 让你易上手的Jetpack Paging3教程
2. 让你易上手的Jetpack DataStore教程
3. Android Jetpack Room的详细教程
4. Paging在Android中的应用
5. Android WorkManager的使用

其他教程

1. 神一样的存在,Dagger Hilt !!
2. Android10的分区存储机制(Scoped Storage)适配教程
3. Android的属性动画(Property Animation)详细教程
4. Android ConstraintLayout的易懂教程
5. Google的MergeAdapter的使用

文章分类
Android