2月底的时候,Android 官方发布了Compose的完整课程。了解到许多小伙伴还没开始学习Compose,所以我写了一篇基础文章,让我们一起轻松上手Compose~
在这篇文章中我们将初步了解 Jetpack Compose,并学习可组合函数、基本布局和状态以及主题等基础知识。
Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助开发者简化并加快 Android 界面开发。
在此之前,我们如何实现一个业务功能呢?我们是在Activity中编写Java/Kotlin的代码,在XML中编写布局代码,这种方式是我们已经使用了很久的方式,而Jetpack Compose完全抛弃了之前的方式,新创造了一种“使用代码”编写页面的方式,而这种方式,有一个好听的名字,叫做声明式UI。接着我们来看,如何创建一个Compose项目?
我们直接选择Material3的Compose项目模板。
Compose最低支持的版本是21。创建好项目后,我们来看默认生成MainActivity的代码。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose01Theme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Compose01Theme {
Greeting("Android")
}
}
setContent类似setContentView一样为Activity设置布局,这里的Compose01Theme是根据项目名称生层的主题名称。这一块我们稍后再来了解。
Greeting函数使用了@Composeable注解称之为组合函数,@Composeable注解注释可告知 Compose 编译器,此函数需要转化为页面显示,并且和协程中suspend函数一样,只能在Composeable注解函数中调用另外一个Composeable注解函数,@Preview注解是方便开发者在不运行的前提下可预览效果。效果如下图所示。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Greeting函数中的Text组件,就是Compose提供的文本组件,类似XML方式中的TextView组件,代码如下所示:
<TextView
android:id="@+id/tvName"
android:layout_width="200dp"
android:layout_height="200dp"
/>
tvName.setText = "Hell $name!"
从上述对比可以看出来,Compose的写法是非常简洁的。
现在我们在Greeting函数中再添加一个Text组件,代码如下:
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
Text(text = "First Compose Demo")
}
运行程序,结果如下图所示。
我们看到文字都堆叠在一起了,我们知道在XML布局中有LinearLayout、RelativeLayout等布局组件,那么在Compose中有哪些布局呢?
Compose中的基础布局主要有Column、Row、Box等,接下来我们来看这些布局如何使用。
Column布局使得组件垂直排列,类似LinearLayout 的orientation属性设置为vertical。我们使用Column布局来解决上面的问题。修改Greeting方法代码如下所示:
@Composable
fun Greeting(name: String) {
Column() {
Text(text = "Hello $name!")
Text(text = "First Compose Demo")
}
}
运行程序,结果如下所示:
Row布局使得组件水平排列,类似LinearLayout 的orientation属性设置为horizontal,将上面两个Text改为水平排列,修改代码如下所示:
@Composable
fun Greeting(name: String) {
Row() {
Text(text = "Hello $name!")
Text(text = "First Compose Demo")
}
}
运行程序,结果如下所示。
Box相当于XML中的FrameLayout,还有ConstraintLayout等布局,这里就不一一展示了。感兴趣的大家可自行了解。
在上面的图中我们看到,两个Text紧紧的贴在一起了,在XML布局中我们可以使用padding或者margin来解决这个问题,在Compose中如何处理呢?以及我们如何为文字设置颜色、大小等样式呢?这就需要使用Compose的Modifier修饰符。
使用Compose修饰符可以更改大小、布局、外观与添加点击事件等。我们先来解决上面遗留的问题。
我们可以使用padding修饰符来为组件添加内边距。修改代码如下所示:
@Composable
fun Greeting(name: String) {
Row() {
Text(text = "Hello $name!")
Text(text = "First Compose Demo",
modifier = Modifier
.padding(10.dp)
.background(Color.Red)
.clickable {
//click
})
}
}
我们使用padding修饰符为Text添加了10dp的边距,使用background修饰符为Text添加红色的背景,使用clickable属性为Text添加点击事件。
运行程序,结果如下图所示。
在Compose中是没有类似margin外边距修饰符的。这是因为modifier修饰符的顺序会影响最终结果。怎么理解呢,比如我们将上面的代码背景与边距的先后顺序调整一下,代码如下所示:
@Composable
fun Greeting(name: String) {
Row() {
Text(text = "Hello $name!")
Text(text = "First Compose Demo",
modifier = Modifier
.background(Color.Red)
.padding(10.dp)
.clickable {
//click
})
}
}
再次运行程序,结果如下图所示。
这样先添加背景色,再设置边距就成了内边距的效果,同理,如果调整padding与clickable的修饰符,点击区域也会发生变化,感兴趣的可以自行尝试。
除此之外,modifier还提供了align对其方式等各种修饰符,需要大家在以后的使用中,慢慢熟悉。
到现在为止,我们已经学习了基础布局和修饰符的使用,接下来我们来根据效果图来“实战一下吧”~
接下来我们实现这样的一个效果图,文字和按钮左右排列,并为文字和按钮设置你喜欢的任意颜色。
OK,实践起来很简单,直接上代码~,如下所示:
@Composable
fun More() {
Row(modifier = Modifier
.background(Color.Red)
.padding(10.dp)) {
Text(text = "Compose课程已完善,快来学习吧~",
fontSize = 16.sp, color = Color.White, modifier = Modifier.weight(1f)
)
Button(onClick = { }) {
Text(text = "查看详情", color = Color.White)
}
}
}
这里你可能需要关注为Text设置字体大小颜色的方法以及Button组件,这是我们之前没有提到过的。运行程序,结果如下图所示。
如果我们想生成多条数据怎么办?我们只需要采用Kotlin语句就可以。代码如下所示:
Column() {
for(i in 0..10){
more()
}
}
因为组件要垂直排列,所以我们在外面套一个Column布局,然后for循环10次,运行程序,结果如下图所示。
现在数据是写死的,无法动态修改数据,More函数并不是一个可以复用的状态。接下来我们将More方法抽取为可复用的状态,即将相关参数提取出来。修改代码如下所示:
@Composable
fun More(title:String) {
Row(modifier = Modifier
.background(Color.Red)
.padding(10.dp)
) {
Text(text = title,
fontSize = 16.sp, color = Color.White, modifier = Modifier.weight(1f)
)
Button(onClick = { }) {
Text(text = "查看详情", color = Color.White)
}
}
}
然后在调用的地方动态生成数据,代码如下:
Column() {
for (i in 0..10) {
More("Compose课程第${i+1}课,快来学习吧~")
}
}
再次运行程序,结果如下图所示。
将Compose函数抽取为可复用的组合项,将会是我们经常使用的。
接着我们思考一个问题:如果生成20条数据呢?就会发现屏幕显示不下了,在XML中我们可以嵌套ScrollView或者修改成RecycleView的方式来处理。在Compose中最简单的一种处理方式就是为Column添加可滚动的属性,代码如下所示:
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
for (i in 0..10) {
More("Compose课程第${i+1}课,快来学习吧~")
}
}
这种方式虽然可以解决问题,但是当数据量很大的时候性能可能会非常低,Compose中也为我们提供了延迟列表组件。快来一起学习一下吧~
Compose为我们提供了LazyColumn和LazyRow组件,相当于XML中的RecycleView组件,从名字中我们也可以知道一个是垂直滚动一个是水平滚动。我们先来看垂直滚动列表组件 —— LazyColumn。
修改上面的代码如下所示:
LazyColumn(content = {
item {
for (i in 0..10) {
More("Compose课程第${i + 1}课,快来学习吧~")
}
}
})
LazyColumn API 会在其作用域内提供一个 item 元素,并在该元素中编写各项内容,当然在实际项目中我们可能会把数据包装起来,比如包装成一个list,比如我们定义一个生成20条数据的方法,代码如下所示:
fun getData(): List<String> {
return List(20) { "Compose课程第${it + 1}课,快来学习吧~" }
}
这样一来,上面的代码就变成了这样:
LazyColumn() {
items(items = getData()) { data ->
More(title = data)
}
}
运行程序,结果如下图所示:
我们也可以为LazyColumn设置其他属性,具体可自行参照LazyColumn的源码。
LazyRow与LazyColumn的使用方法是一样的,只是效果是水平滚动,这里简单看一下,修改代码如下所示:
LazyRow() {
items(items = getData()) { data ->
More(title = data)
}
}
运行程序,结果如下图所示。
我们都知道在RecycleView中还提供了网格布局布局和流布局,在Compose中也分别对应LazyGrid与LazyVerticalStaggeredGrid,感兴趣的大家可自行了解。
到现在为止我们已经实现了一个简单的列表实现,但是列表中的“查看详情”功能还没有实现。实现这一功能需要使用Compose中的状态,接下来我们就一起学习Compose中的状态吧~
我们说Compose是声明式的,与之对应的XML是命令式的,以文本设置值为例,命令式之所以被称之为命令式,是当文本变化的时候我们都需要手动调用textview.setText = “”,而由于 Compose 是声明式的,所以更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。可组合项也必须明确获知新状态,才能相应地进行更新。我们来通过一个实例看一下。
新建一个Compose函数,我们来尝试实现一个计数器的功能:点击加号按钮数字增加,代码如下所示:
@Composable
fun Counter(){
var number = 0
Column() {
Text(text = "当前数值:$number")
Button(onClick = {
number++
}) {
Text(text = "add")
}
}
}
文本显示变量number,文本和按钮垂直排列,点击按钮时number加1,运行程序,结果如下图所示:
一切看起来很正常,但是点击“add”我们会发现,文本中显示的数值并没有改变。这就是我们上面所说的必须明确获取新状态,才能进行相应的更新。
使用mutableStateOf 会创建可观察的 MutableState,源码如下所示:
interface MutableState<T> : State<T> {
override var value: T
}
使用 remember API 将对象存储在内存中。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。
当 value 发生变化时,系统就会将使用到 value 的所有可组合函数重组。所以这里我们将number变量改写,代码如下所示:
@Composable
fun Counter(){
var number by remember {
mutableStateOf(0)
}
Column() {
Text(text = "当前数值:$number")
Button(onClick = {
number++
}) {
Text(text = "add")
}
}
}
运行程序,结果如下图所示。
可以看到,这样当点击“add”按钮时,文本的数值会不断增加。因为我们将 number变量声明为State类型使其变为Compose可观察的状态,Compose监测到状态变化触发函数重组,这背后的原理得益于Compose的快照系统,感兴趣的大家可以自行了解。
Compose 是一个声明性界面框架。它描述界面在特定状况下的状态,而不是在状态发生变化时移除界面组件或更改其可见性。调用重组并更新界面后,可组合项最终可能会进入或退出组合。
不过Counter函数内部包含有状态的可组合项,可能不利于函数的复用,比如我们现在还有一个功能,每次点击计数器时数值加10,这个时候我们就要再copy一个函数改写代码。我们应该让可组合项尽可能的不保存任何状态。解决这个问题我们可以使用状态提升。
Compose 中的状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。如上面的计数器代码,我们是在setContent中调用的,代码如下所示:
Compose01Theme {
Surface(modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
color = MaterialTheme.colorScheme.background) {
Counter()
}
}
}
我们将状态提升到调用的地方,一般的状态提升是将状态变量替换为两个参数。
此值表示任何可修改的状态,比如计数器中的number变量,onValueChange只是一个方法名,根据上下文随意命名即可。
修改Counter函数代码如下所示:
@Composable
fun Counter(number: Int, onClickValue: () -> Unit) {
Column() {
Text(text = "当前数值:$number")
Button(onClick = onClickValue) {
Text(text = "add")
}
}
}
然后在调用的地方修改如下:
Surface(modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
color = MaterialTheme.colorScheme.background) {
var number by remember {
mutableStateOf(0)
}
Counter(number, onClickValue = {
number += 1
})
}
这样就完成了对counter函数的状态提升。如果现在我们有其他计数需求可以直接这样:
var number2 by remember {
mutableStateOf(0)
}
Counter(number, onClickValue = {
number += 10
})
通过状态提升我们可以达到组合项复用、解耦等目的。并且状态提升将状态下降,事件上升,也符合官方推荐的UDF单项数据流架构。
当然这里的number变量或者事件也可以来自ViewModel,这一点将在后面的课程中着重为大家分享。
了解了Compose的状态和状态提升之后我们现在回过头来看,如何实现上面课程列表查看详情的功能。
查看详情功能,这里我们设计为卡片展开样式,卡片展开后显示详情,所以我们需要定义一个变量控制是否展开详情,如果处于展开状态,则显示,并且按钮文字变为“收起”。相信你很快可以编写出下面的代码:
@Composable
fun more(title: String) {
var expanded by remember {
mutableStateOf(false)
}
Column() {
Row(modifier = Modifier
.background(Color.Red)
.padding(10.dp)
) {
Text(text = title,
fontSize = 16.sp, color = Color.White, modifier = Modifier.weight(1f)
)
Button(onClick = {
expanded = !expanded
}) {
Text(text = if (!expanded) "查看详情" else "收起", color = Color.White)
}
}
if (expanded) {
Text(text = "我是详情哈哈哈哈哈", modifier = Modifier.height(100.dp))
}
}
}
在XML实现这个功能我们可能是通过隐藏或显示组件,但是在Compose中我们通过是否将可组合项添加到界面树中来控制。如上代码所示,使用一个高度为100dp的文本组件充当详情。
运行程序,结果如下图所示。
Ok,非常的完美?仍然有一些小瑕疵,比如我们点击查看详情后,旋转屏幕会发现,原本展开的列表收起了。解决这个问题也非常的简单,我们将remember修改为rememberSaveable即可。这里就不再演示了。
我们也可以为查看详情添加动画效果,这一点你可以在学习了动画相关的内容后,自行尝试。
不知道你有没有发现,截图中的顶部和按钮颜色都是褐色的,并且文字也有默认的颜色,这都是Compose中的主题帮我们设置好的,最后我们一起简单了解一下吧~
在初识Compose项目中,我们已经知道,Compose01Theme是系统项目名称生成的,并且系统创建了一个theme文件夹,存放Theme、Color、Type。Compose01Theme代码如下所示:
@Composable
fun Compose01Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
从上述代码可以知道,默认情况下,如果手机系统为12及以上会使用根据darkTheme是否为暗色模式判断使用dynamicLightColorScheme主题或dynamicDarkColorScheme主题,否则直接使用亮色或暗色主题。
文件中定义的DarkColorScheme和LightColorScheme,代码如下所示:
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
dynamicLightColorScheme其实就是二次封装的LightColorScheme,为了便于修改主题,我们先将dynamicLightColorScheme换成LightColorScheme,代码如下所示:
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else LightColorScheme
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
运行程序,如下图所示。
我们看到标题栏的颜色和按钮的颜色都发生了改变,现在我们手动修改标题栏的颜色,从上面的代码中我们可以看到标题栏的颜色使用的是primary属性值。
所以如果我们想修改标题栏的颜色为蓝色,我们只需要修改primary的颜色值即可,在Color文件中定义蓝色值,代码如下所示:
val BLUE = Color(0XFF7CEFA)
然后替换到LightColorScheme中
private val LightColorScheme = lightColorScheme(
primary = BLUE,
secondary = PurpleGrey40,
tertiary = Pink40
)
再次运行程序,代码如下所示。
如此我们就成功修改了标题栏的颜色,当然我们还可以修改文本默认颜色、文本样式、文本形状等。这里就不再一一展示了。
到这里,本次的分享到这里就结束了,通过本次的分享,我们学到了以下内容:
相信,这篇文章足够让你从0入门Compose。不破不立,未来可期~