Compose
今天的的小目标是将Compose的新生教程走完。hh
Compose编程思想
View体系和Compose在层级结构上没有任何的区别。
Compose最大也是最不同的一点就是他的编程思想。
View体系是命令式的,Compose是声明式的。
声明式编程规范
命令式正如他的名称一样,我们用代码去命令它发生改变。比如setText,setImageResource,addView等等。
声明式就是当需要改变状态的时候,不管他。对,就是不管他,它自动会发生改变,我只声明这个组件,声明后我不会再命令你去改变,这样更新界面的职责就好像是落到了界面自己身上,会便捷很多。
自动?这不就是响应式编程?观察者模式嘛,这我熟。
使用过LiveData都知道,通过observe的方法能很大程度上方便我们的开发,而且这玩意还能减小我们出bug的几率。主要就是我们写了界面以后对界面的状态不管不问,通过改变对应的livedata,View自动进行调整。这样好像也能达到命令式的效果。
但是Compose对其的支持更为强大。
@Composable
这是Compose的Hello World的写法
@Composable
fun Greeting(str:String){
Text(text =str)
}
其中有几个需要注意的
-
这个函数带有@Composable注解(这个注解是告诉编译器,此函数旨在将数据转换为界面)
-
@Composable标注的函数的首字母是大写的。
当然是规范啦,没得讲,Compose是个新东西,为了区分Compose函数和普通函数,写法上进行规范,提高可读性。
-
此函数接受数据。可组合函数可以接受一些参数,这些参数可让应用逻辑描述界面。在本例中,我们的Greeting接受了一个
String。 -
组合函数可以调用其他的组合函数,比如Greeting调用了组合函数Text
-
此函数不会返回任何内容。界面的 Compose 函数不需要返回任何内容。
声明式UI的转变
在许多面向对象的命令式界面工具包中,您可以通过实例化微件树来初始化界面。您通常通过膨胀 XML 布局文件来实现此目的。每个微件都维护自己的内部状态,并且提供 getter 和 setter 方法,允许应用逻辑与微件进行交互。
在 Compose 的声明性方法中,微件相对无状态,并且不提供 setter 或 getter 函数。
这使得向架构模式(如 ViewModel提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。
当用户与界面交互时,界面会发起 onClick 等事件。这些事件应通知应用逻辑,应用逻辑随后可以改变应用的状态。当状态发生变化时,系统会使用新数据再次调用可组合函数。这会导致重新绘制界面元素,此过程称为“重组”。
小结:
- 逻辑层发送一个状态给View,然后View自己去观察这个状态数据,更新界面。
- 当用户有点击事件的时候将事件发送到逻辑层,逻辑层处理以后更改数据,数据更改又会调用可组合函数,view重新绘制。
好像和MVVM差不了太多。
重组
在命令式界面模型中,如需更改某个微件,您可以在该微件上调用 setter 以更改其内部状态。在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。
例如,假设有以下可组合函数,它用于显示一个按钮:
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
每次点击该按钮时,调用方都会更新 clicks 的值。Compose 会再次调用 lambda 与 Text 函数以显示新值;此过程称为“重组”。不依赖于该值的其他函数不会进行重组。
(恐怖如斯。)也就是说原生就支持了局部刷新。这......
感觉重组就和重新刷新界面没啥两样的。界面元素的重新组合,嗯重组。
重组是指在输入更改时再次调用可组合函数的过程。
Note:下列3件事情是不能做的,由于重组过程的一些特性。
- Writing to a property of a shared object 更改共享的变量
- Updating an observable in
ViewModel更新ViewModel的可观察对象 - Updating shared preferences 更新sharedpreferences
关于可组合函数的其他注意事项
1.可组合函数可以是按照任何顺序执行
看看下面地代码我们第一地想法就是先绘制StartScreen,然后再MiddleScreen,EndScreen。
其实真实情况不是这样地。
对于他们的绘制顺序可以是任意的。
它可能是EndScreen然后MiddleScreen然后StartScreen,也可是...(主要的原因是Compose会为一些元素划分优先级,优先级高的会优先绘制,优先级低的就后绘制。)
所以不要在这个代码中更新一些共享的变量,容易发生一些不可预知的问题,其中每个函数都需要保持独立。
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
2.可组合函数可以并行执行
Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数(不在屏幕上)。
这种并行优化意味着线程不安全,如果我们修改viewmodel中的可观察对象,那就会出现奇奇怪怪的bug。
为了确保应用正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。
这里的附带效应也就是一些逻辑操作,可组合函数内部应该只有一些显示的逻辑,运算等逻辑是不应该的,因为并行线程不安全。
比如这样是可以的
van♂瑞固德,运行时没有问题的
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
这样是不行的
this code will not be thread-safe or correct
@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++这就出问题了,线程不安全。
3.可组合函数会尽力避开不需要重组的内容
/**
* 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) }))
}
这些作用域中的每一个都可能是在重组期间执行的唯一一个作用域。当 header 发生更改时,Compose 可能会跳至 Column lambda,而不执行它的任何父项。此外,执行 Column 时,如果 names 未更改,Compose 可能会选择跳过 LazyColumnItems。
同样,执行所有可组合函数或 lambda 都应该没有附带效应。当您需要执行附带效应时,应通过回调触发。
4.重组是乐观操作
只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。
取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组成操作,也会应用该附带效应。这可能会导致应用状态不一致。
确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
比如数据库的增,改是非幂等的,查询是幂等的。
5.可组合函数可以非常频繁地执行
在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。
比如我们进行设备的读取操作,如果放置在组合函数中他可能会在一秒内读取上百次,这会对性能造成恐怖的影响。
小结
@Composable标注的函数是一个比较特殊的函数。
- 它执行绘制的顺序是不定的。
- 它可能会被多线程调用。
- 当数据变动时它会刷新变动的地方,没发生变动的地方会保持不变。
- 重组时乐观操作
- 它可能会被频繁调用。
Compose basic Codelabe
搭建环境
这个就不说了,有一点需要注意
我们需要删除一下as的kotlinCompilerVersion
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.5.21'
}
composeOptions这样就够了
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
Compose组合函数的概念
也就是被@Composable标注的函数,这个函数是有一定限制的,重组那里已近给出了相关的描述。
Compose的声明式UI
Modifier
Modifier之前摸过,官方给出的定义是Modifier是进行布局操作的一个东西。我们可以通过Modifier参数设置宽高,shape,padding等布局属性。
Modifier parameters tell a UI element how to layout, display, or behave within its parent layout. Modifiers are regular Kotlin objects.
其他的好像就讲了一个Modifier.padding。离谱鸟
组合函数的放置
其实我们没必要把所有的Compose UI logic(Compose显示逻辑)放在MainActivity中。比如这样。
Activity莫名的就膨胀起来了。
package com.example.compose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.compose.ui.theme.ComposeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
//Greeting("Android")
//ManageLayout()
ComposeRecyclerView(messages = (0..100).map {
Message("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
})
}
}
}
}
}
//Part 8
@Preview
@Composable
fun Pre() {
ComposeRecyclerView(messages = (0..100).map {
Message("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
}
)
}
//Part 7
@Composable
fun ComposeRecyclerView(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
ManageLayout(message = message)
}
}
}
//Part 6
@Composable
//@Preview
fun ManageLayout(message: Message) {
Row(
modifier = Modifier.padding(all = 8.dp),
) {
Image(
painter = painterResource(id = R.drawable.img),
contentDescription = "这是一个小姐姐的图片",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(
width = 1.5.dp,
color = MaterialTheme.colors.secondary,
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(8.dp))
var isExpandable by remember {
mutableStateOf(false)
}
val surfaceColor by animateColorAsState(
if (isExpandable) MaterialTheme.colors.primary else MaterialTheme.colors.surface
)
Column(modifier = Modifier.clickable { isExpandable = !isExpandable }) {
Text(
text = message.str1,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)
Spacer(modifier = Modifier.height(8.dp))
Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
color = surfaceColor,
modifier = Modifier
.animateContentSize()
.padding(1.dp)) {
Text(
text = message.str2,
style = MaterialTheme.typography.body2,
maxLines = if (isExpandable) Int.MAX_VALUE else 1,
)
}
}
}
}
//Part 5
@Composable
@Preview
fun UseImage() {
val painter = painterResource(id = R.drawable.img)
Row {
Image(
painter = painter,
contentDescription = "这是一个小姐姐的照片"
)
Column {
Text(text = "不知道写啥了")
Text(text = "真的不知道写啥了")
}
}
}
//Part 4
@Preview
@Composable
fun UseColumn() {
Column(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.Start,
) {
Text(text = "Hello")
Text(text = "World")
}
}
//Part 3
data class Message(
val str1: String,
val str2: String,
)
@Preview
@Composable
fun MessagePreview() {
MessageCard(message = Message("Hello", "World"))
}
@Composable
fun MessageCard(message: Message) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = message.str1)
Text(text = message.str2)
}
}
//Part 2
@Preview(name = "第一个",
widthDp = 100,
heightDp = 50,
showBackground = true,
backgroundColor = 0x12ffff,
showSystemUi = true,
locale = "fr-rFR")
@Composable
fun Greeting(name: String = "World") {
val resource = painterResource(id = R.drawable.abc_vector_test)
Row() {
Text(text = "Hello $name!")
Image(
painter = resource,
contentDescription = "Test Logo",
)
}
}
// Part 1
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeTheme {
Greeting("Android")
}
}
我们其实可以把Compose函数直接放在其他的文件中,然后activity内部调用就可以了,就像这样
package com.example.compose
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.compose.ui.theme.ComposeTheme
/**
*@author ZhiQiang Tu
*@time 2021/8/21 15:37
*@signature 我们不明前路,却已在路上
*/
//Part 8
@Preview
@Composable
fun Pre() {
ComposeRecyclerView(messages = (0..100).map {
Message("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
}
)
}
//Part 7
@Composable
fun ComposeRecyclerView(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
ManageLayout(message = message)
}
}
}
//Part 6
@Composable
//@Preview
fun ManageLayout(message: Message) {
Row(
modifier = Modifier.padding(all = 8.dp),
) {
Image(
painter = painterResource(id = R.drawable.img),
contentDescription = "这是一个小姐姐的图片",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(
width = 1.5.dp,
color = MaterialTheme.colors.secondary,
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(8.dp))
var isExpandable by remember {
mutableStateOf(false)
}
val surfaceColor by animateColorAsState(
if (isExpandable) MaterialTheme.colors.primary else MaterialTheme.colors.surface
)
Column(modifier = Modifier.clickable { isExpandable = !isExpandable }) {
Text(
text = message.str1,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)
Spacer(modifier = Modifier.height(8.dp))
Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
color = surfaceColor,
modifier = Modifier
.animateContentSize()
.padding(1.dp)) {
Text(
text = message.str2,
style = MaterialTheme.typography.body2,
maxLines = if (isExpandable) Int.MAX_VALUE else 1,
)
}
}
}
}
//Part 5
@Composable
@Preview
fun UseImage() {
val painter = painterResource(id = R.drawable.img)
Row {
Image(
painter = painter,
contentDescription = "这是一个小姐姐的照片"
)
Column {
Text(text = "不知道写啥了")
Text(text = "真的不知道写啥了")
}
}
}
//Part 4
@Preview
@Composable
fun UseColumn() {
Column(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.Start,
) {
Text(text = "Hello")
Text(text = "World")
}
}
//Part 3
data class Message(
val str1: String,
val str2: String,
)
@Preview
@Composable
fun MessagePreview() {
MessageCard(message = Message("Hello", "World"))
}
@Composable
fun MessageCard(message: Message) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = message.str1)
Text(text = message.str2)
}
}
//Part 2
@Preview(name = "第一个",
widthDp = 100,
heightDp = 50,
showBackground = true,
backgroundColor = 0x12ffff,
showSystemUi = true,
locale = "fr-rFR")
@Composable
fun Greeting(name: String = "World") {
val resource = painterResource(id = R.drawable.abc_vector_test)
Row() {
Text(text = "Hello $name!")
Image(
painter = resource,
contentDescription = "Test Logo",
)
}
}
// Part 1
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeTheme {
Greeting("Android")
}
}
将组合函数作为函数参数传入
content: @Composable () -> Unit表示我们传入一个Composable的高阶函数,跟协程传入一个suspend函数是一致的。
@Composable
fun PassComposeContent(content: @Composable () -> Unit) {
ComposeTheme {
content()
}
}