【Android-JetpackCompose】2、Compose 编程思想

一、声明式编程

通常过程式范式中,Android 的视图表示为 widget 树,通过 inflate xml 文件来实现布局。每个 widget 都有自己内部的状态,提供 getter() 和 setter() 方法,允许 App 和 widget 交互。当状态变化而导致需更新 UI 时,常用 findViewById() 函数,通过 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。

手动操作 view 容易出错,同时更新同一个 view 会数据不一致。为了简化工作,业界都用声明式了,例如前端的 Vue、React 都是如此。我们只需用 kotlin 声明 UI,并且改变变量,剩下的都交给 Compose 库:当变量改变时,其会帮我们重绘依赖于此变量的 UI。

在 Compose 声明式范式中,widget 无状态,不提供 getter 和 setter 方法。实际上,widget 不是以对象形式提供,而是以 @Composable 函数提供,且该函数可接受不同的参数。这样只要状态改变,Compose 会自动监听到状态变化,并替我们更新UI。

数据流向是这样的:

首先,应用逻辑向 @Composable 函数传入数据,如果是嵌套的 @Composable 函数,就逐级传递到最底层,示例如下:

在这里插入图片描述

其次,当用户操作界面时,界面会发起 onClick() 等事件,这些事件会通知应用逻辑,应用逻辑随后更改数据。当数据变化时,Compose 会通知使用了这些数据的 @Composable 函数重绘,示例如下:

在这里插入图片描述

二、@Composable 函数

通过 @Composable 函数,我们可以声明 UI。
在这里插入图片描述
@Composable 函数有如下特点:

  • 带有 @Composable 的函数,Compose 编译器会将其转换为 UI。
  • @Composable 函数可接收参数,用参数来描述 UI。例如上例中的 Greeting() 函数中的 String 参数。
  • @Composable 函数可以嵌套,来构造更复杂的 UI。例如 Row() 嵌套 Column() ,再嵌套 Text() 等。
  • @Composable 函数没有返回值。
  • @Composable 函数快速、幂等、没有副作用。
  • 多次地、用同一参数,调用此函数,行为需相同。(如全局变量,或 ramdom() 函数)
  • 函数应没有副作用,如不应修改属性,或不应修改全局变量。

三、动态内容

由于可组合函数是用 Kotlin 而不是 XML 编写的,因此它们可以像其他任何 Kotlin 代码一样动态。可以使用 if 语句来确定是否要显示特定的界面元素。可以使用 for。可以调用辅助函数。有底层语言的全部灵活性。

例如,假设您想要构建一个界面,用来问候一些用户:

@Composable
fun Greeting(names: List<String>) {
    
    
    for (name in names) {
    
    
        Text("Hello $name")
    }
}

四、重组

重组是指:输入改变时,再次调用 @Composable 函数的过程。只需改变数据,Compose 会监听到数据变化并使函数重组:用新数据重绘。

例如,当下例的 clicks 变量变化时,因 Button 依赖此变量而会重组,代码如下:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    
    
    Button(onClick = onClick) {
    
    
        Text("I've been clicked $clicks times")
    }
}

切勿依赖于 @Composable 函数所产生的附带效应,防止发生奇怪、不可预测的行为。附带效应是指:对应用的其余部分可见的修改。例如,以下操作是危险的附带效应:

  • 写入共享对象
  • 更新 ViewModel 的可观察项
  • 更新 SharedPreferences

@Composable 函数可能会像每帧一样重复执行,所以应简洁快速,复杂的逻辑尽量放在后台协程中,并将协程的结果传给 @Composable 函数。例如,如果您的 widget 尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以将成本高昂的工作移至组成操作线程之外的其他线程,并使用 mutableStateOf 或 LiveData 将相应的数据传递给 Compose。

4.1 @Composable 函数应独立、可按任意顺序执行

例如下例中,对 StartScreen、MiddleScreen 和 EndScreen 的调用可按任意顺序执行,即我们不能让 A() 函数设置某全局变量(附带效应)并让 B() 函数利用这次修改。而应当保持各函数的独立。

@Composable
fun ButtonRow() {
    
    
    MyFancyNavigation {
    
    
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

4.2 @Composable 函数可并行

调用@Composable 函数时,可能会优化在后台线程池中执行。所以应避免修改 @Composable 函数的变量,因为应避免这些附带效应,避免线程不安全。

下例就是无附带效应的例子

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

但如果函数写入局部变量,就线程不安全了。例如下例中,每次调用都会修改 items 变量,会有附带作用,代码如下:

@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")
    }
}

5.3 重组会跳过尽量多的内容

Compose 置灰重组需要更新的部分,跳过无需更新的部分,例如下例代码:

/**
 * 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()

        // 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)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    
    
    Text(name, Modifier.clickable(onClick = {
    
     onClicked(name) }))
}

猜你喜欢

转载自blog.csdn.net/jiaoyangwm/article/details/127157701
今日推荐