【译】程序员都应该了解的Jetpack Compose的概念

1,015 阅读8分钟

Jetpack Compose Concepts Every Developer Should Know
作者:Clinton Teegarden

原文链接

什么是Compose

Jetpack Compose是Android新的声明式UI框架。Android开发早已熟悉用xml写UI,在视图的层级中穿梭来更新这些带状态的视图。而Jetpack Compose,通过使用Kotlin的函数编写的是无状态的UI。

Composable函数通过添加@Composable的注解。Composable的函数必须加上这个注解,这样编译器才能知道这个函数是往视图层级里面添加UI。Composable函数本身只能被其他Composable函数调用,但是它们自己可以调用其他标准函数。

如果你还没开始用Compose,我强烈推荐你去看看这里的给Compose学习资料,这里提供了一些基础的细节和示例可以让你快速开始你的Compose之旅。

单项数据流

Compose 是基于单项数据流来构造的,这样一来Compose的编程模型对合理实现Compose有比较大的要求。跟传统的Android UI体系不同的是,Composable相对来说说是无状态的——也就是说UI的展示状态应该通过Composable函数自己的参数传递进去。

在大部分基础的场景下,不管事件(events)是从UI来(按钮点击,文本输入等等)或者其他地方传来(API调用,回调等等)都是通过handler来处理,完了传递给Composable函数去更新UI的状态。因为Composable是没有状态的,会根据提供的UI状态来构建UI。

在上面的流程图里面,UI层就是Composable。事件从这一层出发,比如按钮点击传递给Event Handler,比如ViewModel。ViewModel通过LiveData/StateFlow进行UI状态控制。随着状态的改变,这些更新会被推送给Composable用最新更新的状态进行重组。

下面的代码演示了上面单项的数据流——从ViewModel收集状态,将事件发送给ViewModel,然后通过ViewModel进行状态更新。

MVI_ComposeTheme {
    Surface(color = MaterialTheme.colors.background) {
        val state = viewModel.viewState.collectAsState().value
        Button(onClick = { viewModel.processEvent()}) {
            Text(state.message)
        }
    }
}

组合和重组

对于用户组合就是Composable函数执行和UI创建的过程。重组就是随着Composable用于展示的状态和数据发生变化的时候更新UI的过程。在重组的过程中,compose能知道是哪个Composable用到的数据发生了变化,并只更新有变化的UI组件。其他不变的Composable则跳过。

在整个生命周期里面,组合和重组其实是不同的。

  • Composable函数可能一帧都会进行重组,例如动画
  • Composable函数可能会有任意执行顺序
  • Composable函数可能会并行执行

这就意味着不能包含当组合函数里面执行时要做的逻辑——有时候称为副作用。

Compose_Theme {
    MainScreen()
    
    // DO NOT DO THIS
    viewModel.makeAPICall()
}

有状态的Composable和保存实例状态告别

虽然我们的目标是让Composable主要是无状态的,但是有时候我们也需要部分是有状态的,比如记住滚动状态,在Composable之间共享一个变量等等。因为重组每一帧都可能发生,如果每次重组就会丢掉滚动的位置就不好了。

我们可以在Composable里面通过创建变量来记录这个值:

val myInt = remember{ Random(10).nextInt() }

在上面的例子里,这个随机生成的整数会在组合的过程被记住二不需要计算。如果这个整数没有被remember包裹,每次重组都会重新计算。

更进一步,我们在配置发生变化的时候也能记住。

val myInt = rememberSaveable{ Random(10).nextInt() }

有时候我们需要这样一个记住的变量,当一个Composable更新导致另一个Composable的重组。在下面这个例子里,按钮点击会增加计数器,这个计算器被Text Composable用来展示。因此,当按钮点击的时候,我们想Text显示内容随着计算增加而变化。

为了实现这个目标,我们会像上面的例子一样用到Compose提供的remember函数,但是这个整数会被MutableState的对象包装。MutableState类会持有一个单一值,读写都可以被Compose监听到,并且会导致相关的Composable发生重组。

Column{
    // create state for buttonCount and a function to update it -      
    // setButtonCount
    val (buttonCount, setButtonCount) = 
       rememberSaveable { mutableStateOf(0) }
    Button(onClick = {
        setButtonCount(buttonCount + 1)
    }) {
        Text(text = "Press Me!")
    }
    // recomposes whenever button is pressed
    Text(text = "Button Pressed $buttonCount")
}

槽(slot)API

Compose引入了槽API的概念。这可以让Composable实现高度定制,而不需要Composable函数提供大量的实现去做多种多样的定制UI。因为每个场景和实现可能不同,槽API支持在Composable里用空的slots来定制UI。

例如,有很多按钮需要不进展示文本,有的需要展示加载条,有的在左边展示图标,有的图标展示在右边。槽API支持这种多样性的定制Composable。

其他的Composable,类似Scaffolds完全就是用槽实现的。想象一下用这些保留间隔的各种各样UI组件来构建你的UI会是什么样的,这些组件有toolbar,bottomNav,Drawer,Screen Content等等。

修饰器(Modifier)

Modifier可以同xml的属性进行类比,通常用来设置UI的样式。但是modifier更简单,还有一些奇技淫巧。modifier可以让你装饰和修改Composable的默认实现。修改外观,添加无障碍信息,处理UI交互事件等等都可以通过modifier来实现。由于modifier只是Kotlin的对象,因此也可以添加到自定义的Modifier。

Text(text = "Your Text", modifier = Modifier.padding(5.dp))

Modifier是是非常强大的,因为它提供了实现Composable层而不需要嵌套到其他Composable的可能。例如下面这个例子,在Android传统UI里面不嵌套很多View是无法实现的。

但是,用Modifier用一个Composable就能实现。这是因为Modifier是有序的,通过padding,color的不同顺序实现了不同UI的组合。

Text(text = "Fake Button",
    modifier = Modifier.padding(5.dp)
        .background(Color.Magenta)
        .padding(5.dp)
        .background(Color.Yellow))

懒加载列表(Lazy List)

LazyLists是一个类似RecyclerViews的Compose。我会跟你说,我不会再怀念之前写RecyclerView Adapters, ViewHolders还有其他一些相关的模板代码了。下面这个例子展示了LazyList的列形式(纵向滚动)基于奇数偶数展示不同的UI元素。如果用RecyclerView的话,我们需要一个Adapter,至少两个不同的ViewHolder。而用Compose,我们只需要一个LazyColumn 的列来动态添加内容的Composable函数。

val listSize = 100
LazyColumn {
    items(listSize) {
        if (it % 2 == 0) {
            Text("I am even")
        }else{
            Text("I am odd")
        }
    }
}

就是这个了,这可能是我用了Compose的最开心的事情吧。

约束布局(Constraint Layout)

Compose有它自己的约束布局版本,这个我们在传统UI里面所熟悉和喜爱的。约束布局在传统UI系统里面是强烈推荐的,因为它可以实现高度定制UI的方式通过相对于其他View来布局二不需要嵌套。有了Compose之后,嵌套不再是一个问题了,但是有时候还是需要一些工具比如constraints, barriers, weights等等约束布局去提供。

ConstraintLayout {
    // Creates references for the three Composables
    val (button1, button2, text) = createRefs()
    Button(
        // constraintAs is like setting the ID, required.
        modifier = Modifier.constrainAs(button1) {
            top.linkTo(parent.top, margin = 16.dp)
        }
    ) {
        Text("Button 1")
    }
    // constraintAs is like setting the ID, required.
    Text("Text", Modifier.constrainAs(text) {
        top.linkTo(button1.bottom, margin = 16.dp)
        centerAround(button1.end)
    })
   // Create barrier to set the right button to the right of the             
   button or the text, which ever is longer. 
   val barrier = createEndBarrier(button1, text)
    Button(
        modifier = Modifier.constrainAs(button2) {
            top.linkTo(parent.top, margin = 16.dp)
            start.linkTo(barrier)
        }
    ) {
        Text("Button 2")
    }
}

Compose和Navigation

Jetpack Compose里面的Navigation可以实现很多我们习惯用的Jetpack Navigation。但是Jetpack Compose可以让我们在同一个Activity里面进行Navigate不同页面,二不需要多个fragment。

只需要创建一个NavHost嵌套一个Screen Composable到里面。

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
    composable("home"){
        val vm: HomeVM = viewModel()
        HomeScreen(vm)
    }
    composable("settings"){
        val vm: SettingsVM = viewModel()
        SettingsScreen(vm)
    }
    composable("profile"){
        val vm: ProfileVM = viewModel()
        ProfileScreen(vm)
    }
}

通过androidx.lifecycle:lifecycle-viewmodel-compose可以在Composable内创建ViewModel:

val vm: MyVM = viewModel()

Composable内部创建的ViewModel会一直保留直到它们的scope(Activity/Fragment)被销毁。这让你可以像现在同fragment一样来同NavHost Screen。在我的例子里面,我是在NavigationHost里面创建的ViewModel,这是是一个例子方便你去了解它的用法。

把NavHost应用于BottomNav或者其他Navigation组件,你可以用Scaffold:

Scaffold(
    bottomBar = {
        BottomNavigation {
            // your navigation composable here
        }
    },
) {
    NavHost(
      navController = navController, 
      startDestination = "home") {
        // your screen composables 
    }
}

小结

上面的概念只是Compose给我们提供的一个介绍。Compose 相对于Android开发者经常用来构建UI的方式有很大不同,但是确实一个很高兴看到的改变,因为它大大简化了传统UI体系的很多问题。如果你还没开始用Compose,我强烈推荐你去看看这里的给Compose学习资料。虽然非常长,但是里面每个字都值得你去花时间。祝你有一个愉快的Compose之旅。