Android Jetpack Compose的使用

414 阅读8分钟

Compose 简介

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

声明式 UI

声明式UI的意思就是,描述你想要一个什么样的UI界面,状态变化时,界面按照先前描述的重新“渲染”即可得到状态绝对正确的界面,而不用像命令一样,告诉程序一步一步该干什么,维护各种状态。关于声明式的更多介绍,可以看这篇文章:zhuanlan.zhihu.com/p/68275232

组合函数

Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素,将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

setContent 设置了 activity 的布局,在其中调用可组合函数Greeting,将文本渲染到界面上。可组合函数只能从其他可组合函数调用,这里的Text()也是组合函数。 可组合函数其实就是UI组件集合函数,通过引用不同的可组合函数,组成我们需要的UI界面。
想要预览界面,在没有参数的组合函数上添加@Preview注解

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeDemoTheme {
        Greeting("Android")
    }
}

Modifier

一个 有序的不可变的修饰元素集合,用于添加装饰或者行为到Compose UI元素。例如background、padding 、点击事件等。或者给Text设置单行、给Button设置各种点击状态等行为。UI元素通用的属性一般都在Modifier里面找

Text(
    text = "Hello $name!",
    Modifier
        .wrapContentHeight()
        .wrapContentWidth()
        .background(Color.Blue)  设置背景蓝色
        .padding(20.dp)  设置内间距20,此时的间距是对应到蓝色框的边
        .background(Color.Green) 覆盖背景色为绿色
        .padding(50.dp) 设置内间距为50,此时的间距是对应到绿色框的边
        .clickable { 设置点击事件,点击事件只有点击到文字才会生效
            Toast
                .makeText(context, "hello", Toast.LENGTH_SHORT)
                .show()
        },
    textAlign = TextAlign.Center,
    color = Color.Red
)

1.png

Padding

Compose没有设置外边距的地方是因为不需要,用Padding就能实现。
跟原生UI不一样,重复调用setPadding、setBackground,原生会进行覆盖。
而Compose UI则是下发式一层一层传递处理,不会丢失上一次处理结果,变得很灵活。 所以如果要设置外边距,先padding,再处理其他;
设置一个背景多个不同点击事件,隔层次设置clickable即可

layout_width / layout_height

// Compose中可以不写,默认宽高都是wrap_content

// 分开设置宽高
Modifier.width(100.dp).height(100.dp)

// 同步设置宽高
Modifier.size(100.dp)

// match_parent,需手动设置
Modifier.fillMaxWidth()
Modifier.fillMaxHeight()

// 宽高撑满
Modifier.fillMaxSize()

Text

Text是显示文本的组件,最常用的组件,都没有之一,text参数是必须要传的,其它的可以为空。基本用法如下

Text(text = "hello")

Text的组合函数有以下参数

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) 

fontStyle 字体的风格,有两个值,Normal是普通的,Italic斜体
fontWeight 文字的粗细,值是1-1000,常用的加粗是700 可以在FontWeight查看
fontFamily 渲染文本时使用的字体系列 默认情况下,系统会添加 Serif、Sans Serif、等宽和 Cursive 字体系列
letterSpacing 设置字体之间的间距
textDecoration用于设置文本的下划线和中划线, TextDecoration.Underline下划线,TextDecoration.LineThrough中划线
TextDecoration还提供了一个combine函数,可以将下划线和中划线组合

val combineDecoration = listOf(TextDecoration.Underline, TextDecoration.LineThrough)
Text(
    text = stringResource(R.string.ping),
    textDecoration = TextDecoration.combine(combineDecoration)
)

overflow 文本内容超过可见范围时的处理
softWrap 是否换行
onTextLayout 文本布局完成的回调
style是把字体的属性写个一个style里,通过style配置通类型文本的样式\

TextField 文本输入框

TextField( value = "请输入", onValueChange = {  } )

Button

Button 按钮
IconButton 图标按钮
FloatingActionButton 悬浮按钮
IconToggleButton 图标切换按钮,向点赞,收藏等可以使用这个
RadioButton 单选按钮
TextButton 文本按钮

Image

Image(
    painterResource(R.drawable.ic_launcher_background),
    contentDescription = "Image"
)

// Bitmap
// 并非Android原生Bitmap,是Compose独立于平台的Bitmap
// Canvas也是如此
Image(ImageBitmap = , contentDescription = "")
// 矢量图
Image(imageVector = , contentDescription = "")

加载网络图片

// Coil 官方目前推荐的
// 支持kotlin特性(扩展函数、协程)
// implementation "com.google.accompanist:accompanist-coil:<version>"
CoilImage("https://***.jpg", contentDescription = "")

Layout

// FrameLayou
// 一层一层叠加
Box() {
    Text(text = "Text1")
    Text(text = "Text2")
    Text(text = "Text3")
}

// LinearLayout
// 纵向排列
Column() { 
    Text(text = "")
    Image(bitmap =, contentDescription =)
    CoilImage(data =, contentDescription =)
}

// 横向排列
Row() {
    Text(text = "")
    Image(bitmap =, contentDescription =)
    CoilImage(data =, contentDescription =)
}

//约束布局
ConstraintLayout {
    val (text) = createRefs()
    Text(
      text = "hello",
      modifier = Modifier
        .constrainAs(text) {
          top.linkTo(parent.top)
        }
        .padding(start = 16.dp, top = 12.dp)
)

RecyclerView

// 纵向
LazyColumn {
    //添加多个item,回调中没有数据的下标
    items(itemsList) {
        Text("第$it 个Item")
    }
    //添加单个item
    item {
        Text("单个 item")
    }
    //添加多个Item,可以获取到数据的下标
    itemsIndexed(itemsIndexedList) { index, item ->
        Text("第$index 个Item,value = $item")
    }
}

// 横向
LazyRow {
    items(listOf(1, 2, 3, 4, 5, 6)) { item ->
        Text(text = "item $item")
    }
}

状态订阅与自动更新

由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新。

@Composable
fun NoMutableStateDemo() {
    Column(modifier = Modifier.padding(16.dp)) {
            OutlinedTextField(
                value = "",
                onValueChange = { },
                placeholder = {
                    Text(text = "请输入手机号码")
                }
            )
        }
}

运行上面的代码,会发现键盘输入,TextField不会有任何变化

@Composable
fun NoMutableStateDemo() {
    Column(modifier = Modifier.padding(16.dp)) {
            OutlinedTextField(
                value = "",
                onValueChange = { },
                placeholder = {
                    Text(text = "请输入手机号码")
                }
            )
        }
}

组合函数可以使用 remember 可组合项记住单个对象。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。remember 既可用于存储可变对象,又可用于存储不可变对象

@Composable
fun MutableStateDemo() {
    val phone = remember{
        mutableStateOf("")
    }
    Column(modifier = Modifier.padding(16.dp)) {
            OutlinedTextField(
                value = phone.value,
                onValueChange = { },
                placeholder = {
                    Text(text = "请输入手机号码")
                }
            )
        }
}

运行上面的代码,会发现TextField可以正常输入了,由于使用remeber存储了一个可变对象phone,phone mutableStateOf 会创建可观察的 MutableState<T>,后者是与 Compose 运行时集成的可观察类型

interface MutableState<T> : State<T> {
    override var value: T
}

value 如有任何更改,系统会安排重组读取 value 的所有可组合函数
在可组合项中声明 MutableState 对象的方法有三种:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) } 虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveablerememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象

注意:在 Compose 中将可变对象(如 ArrayList<T> 或 mutableListOf())用作状态会导致用户在应用中看到不正确或陈旧的数据。
不可观察的可变对象(如 ArrayList<T> 或可变数据类)不能由 Compose 观察,因而 Compose 不能在它们发生变化时触发重组。
建议使用可观察的数据存储器(如 State<List<T>>)和不可变的 listOf(),而不是使用不可观察的可变对象

有状态与无状态

使用 remember 存储对象的可组合项会创建内部状态,使该可组合项有状态**。HelloContent 就是一个有状态可组合项的示例,因为它会在内部保持和修改自己的 name 状态。在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用。但是,具有内部状态的可组合项往往不易重复使用,也更难测试。

**无状态可组合项是指不保持任何状态的可组合项。实现无状态的一种简单方法是使用状态提升

在开发可重复使用的可组合项时,您通常想要同时提供同一可组合项的有状态和无状态版本。有状态版本对于不关心状态的调用方来说很方便,而无状态版本对于需要控制或提升状态的调用方来说是必要的。

状态提升

Compose 中的状态提升是一种将状态移至可组合项的调用方,以使可组合项无状态的模式。Jetpack Compose 中的常规状态提升模式是将状态变量替换为两个参数:

  • value: T:要显示的当前值
  • onValueChange: (T) -> Unit:请求更改值的事件,其中 T 是建议的新值

不过,并不局限于 onValueChange。如果更具体的事件适合可组合项,应使用 lambda 定义这些事件,就像使用 onExpand 和 onCollapse 定义适合 ExpandingCard 的事件一样。

以这种方式提升的状态具有一些重要的属性:

  • 单一可信来源:我们会通过移动状态而不是复制状态,来确保只有一个可信来源。这有助于避免 bug。
  • 封装:只有有状态可组合项能够修改其状态。这完全是内部的。
  • 可共享:可与多个可组合项共享提升的状态。如果想在另一个可组合项中执行 name 操作,可以通过变量提升来做到这一点。
  • 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
  • 解耦:无状态 ExpandingCard 的状态可以存储在任何位置。例如,现在可以将 name 移入 ViewModel
@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

上面的代码HelloContent 中提取 name 和 onValueChange,并按照可组合项的树结构将它们移至可调用 HelloContent 的 HelloScreen 可组合项中。

通过从 HelloContent 中提升出状态,更容易推断该可组合项、在不同的情况下重复使用它,以及进行测试。HelloContent 与状态的存储方式解耦。解耦意味着,如果修改或替换 HelloScreen,不必更改 HelloContent 的实现方式。

要点:提升状态时,有三条规则可帮助您弄清楚状态应去向何处:

  1. 状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项。**
  2. 状态应至少提升到它可以发生变化(写入)的最高级别。**
  3. 如果两种状态发生变化以响应相同的事件,它们应一起提升

可以将状态提升到高于这些规则要求的级别,但欠提升状态会使遵循单向数据流变得困难或不可能

在 Compose 中恢复状态

在重新创建 activity 或进程后,可以使用 恢复界面状态。rememberSaveable 可以在重组后保持状态。此外,rememberSaveable 也可以在重新创建 activity 和进程后保持状态。

存储状态的方式

添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择。

Parcelize

最简单的解决方案是向对象添加 @Parcelize注解。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

如果某种原因导致 @Parcelize 不合适,您可以使用 mapSaver 定义自己的规则,规定如何将对象转换为系统可保存到 Bundle 的一组值。

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

为了避免需要为映射定义键,您也可以使用 listSaver 并将其索引用作键:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Compose 和其他库

ViewModel

Compose使用viewModel 依赖 androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07 使用livedata 依赖 "androidx.compose.runtime:runtime-livedata:$compose_version"

@Composable
fun ViewModelDemo(viewModel: ViewModelSample = viewModel()) {
    val num by viewModel.num.observeAsState()
    Text(
        text = "增加1",
        modifier = Modifier
            .padding(bottom = 8.dp)
            .clickable {
                viewModel.increase()
            },
        style = MaterialTheme.typography.h5
    )

    num?.let {
        Content(it)
    }
}

@Composable
fun Content(num: Int) {
    Text(
        text = "数值:$num",
        modifier = Modifier.padding(top = 40.dp),
        style = MaterialTheme.typography.h5
    )
}
class ViewModelSample : ViewModel(){
    val num = MutableLiveData<Int>()
    private var count = 0
    fun increase() {
        count++
        num.value = count
    }
}

页面导航 Navigation

Navigation详细使用 developer.android.google.cn/guide/navig…

图片加载 coil

@Composable
fun NetworkImageDemo() {
    val imageUrl = "https://img2.baidu.com/it/u=3479666900,2123308694&fm=253&fmt=auto&app=138&f=JPEG?w=477&h=270"
    Image(
        painter = rememberImagePainter(
            data = imageUrl,
            builder = {
                transformations(RoundedCornersTransformation())
            }
        ),
        contentDescription = null,
        modifier = Modifier.size(128.dp)
    )
}

coil详细使用 coil-kt.github.io/coil/compos…