Jetpack Composed
Jetpack Composed创建Jetpack Compose项目基础知识可组合函数预览功能背景颜色设置重复使用可组合项布局Compose状态状态提升列表保留状态添加动画效果设置应用的样式和主题
用于构建原生Android界面的新工具包。可以简化并加快Android上的界面开发,帮助使用更少的代码、强大的工具和直观的Kotlin API,快速打造生动而精彩的应用,使用了声明式编程范式,在前端主流框架中【如:Vue、React】声明式界面设计普遍存在,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计
优势
-
更少的代码
- 使用更少的代码实现更多的功能,并且可以避免各种bug,从而使得代码简洁且易于维护
-
直观
- 只需描述界面,Compose会负责处理剩余的工作。应用状态变化时,界面会自动更新
-
加快应用开发
- 兼容现有的所有代码,方便随时随地采用。借助实时预览和全面的Android Studio支持,实现快速迭代
-
功能强大
- 凭借对Android平台API的直接访问和对于Material Design、深色主题、动画等的内置支持,创建精美的应用
声明性编程范式
长期以来,Android 视图层次结构一直可以表示为界面控件树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById()
等函数遍历树,并通过调用button.setText(String)
、container.addChild(View)
或 img.setImageBitmap(Bitmap)
等方法更改节点。这些方法会改变控件的内部状态。
手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以意外的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护复杂性会随着需要更新的视图数量而增长。
在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。
重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减轻这一成本,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。
创建Jetpack Compose项目
在使用Jetpack Composed
时需要更新AndroidStudio
,此处学习使用版本如下:
Android Studio Bumblebee | 2021.1.1 Patch 2
新建项目选择Empty Compose Activty
build文件差异
project build.gradle
buildscript {
ext {
//规定Jetpack Compose版本
compose_version = '1.0.1'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.2' apply false
id 'com.android.library' version '7.1.2' apply false
id 'org.jetbrains.kotlin.android' version '1.5.21' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
与普通的
kotlin
应用项目的build.gradle
文件相比就增加:buildscript { ext { compose_version = '1.0.1' } } ... task clean(type: Delete) { delete rootProject.buildDir }
app build.gradle
文件
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
...
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
//默认添加的依赖
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
从添加的依赖文件中可以看出
jetpack compose
中内置了协程
基础知识
可组合函数
Jetpack Compose是围绕可组合函数构建的。这些函数可以让您以程序化方式定义应用的界面,只需描述应用界面外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素,将其附加到父项等)
可组合函数是带有 @Composable
注解的常规函数。这类函数自身可以调用其他 @Composable
函数
@Composable
private fun Greeting(name: String) {
Text(text = "Hello $name!")
}
其中
Text
是由库提供的可组合函数@Composable fun Text( ... ){...}
在使用 Compose 时,Activities
仍然是 Android 应用的入口点,通过setContent()
定义布局,但与传统使用xml文件不同,我们可以在该函数中调用可组合函数
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = "Fucking World")
}
}
}
在此项目中导入与
Jetpack Compose
相关的类时,请从以下位置导入:
- androidx.compose.*(针对编译器和运行时类)
- androidx.compose.ui.*(针对界面工具包和库)
即
Jetpack Compose
不支持android.support.*
预览功能
若要使用Android Studio
预览,通过 @Preview
注解标记所有无参数可组合函数或采用默认参数的函数
@Composable
fun Greeting(name: String) {
Text(text = "Fucking $name!")
}
@Preview
@Composable
fun NoParamPreview() {
Greeting("World")
}
@Preview
@Composable
fun DefaultParamPreview(name:String="World") {
Greeting(name)
}
@Preview
注解还有很多参数:具体的可以看源码中的注释,在此处标注出常用的参数及其作用
@MustBeDocumented @Retention(AnnotationRetention.SOURCE) @Target( AnnotationTarget.FUNCTION ) @Repeatable annotation class Preview( //预览的显示名称,没有指定就是方法名 val name: String = "", //组名,进行分组时使用做到仅显示其中一个或多个 val group: String = "", //渲染带注释的@Composable 时使用的 API 级别 @IntRange(from = 1) val apiLevel: Int = -1, //设置渲染窗口的宽度和高度 val widthDp: Int = -1, val heightDp: Int = -1, val locale: String = "", @FloatRange(from = 0.01) val fontScale: Float = 1f, //是否显示设备的状态栏和操作栏 val showSystemUi: Boolean = false, //是否显示背景 val showBackground: Boolean = false, //背景颜色,若showBackground为false则不生效 val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT )
背景颜色设置
如果只能干巴巴的使用Android原生控件,UI界面绝对不会好看。那么如何对控件进行修饰呢?此处以背景颜色设置举例
1.通过Modifier
修饰符修改
大多数 Compose 界面元素(例如 Surface
和 Text
)都接受可选的 modifier
参数。修饰符会指示界面元素如何在其父级布局中放置、显示或表现
@Composable
fun GreetingWithModify(name: String = "World") {
Text(
text = "Fucking $name!",
modifier = Modifier
.background(Purple200)
.padding(16.dp)
)
}
相当于xml文件中修改
background
值,只不过是通过Modifier
进行设置
Modifier
中可以设置控件的通用属性,如:padding、background、width、height等
2.通过布局设置颜色
@Composable
fun GreetingWithSurface(name:String="World") {
Surface(color = Purple200) {
Text(text = "Fucking $name!")
}
}
这种方式相当于父控件设置背景颜色,若子控件没有设置背景颜色就会延用父控件的背景颜色
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/purple_200"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Fucking World!" /> </FrameLayout>
其中
Surface
是Material Design
相关概念,会在之后介绍。通过查看源码可以知道Surface
也是通过Modifier
设置背景颜色的Purple200是项目创建时默认生成的值,位置为
包名.ui.theme.Color.kt
重复使用可组合项
添加到界面的组件越多,创建的嵌套层级就越多。如果函数变得非常大,可能会影响可读性。通过创建可重用的小型组件,可以轻松构建应用中所用界面元素的库。每个组件对应于屏幕的一个部分,可以单独修改
布局
既然我们知道如何去创建一个控件,那么如何将控件有序的摆放呢?就像是 xml 中的Linear Layout
一样
在 Compose 中有三个基本标准布局元素: Column
、Row
和 Box
可组合项
Column
可以视为android:orientation="vertical"
的LinearLayout
Row
可以视为android:orientation="horizontal"
的LinearLayout
Box
可以视为FrameLayout
,对控件进行重叠摆放
例:
@Preview(group = "1.3", widthDp = 320, showBackground = true, backgroundColor = 0xFFFFFF)
@Composable
fun ColumnPreview() {
ColumnGreeting()
}
@Composable
fun ColumnGreeting() {
Column(
modifier = Modifier
//相当于外边距,margin
.padding(12.dp)
.background(Teal200)
.fillMaxWidth()
//相当于内边距,padding
.padding(12.dp)
) {
listOf("world", "World", "WORLD").forEach {
RowGreetingContent(it)
}
}
}
@Composable
fun RowGreetingContent(name: String) {
Row(modifier = Modifier.background(Purple200)) {
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Text(text = "Hello", modifier = Modifier)
Text(text = name, modifier = Modifier)
}
Button(
onClick = { /*TODO*/ },
modifier = Modifier.weight(1f)
) {
Text(text = "Button")
}
}
}
Modifier
中没有margin
属性,margin
效果的实现就需要通过padding
的执行顺序实现
Compose状态
之前实现的控件都是静态的,无法响应事件与用户进行交互。那么该如何让控件响应时间呢?比如实现以下效果:
[图片上传失败...(image-45f5c8-1652016292431)]
很显然在响应事件前,我们应该设置变量用来记录事件状态类型
@Composable
fun ColumnStateContent(name: String) {
//错误
var isExpand = false
Row(
modifier = Modifier
.padding(4.dp)
.background(Purple200)
.padding(16.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Fucking,")
Text(text = name)
}
OutlinedButton(onClick = { isExpand = !isExpand }) {
Text(text = if (isExpand) "show less" else "show more")
}
}
}
然而这种方式无法按预期发挥作用,为 isExpand
变量设置不同的值不会使 Compose 将其检测为状态更改,因此不会产生任何效果。
更改此变量不会触发重组的原因是 Compose 并未跟踪此更改。此外,每次调用 ColumnStateContent
时,都会将该变量重置为 false。
如需向可组合项添加内部状态,您可以使用 mutableStateOf
函数,该函数可让 Compose 重组读取该 State
的函数。
State
和MutableState
是两个接口,它们具有特定的值,每当该值发生变化时,它们就会触发界面更新(重组)
但是,不能将mutableStateOf
分配给可组合项中的某个变量,因为重组可能会随时发生,会再次调用可组合项函数ColumnStateContent
,会导致isExpand
重置为值为 false
的新可变状态
@Composable
fun ColumnStateContent(name:String) {
//错误
var isExpand = mutableStateOf(false)
...
}
如需在重组后保留状态,请使用 remember
记住可变状态,然后添加一个依赖于状态的额外变量用于内边距距离修改
@Composable
fun ColumnStateContent(name: String) {
val isExpand = remember { mutableStateOf(false) }
val paddingValue = if (isExpand.value) 48.dp else 0.dp
Row(
modifier = Modifier
.padding(4.dp)
.background(Purple200)
.padding(16.dp)
) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = paddingValue)
) {
Text(text = "Fucking,")
Text(text = name)
}
OutlinedButton(onClick = { isExpand.value = !isExpand.value }) {
Text(text = if (isExpand.value) "show less" else "show more")
}
}
}
使用
remember
记录extraPadding
,因为该设置依赖于状态,只需执行一个简单的计算即可通过状态的作用方式,可以看除Compose的重组会重新调用需要修改的
Composable函数
状态提升
在可组合函数中,被多个函数读取或修改的状态应位于共同父实体中,此过程称为状态提升
状态提升,可以避免复制状态和引入bug,有助于重复使用可组合项,并大大降低可组合项的测试难度。相反,不需要由可组合项的父级控制的状态则不应该被提升。可信来源属于该状态的创建者和控制者
例:
通过按钮切换显示的界面
@Composable
fun StateHoistScreen() {
var isShow by remember { mutableStateOf(true) }
Surface {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = { isShow = false }
) {
Text("Continue")
}
}
}
}
isShow
使用的是by
关键字,而不是=
。使用了委托可以无需每次都输入.value
来获取值
但是在父实体中无法获取isShow
状态
@Composable
fun MyApp() {
//编译器:干嘛啊,小老弟,没这玩意!
if (isShow) {
StateHoistScreen()
}else{
ColumnState()
}
}
此时就需要提升状态
,将isShow
状态移至需要访问它的共同父实体中,在本案例中一共有两个可组合函数需要该状态【MyApp
和StateHoistScreen
】,其共同父实体就是MyApp
,所以将isShow
状态提升到MyApp
中
同时我们需要和StateHoistScreen
共享isShow
状态
1.通过传递MutableState
实现
@Composable
fun StateHoistScreen(isShow:MutableState<Boolean>) {
Surface {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = {
isShow.value=false
}
) {
Text("Continue")
}
}
}
}
@Composable
fun MyApp() {
val isShow = remember { mutableStateOf(true) }
if (isShow.value) {
StateHoistScreen (isShow)
} else {
ColumnState()
}
}
不推荐这种写法,因为这会导致业务逻辑在各个可组合函数中分布,代码逻辑混乱不利于维护。同时也将业务固定,不利于可组合函数的可重用性
2.通过向下传递回调
@Composable
fun StateHoistScreen(onClick: () -> Unit) {
Surface {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = onClick
) {
Text("Continue")
}
}
}
}
@Composable
fun MyApp() {
var isShow by remember { mutableStateOf(true) }
if (isShow) {
StateHoistScreen {
isShow = false
}
} else {
ColumnState()
}
}
推荐向下传递回调方式,可以提高该可组合项的可重用性,并防止状态被其他可组合项更改
这种方式的使用类似于
onClickListener
回调
列表
Android应用中列表的使用非常常见,那么对于多条数据Compse
如何实现的呢?
@Composable
fun PluralGreeting() {
Column {
(0..1000).forEach {
Greeting(name = "$it")
}
}
}
这种方式很显然不可取,即使屏幕上放不下这些问候语,仍会去加载这些控件导致应用启动很慢,甚至在模拟器中直接卡死。并且这种方式创建的列表不能进行滚动
如何让列表进行滚动呢?可以通过Modifier.verticalScroll
设置滚动状态
@Composable
fun PluralGreeting() {
val rememberScrollState = rememberScrollState()
Column(modifier = Modifier.verticalScroll(rememberScrollState)) {
(0..1000).forEach {
Greeting(name = "$it")
}
}
}
但是加载控件速度慢的缺点还是没有解决
为了显示可滚动列和加载速度考虑,Compose
提供了LazyColumn
。LazyColumn
只会渲染屏幕上可见的内容,从而在渲染大型列表时提升效率
LazyColumn
和LazyRow
相当于 Android View 中的RecyclerView
LazyColumn
API 会在其作用域内提供一个 items
元素,并在该元素中编写各项内容的渲染逻辑:
@Composable
fun LazyColumnPluralGreeting() {
LazyColumn{
items(items = (0..1000).toList()) { index ->
Greeting(name = "$index")
}
}
}
LazyColumn
不会像RecyclerView
一样回收其子级。它会在您滚动它时发出新的可组合项,并保持高效运行,因为与实例化 Android Views 相比,发出可组合项的成本相对较低
保留状态
运行应用,旋转屏幕,更改为深色模式等情况会重启整个Activity
,而remember
函数仅在可组合项包含在组合中时起作用,重启Activity
的情况会导致所有状态丢失。
我们可以使用 rememberSaveable
,而不使用 remember
。这会保存每个在配置更改(如旋转)和进程终止后保留下来的状态。
添加动画效果
在 Compose 中,有多种方式可以为界面添加动画效果:从用于添加简单动画的高阶 API 到用于实现完全控制和复杂过渡的低阶方法,具体的动画会在之后进行介绍
使用 animateDpAsState
可组合项。该可组合项会返回一个 State 对象,该对象的 value
会被动画持续更新,直到动画播放完毕。该可组合项需要一个类型为 Dp
的“目标值”
@Composable
fun MyApp() {
LazyColumn {
items(items = (0..1000).toList()) { index ->
AnimationGreeting(name = "$index")
}
}
}
@Composable
fun AnimationGreeting(name: String) {
var isExpand by remember { mutableStateOf(false) }
//animateDpAsState返回State<DP>类型
val paddingValue by animateDpAsState(if (isExpand) 48.dp else 0.dp)
Row(
modifier = Modifier
.padding(4.dp)
.background(Purple200)
.padding(16.dp)
) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = paddingValue)
) {
Text(text = "Fucking,")
Text(text = name)
}
OutlinedButton(onClick = { isExpand = !isExpand }) {
Text(text = if (isExpand) "show less" else "show more")
}
}
}
如果您展开第 1 项内容,然后滚动到第一项内容离开屏幕,再返回到第 1 项内容,您会发现第 1 项内容已恢复为原始尺寸。如果需要,您可以使用
rememberSaveable
保存此数据
设置应用的样式和主题
如果您打开 ui/Theme.kt
文件,您会看到 [应用名]Theme
在其实现中使用了 MaterialTheme
:
@Composable
fun StudyOfJetpackComposeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
MaterialTheme
是一个可组合函数,体现了 Material Design 规范中的样式设置原则。样式设置信息会逐级向下传递到位于其 content
内的组件,这些组件会读取该信息来设置自身的样式。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
StudyOfJetpackComposeTheme{
AnimationPreview()
}
}
}
}
由于 [应用名]Theme
将 MaterialTheme
包围在其内部,因此 MyApp
会使用该主题中定义的属性来设置样式。从任何后代可组合项中都可以检索 MaterialTheme
的三个属性:colors
、typography
和 shapes
@Composable
fun ThemeText() {
StudyOfJetpackComposeTheme {
Text(
text = "Fucking World",
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onSurface
)
}
}
上例中的 Text
可组合项会设置新的 TextStyle
。您可以创建自己的 TextStyle
,也可以使用 MaterialTheme.typography
检索主题定义的样式(首选)。此结构支持您访问 Material 定义的文本样式,例如 h1
-h6
、body1,body2
、caption
、subtitle1
等
通常来说,最好是将颜色、形状和字体样式放在
MaterialTheme
中。例如,如果对颜色进行硬编码,将会很难实现深色模式,并且需要进行大量修正工作,而这很容易造成错误。
Compose 还可以基于现有的颜色或样式进行设置,通过copy
函数修改预定义的样式,只会修改传入的参数类型的值,其余值不变
@Composable
fun ThemeText() {
Text(
text = "Fucking World",
style = MaterialTheme.typography.h4.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colors.onSurface.copy(alpha = .2f)
)
}
除此之外在预览中可以设置显示的模式
@Preview(
showBackground = true,
widthDp = 320,
//设置为深色主题
uiMode = UI_MODE_NIGHT_YES
)
@Composable
fun DefaultPreview() {
StudyOfJetpackComposeTheme {
ThemeText()
}
}