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

1. 简介

Jetpack Compose是Google最新提出的一个可以用声明式来绘制UI的框架。这个框架可以有效的提高UI的重复使用率,编程速度,以及UI的绘制效率。

现在Jetpack Compose是beta版本,API终于变得稳定了一点,我们用也可以认真地,全面地开始学习这个框架了。

这一篇我们首先学习一下Jetpack Compose的编程思想和一些名词。

2. 编程思想及专有名词

2.1 声明式编程

大部分Android开发者都知道Jetpack Comopse是声明式UI编程。那到底什么是声明式编程呢。

命令式编程:用代码告诉系统一步步具体的步骤。

声明式编程:告诉系统最后需要实现的结果,具体实现的过程全部交给系统,不过问细节。

像Flutter和Compose等框架中,我们需要告诉系统我们想要构建怎样的UI,但是具体如何高效的渲染,如何管理UI的更新等问题全部交给系统,开发者则不必关心这些问题。

这样可以大幅度减小开发难度,提高开发速度,因为这些原因声明式编程逐渐成为了前端的主流。

2.2 可组合 (Composable)

@Composable是Compose中的注释,用于告知Compose编译器此函数是用于显示界面UI的函数。所有用于构建UI的函数都应该加上@Composable注释。

还有一点需要注意的是可组合函数不应该返回任何数据。因为它们描述所需的屏幕状态,而不是构造界面微件。

这里有一点需要注意的是,我们习惯性的把可组合函数写在文件的最顶层,这样就可以方便全局调用。

2.3 动态的展示内容和布局

为了让可组合函数能被重复的利用,我们要考虑如何动态的展示内容和动态的设置布局。

听起来很唬人,但是具体做法是相当简单的。

我们在创建可组合函数时考虑哪些数据要动态的显示,这些数据将作为该函数的传入参数。

@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)
)
复制代码

这里有两个注意点:

  1. 传入参数Modifier应该设置在内部的最顶层UI组件中。上面的例子只有Text,所以设置在Text即可。如果在Text的上层有Column或者Row的时候应该传入该组件中。

  1. 尽量不要在传入参数中直接传入数字来设置内部的布局,这个时候应该更进一步的切碎UI,设置成多个可组合函数。

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

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

当我们看到上述代码时第一时间认为上述的代码是按顺序执行的。

因为每个函数的组成不同,优先级不同,所以实际上并不是按顺序执行的。

所以我们不能把StartScreen()设置某个全局变量并让MiddleScreen()的布局产生变化。(这里的全局变量会产生附带效应,下面会有讲到)

所以我们应该要做的是让三个布局互相保持独立。

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

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}")
    }
}
复制代码

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

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

  1. 状态(state): 任何可以随时间变化的值。

  1. 事件(event): 通知程序发生了什么事情。

  1. 单向数据流模式(unidirectional data flow): 指的是向下传递状态,向上传递事件的设计模式。

具体在Compose中分析,就是把Activity中产生的event传给ViewModel,ViewModel再把值传回Activity。

如下图。下图所表示的就是unidirectional data flow。

让你易上手的Jetpack Compose教程 - 1. Compose的编程思想_第1张图片

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

  1. 可测试性(testability): 通过将状态和UI分离的方法,可以更轻松的测试Activity和ViewModel。

  1. 状态封装(State encapsulation): 因为状态只能在一个地方(ViewModel)进行更新,所以随着UI的增长,也不容易引入局部状态更新错误。

  1. 用户界面一致性(UI consistency): 通过使用Observable state holders(LiveData),所有状态更新都会立即反映在UI中。

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

可以看出Activity中event的改变传递给了ViewModel,如EditText中的文本发生变化的事件传递给ViewModel。

还可以看出ViewModel进行处理后把state传递给了Activity,如Activity观察了ViewModel的name来获得state。

总结如下。

  1. event: 当文本输入更改时,UI会调用onNameChanged。

  1. update stat: onNameChanged进行处理,然后设置_name的状态。

  1. 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"
       }
   }
}
复制代码

2.4 重组 (recomposition)

在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")
    }  
}
复制代码
2.4.1 重组会跳过尽可能多的内容

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

只要Compose认为可组合项中的数据或状态发生了改变,就会开始重组。

重组是个乐观的操作,所以Compose会预计在下一次改变发生前完成重组。

但是如果在重组完成前再次发生变化时,Compose会取消当前的重组,并使用新数据重新开始重组。

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

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

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

2.5 附带效应 (side-effect)

在上面讲过,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开发核心知识点笔记

Android Framework核心知识点笔记

Android Flutter核心知识点笔记与实战详解

音视频开发笔记,入门到高级进阶

性能优化核心知识点笔记

Android开发高频面试题,25个知识点整合

Android开发核心架构知识点笔记

你可能感兴趣的:(Android,Jetpack,android,ui,android,jetpack,性能优化,compose)