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
)
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可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用rememberSaveable。rememberSaveable会自动保存可保存在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 的实现方式。
要点:提升状态时,有三条规则可帮助您弄清楚状态应去向何处:
- 状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项。**
- 状态应至少提升到它可以发生变化(写入)的最高级别。**
- 如果两种状态发生变化以响应相同的事件,它们应一起提升。
可以将状态提升到高于这些规则要求的级别,但欠提升状态会使遵循单向数据流变得困难或不可能
在 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…