Android Compose 初探!
使用前的准备工作
android studio
Arctic Fox
版本或更新的版本如果是一个新项目,可以在创建的时候,新建一个
Empty Compose Activity
在module的
build.gradle
文件中添加android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.4.32'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
}
需要添加
buildFeatures {
compose true
}
组件
组件的定义
在Compose中一个UI组件就是一个带有@Composable
注解的函数
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
布局组件
如果没有采用布局组件,直接单视图写到一个Compose中,会存在异常的情况。官方是这么说的:
A Composable function might emit several UI elements. However, if you don't provide guidance on how they should be arranged, Compose might arrange the elements in a way you don't like
Row
横向排列视图, Row的相关属性如下:inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
Column
纵向排列视图, 其属性和上面的Row
类似Box
将一个元素覆盖在另一个上面, 类似于FrameLayout
这种
视图组件
Text
类似于原生View中的TextViewButton
按钮LazyColumn
类似于原生RecyclerViewImage
图片控件 关于网络图片,可以采用Coil
框架TextField
文件输入框Surface
用来控制组件的背景,边框,文本颜色等AlertDialog
弹窗控件,类似于原生View中的AlertDialog
组件的状态管理
remember
通过remember
来记录组件某些相关属性值,当属性发生变化,会自动触发UI的更新。
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var nameState = remember { mutableStateOf("") }
var name = nameState.value;
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
TextField(
value = name,
onValueChange = { println("data----->$it");nameState.value = it }
)
}
}
这段代码实现的功能就是当用户在一个输入框中输入文字的时候,即时回显在页面上。当采用这种方式编码时,状态是耦合在组件中,当调用者不关心内部的状态的,这种方式是ok的,但它的弊端就是不利于组件的复用。我们可以将状态和组件分离开,此时,便就是利用状态提升(state hoisting
)的手段
@Composable
fun HelloScreen() {
var nameState = remember { mutableStateOf("") }
HelloContent(name = nameState.value, onNameChange = { nameState.value = 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
)
TextField(
value = name,
onValueChange = { onNameChange(it) }
)
}
}
这里是将状态提到HelloContent
的外面, 方面HelloContent
组件的复用
rememberSaveable
和remember
类似,区别在于rememberSaveable
进行状态管理时,当activity或进程重新创建了(如屏幕旋转),其状态信息不会丢失。 将上面的var nameState = remember { mutableStateOf("") }
中的remember
换成rememberSaveable
就可以了
ViewModel
可以利用ViewModel
进行全局的状态管理
class HelloViewModel : ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData = _name
// onNameChange is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChange(newName: String) {
_name.value = newName
}
}
@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
// by default, viewModel() follows the Lifecycle as the Activity or Fragment
// that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.
// name is the current value of [helloViewModel.name]
// with an initial value of ""
val name: String by helloViewModel.name.observeAsState("")
HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(it) })
}
Modifers
Modifers是用来装饰composable, Modifiers用来告诉一个UI元素如何布局,显示,和相关的行为。
布局相关的属性
fillMaxWidth
matchParentSize
height
width
padding
size
显示
background
clip
: 如Modifier.clip(RoundedCornerShape(4.dp))
,一个圆角便出来了
绑定事件
利用clickable
来绑定事件
Row(
Modifier
.fillMaxWidth()
.clickable { onClick(); },
verticalAlignment = Alignment.CenterVertically
) {
...
}
实例
采用Compose方案的开发体验非常接近于用Vue或React, 代码结构非常清晰,不用xml来画UI确实省了不少事,以下是一段代码片断来画一个微信的个人中心页
@Preview(showBackground = true)
@Composable
fun PersonalCenter() {
Column() {
Header("Hello World", "Wechat_0001")
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowList()
Divider(
Modifier
.fillMaxHeight(), GrayBg
)
}
}
@Composable
fun Header(nickName: String, wechatNo: String) {
Row(
Modifier
.fillMaxWidth()
.padding(24.dp, 24.dp, 16.dp, 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "头像",
Modifier
.size(50.dp)
.clip(
RoundedCornerShape(4.dp)
)
)
Column() {
Text(nickName, Modifier.padding(12.dp, 2.dp, 0.dp, 0.dp), TextColor, fontSize = 18.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"微信号 :$wechatNo",
Modifier
.padding(12.dp, 10.dp, 0.dp, 0.dp)
.weight(1.0f), TextColorGray, fontSize = 14.sp
)
Icon(painterResource(R.drawable.ic_qrcode), "二维码", Modifier.size(16.dp))
Icon(
painterResource(R.drawable.right_arrow_3),
contentDescription = "more",
Modifier.padding(12.dp, 0.dp, 0.dp, 0.dp)
)
}
}
}
}
@Composable
fun RowItem(@DrawableRes icon: Int, title: String, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.clickable { onClick(); },
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(icon), contentDescription = title + "icon",
Modifier
.padding(16.dp, 12.dp, 16.dp, 12.dp)
.size(24.dp)
)
Text(title, Modifier.weight(1f), TextColor, fontSize = 15.sp)
Icon(
painterResource(R.drawable.right_arrow_3),
contentDescription = "more",
Modifier.padding(0.dp, 0.dp, 16.dp, 0.dp)
)
}
}
@Composable
fun RowList() {
var context = LocalContext.current;
Column() {
RowItem(icon = R.drawable.ic_pay, title = "支付") { onItemClick(context, "payment") }
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowItem(icon = R.drawable.ic_collections, title = "收藏") {
onItemClick(context, "收藏")
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_photos, title = "相册") {
onItemClick(context, "相册")
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_cards, title = "卡包") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_stickers, title = "表情") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowItem(icon = R.drawable.ic_settings, title = "设置") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
}
}
fun onItemClick(context: Context, data: String) {
Toast.makeText(context, data, Toast.LENGTH_SHORT).show()
}
View中嵌Compose
var view = LinearLayout(this)
view.addView(ComposeView(this).apply {
setContent {
PersonalCenter();
}
})
Compose中嵌View
@Compose
fun RowList() {
...
AndroidView({View(context)}, Modifier.width(20.dp).height(20.dp).background(Color.Green)){}
...
}
总结
- Compose使用了一套新的布局,渲染机制, 它里面的元素和我们以前写的各种View是有区别的,比如Compose里面的Text并不是我们以前认识的
TextView
或其它的原生控件, 它采用了更底层的api来实现 - 数据的自动订阅(完成双向绑定)
- 声明式UI: compose通过自动订阅机制来完成UI的自动更新
- compose和现有的原生View混用