jetpack compose 第二期 搭建界面与理解Compose编程思想

目标:本期我们要搭建一个简单的登录页面,了解Compose的编程思想,一些基本widget的使用。

今天的流程是先撸代码在讲解相关知识点。话不多说直接开始。


本期使用到的Widget,详细的API目前网上有很多介绍,大家可自行检索,这里不在赘述。

Column 

Text

TextField

Button


我们今天要实现的页面是登录页面,登录页面要有两个输入框分别输入用户名和密码,然后一个登录按钮。

首先我们打开 MainActivity 这个文件,删除掉模版自带的代码。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplication2Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {

                }
            }
        }
    }
}

这里我们要使用到 Column 这个widget,我们可以理解为纵向的LinerLayout,同时放置两个Text用于显示,我们先来看下效果。

Surface(...) {
    Column() {
        Text(text = "我是Text1")
        Text(text = "我是Text2")
    }
}

效果图如下:

 我们还需要两个输入框和一个按钮,这里我们使用material提供的TextField和button,代码如下:

Column() {
    Text(text = "请登录")
    TextField(value = "我是输入框1", onValueChange = {})
    TextField(value = "我是输入框2", onValueChange = {})
    Button(onClick = {  }) {
        Text(text = "登录")
    }
}

效果如下:

 注意:此时输入框是不接受任何输入值的,我们今天先搭建页面,后面在做逻辑。

UI页面基本搭建完毕,接下来我们对UI调整一下首先是水平居中显示使用horizontalAlignment属性,并水平全屏显示,同时添加padding边距,这里使用到的是widget的modifier属性,代码如下:

 效果图如下:

到此为止UI页面基本搭建完毕了,但是输入框还不能完全工作,我们现在要对输入框进行一下调整。

...

var userName by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }

TextField(
    modifier = Modifier.fillMaxWidth(),
    value = userName,
    onValueChange = {
        userName = it
    })


TextField(
    modifier = Modifier.fillMaxWidth(),
    value = password,
    onValueChange = {
        password = it
    })

...

关于为什么要这么实现,后面会有详细的解释,到此我们发现输入框可以相应输入法输入了。整理下代码最终呈现这个样子。

package com.dali.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.dali.demo.ui.theme.MyApplication2Theme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplication2Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    LoginScreen()
                }
            }
        }
    }
}

@Composable
fun LoginScreen(){
    var userName by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 16.dp, end = 16.dp),
    ) {
        Spacer(modifier = Modifier.height(48.dp))
        Text(text = "请登录")
        Spacer(modifier = Modifier.height(32.dp))
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = userName,
            onValueChange = {
                userName = it
            })
        Spacer(modifier = Modifier.height(8.dp))
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = password,
            onValueChange = {
                password = it
            })
        Spacer(modifier = Modifier.height(16.dp))
        Button(modifier = Modifier.fillMaxWidth(), onClick = { }) {
            Text(text = "登录")
        }
    }
}

到这里大家已经对Compose有了初步的认识,接下来我们来了解一下Compose的编程思想。

Compose编程思想

Jetpack Compose 是一个适用于 Android 的新式声明式界面工具包,Compose 提供声明式 API,我们可以不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。

@Composable 函数

使用Compose,我们需要定义一组接受数据的Composable函数,也可以叫做可组合函数来构建界面。还记得我们刚刚创建项目时候的例子吗?

这是一个简单的可组合函数,使用传入的数据在屏幕上显示文本widget。

  • 此函数带有@Compose注解,所有的可组合函数必须添加这个注解,此注解可告知Compose编译器,此函数要将数据转换为界面。
  • 此函数接受数据,可组合函数可以接受参数,这些参数用来描述UI界面,Greeting 函数接受一个String 显示。
  • 此函数用来文本显示,我们打开源码可以发现,Text()也是一个可组合函数,改函数会在界面上穿件文本见面元素,可组合函数通过调用其他可组合函数来发出界面层次结构。
  • 此函数不会返回任何内容,可组合函数不需要返回任何内容,因为他们描述所需的屏幕状态。
  • 此函数快速、幂等且没有任何附带效应。使用同一参数多次调用此函数时,行为方式相同,并且此函数不会对全局变量进行任何修改。

声明式编程

我们可以把 Android 的视图层次结构理解为一个 view tree,由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新显示数据。比如我们想要显示用户名称,需要在activity 初始化的时候使用 findView 函数找到 view,然后根据数据使用 setText 进行显示,如果数据有了更新,我们还需要手动的再次对 view 进行更新。这种方式我们可以叫做命令式UI系统。而声明式UI则是通过某种方式将数据和UI进行绑定,之后我们只要维护数据的后台逻辑,而不需要关心如何更新UI。

手动操作视图会提高出错的可能性,如果一条数据在多个位置呈现,狠容易忘记更新显示的逻辑,此外,当两个逻辑更新同意视图时,也非常容易造成一场状态。一般来说,软件维护的复杂程度会随着需要更新的视图数量而增长。

在 Compose 的声明式API中,widget 相对是无状态的,不会以对象的形式向外提供。通过调用不同的参数的统一可组合函数来实现界面的更新。

我们应用的逻辑为可组合函数提供用来显示的数据,该可组合函数通过调用其他可组合函数使用这些数据描述界面,并逐渐沿widget层次结构向下传递数据。当用户与界面交互时,界面会发起 onClick 等事件,这些事件通知应用逻辑,应用逻辑更新数据,系统会使用新数据再次调用可组合函数,这会导致重新绘制界面元素,这个过程我们可以成为“重组”。

用户与界面元素进行了交互,导致触发一个事件。应用逻辑响应该事件,然后系统根据需要使用新参数自动再次调用可组合函数。

重组

在命令式UI系统中,如果我们想要更改某个 view 的显示,可以在该 view 上调用方法更改其内部状态。在 Compose 中,则是需要使用新数据再次调用可组合函数,当数据进行了更改,函数会进行重组,系统会根据需要使用新数据重新调用可组合函数,Compose 框架可以智能地仅重组需要更改的组件。

比如,有这样一个例子,用来显示一个按钮,同时显示点击次数。

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

当每次点击按钮时,都会调用 onClick 回调函数,在回调函数中更新 clicks。此过程称为“重组”,不依赖于 clicks 值的其他函数不会进行重组。在今天我们搭建的界面中两个输入框的原理一样。有兴趣的可以打印log来查看一下。

重组整个界面树在计算上成本高昂,Compose 根据新输入调用可能已更改的函数或者lambda,跳过其余函数,通过这种方式,Compose 可以高效地重组。

可组合函数可能会每一帧都执行一次,也可能只执行一次就不再执行。所以我们应该避免在可组合函数中执行成本高昂的操作,如果有这样的操作应该在后台协程中执行,并将结果作为参数传递到可组合函数。

可组合函数执行顺序-顺序与并行

今天的例子中我们的的可组合函数是按顺序执行的,但在实际开发过程中未必这样,如果某个可组合函数包含对其他可组合函数,或者其他某个变量的更改,则可组合函数就不一定按顺序执行了。

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

比如这段代码中,我们绘制了三个 screen,如果这三个 screen 都各自保持独立,那么此函数都按着顺序执行。

当然,Compose 也可以通过并行运行可组合函数来优化重组,这样就可以利用多个核心以较低的优先级运行可组合函数。这样的优化也就意味着,所有的可组合函数不能有附带效应,应该通过UI线程上的 onClick 等回调出发。

比较一下以下的两个例子,显示了一个列表及其列表数

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

在第二个例子中,每次重组都会修改局部变量 items,不管怎么显示都是错误的列表数量。

重组会跳过尽可能多的内容

Compose 会尽力只重组需要更新的部分,跳过哪些不需要更新的可组合函数。每个可组合函数和lambda 都可以自己进行重组,一下例子中可以很好的说明。

@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // 当header发生变化,Text 会发生重组
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        LazyColumn {
            items(names) { name ->
                // 当names发生变化,这个地方会发生重组,header发生变化,则无影响
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

当header发生变化的时候,Compose可能会跳转到Column的lambda中,而不执行它的上层widget,执行Column时,如果names未更改,Compose可能会选择跳过LazyColumn。

重组是乐观的操作

只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。

取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组合操作,也会应用该附带效应。这可能会导致应用状态不一致。

确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。

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

在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。

例如,尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。

如果可组合函数需要数据,应该为相应的数据定义参数。然后将成本高昂的工作移至其他线程,并使用 mutableStateOf 或 LiveData 将相应的数据传递给 Compose。

总结一下,使用Compose时应该注意,最佳做法都是使可组合函数保持快速、幂等且没有附带效应。

  • 可组合函数可以按任何顺序执行。
  • 可组合函数可以并行执行。
  • 重组会跳过尽可能多的可组合函数和 lambda。
  • 重组是乐观的操作,可能会被取消。
  • 可组合函数可能会像动画的每一帧一样非常频繁地运行

猜你喜欢

转载自blog.csdn.net/z2008q/article/details/126031161
今日推荐