Android Jetpack Compose快速上手

2,699

一、Jetpack Compose简介

Jetpack Compose是Google推出的一个用于构建原生Android 界面的工具包,旨在帮助开发者更快、更轻松地在Android 平台上构建原生客户端应用。同时,作为全新的声明式的UI框架,Jetpack Compose可以使用声明式Kotlin API取代Android 传统的xml布局。

 

那什么是声明式呢?要搞清楚这个问题,我们需要布局开发中的另外一个概念:命令式。事实上,传统的使用xml布局方式就是命令式。在传统的命令式开发流程中,我们首先需要使用xml来创建布局,然后再通过findViewById方法获取控件,最后再绑定数据。而在声明式开发中,我们可以直接调用compose的库组件进行渲染,比如:

@Composable
fun ShowText(content: String){
    Text(text = content)
}

事实上,除了Jetpack Compose,Flutter、React Native和Swift-UI 等框架都是声明式的,可以说,前端的大部分的页面渲染都可以使用声明式来完成。

 

二、快速上手

2.1 环境搭建

工欲善其事,必先利其器。目前,Android Studio对Jetpack Compose 已经有了很好的支持,我们只需要下载最新版的Android Studio即可。

  image.png

安装完成之后,我们可以下载托管在github上的 Jetpack Compose 示例应用来体验Jetpack Compose的魅力。

 

2.2 创建Jetpack Compose应用

为了帮助开发者快速地上手Jetpack Compose,Android Studio提供了支持Jetpack Compose 的新项目模板。我们只需要打开 Android Studio,然后在菜单栏中依次选择 【File】->【New】->【New Project】 ->【Empty Compose Activity】。

image.png

然后,点击【Next】按钮,填写 Name、Package name 和 Save location等参数即可完成Jetpack Compose项目的创建。工程创建完成之后,Jetpack Compose项目会默认添加如下一些依赖。

dependencies {
    implementation("androidx.compose.ui:ui:1.2.1")
    // Tooling support (Previews, etc.)
    implementation("androidx.compose.ui:ui-tooling:1.2.1")
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation("androidx.compose.foundation:foundation:1.2.1")
    // Material Design
    implementation("androidx.compose.material:material:1.2.1")
    // Material design icons
    implementation("androidx.compose.material:material-icons-core:1.2.1")
    implementation("androidx.compose.material:material-icons-extended:1.2.1")
    // Integration with observables
    implementation("androidx.compose.runtime:runtime-livedata:1.2.1")
    implementation("androidx.compose.runtime:runtime-rxjava2:1.2.1")
    // UI Tests
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
}

 

2.3 原有项目添加Jetpack Compose 

当然,使用Android Studio提供的Jetpack Compose 模板来创建项目是最简单的,如果对于老的Android项目,我们需要怎么处理呢?

 

首先,打开项目的build.gralde文件,确保Kotlin的版本是1.4.30以上的版本。

plugins {
    ...
    id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
}

然后,打开app/build.gralde文件,添加或修改如下一些配置:

android {


    // 1、确保最低sdk版本为21或者更高 
    defaultConfig {
        ...
        minSdkVersion 21
    }
   
    buildFeatures {
        // 2、开启 jetpack compose 支持        
        compose true
    }
    ...


    // 3、设置java 和 kotlin 编译器版本为java8或者更高的版本
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }


    // 4、添加kotlin编译器扩展版本
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.0'
    }
}

然后,添加Jetpack Compose 开发需要的一些依赖库。

dependencies {
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.5.1'
    // Compose Material Design
    implementation 'androidx.compose.material:material:1.2.1'
    // Animations
    implementation 'androidx.compose.animation:animation:1.2.1'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.2.1'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.2.1'
}

 

2.4 迁移到 Compose

对于传统的xml布局,我们如何将其迁移到 Jetpack Compose。加入,我们在 XML 布局中有如下一段代码。

<...>
    <!-- Other content -->
    <TextView
        android:id="@+id/greeting"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/margin_small"
        android:layout_marginEnd="@dimen/margin_small"
        android:gravity="center_horizontal"
        android:text="@string/greeting"
        android:textAppearance="?attr/textAppearanceHeadline5"
        ... />
</...>

为了将其迁移到 Compose,我们可以将 TextView 替换为保留了相同布局参数和 id 的 ComposeView,如下所示。

<...>
    <!-- Other content -->
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/greeting"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</...>

然后,在使用了该XML布局的Activity或Fragment中获取ComposeView,然后调用setContent方法并向其中添加Compose内容。

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        val greeting = findViewById<ComposeView>(R.id.greeting)
        greeting.setContent {
            MdcTheme { 
                Greeting()
            }
        }
    }
}


@Composable
private fun Greeting() {
    Text(
        text = stringResource(R.string.greeting),
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

 

三、Compose工具

事实上,为了支持Jetpack Compose 的快速开发,Android Studio引入了许多专用于Jetpack Compose 的新功能。它支持使用代码优先方法,同时提高了开发者的工作效率,因为开发者不必在设计界面或代码编辑器之间二选一。

 

而基于View的界面与Jetpack Compose之间的一个基本区别在于,Compose不依赖View来呈现其可组合项。同时,Android Studio 为Jetpack Compose提供了扩展功能,使其不必像 Android View 一样打开模拟器或连接到设备即可预览,从而加快了开发者实现其界面设计的迭代过程。

 

同时,为了实现如需为 Jetpack Compose 的预览功能,需要我们在应用 build.gradle 文件中添加以下依赖项:

debugImplementation "androidx.compose.ui:ui-tooling:1.2.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.2.1"

3.1 预览模式

3.1.1 可组合项预览

首先,我们打开新建的Jetpack Compose项目,MainActivity.kt的代码如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeDemosTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeDemosTheme {
        Greeting("Android")
    }
}

在上面的代码中,setContent()不再是以前的传递一个View或者是layout,而是一个组合函数,也就是一个Compose组件。而带有@Composable的Kotlin函数就是就是一个Compose组件,一般为了跟普通函数区分,因此上面的Greeting和DefaultPreview 都是一个Compose组件。  

最后,点击拆分(设计/代码)视图,打开显示预览的右侧面板就可以看到效果。

image.png @Preview接受参数来支持Android Studio呈现的方式,我们可以在代码中手动添加这些参数,也可以点击 @Preview 旁边的边线图标来显示配置选择器,以便选择和更改这些配置参数。

除此之外,Android Studio 提供了一些功能来扩展可组合项预览。我们可以通过读取 LocalInspectionMode CompositionLocal来确认可组合项是否正在预览中呈现。如果可组合项在预览中呈现,LocalInspectionMode.current 的结果为 true。

if (LocalInspectionMode.current) {
    //呈现在预览界面
    Text("Hello preview user!")
} else {
    //在App中呈现
    Text("Hello $name!")
}

3.1.2 互动模式

互动模式,可以实现在设备上互动的方式和预览互动,互动模式被隔离在沙盒环境中,在该模式下,我们可以在预览中点击元素并输入用户输入;预览甚至播放动画。预览互动模式可以直接在Android Studio中运行,由于并未运行模拟器,所以存在一些使用限制:

  • 无法访问网络
  • 无法访问文件
  • 有些 Context API 不一定完全可用

 

使用互动模式时,只需要编写好代码,然后点击【Start interactive mode】开启,如下图。

image.png

tooling-interactive-preview-demo.gif  

3.1.3 部署预览

我们可以使用 @Preview 来部署应用到模拟器或实体设备上。点击 @Preview 注解旁边或预览顶部的 Deploy to Device 图标 ,Android Studio 会将该 @Preview 部署到连接的设备或模拟器上。

image.png

tooling-deploy-preview-demo.gif

3.1.4 Multipreview

使用 Multipreview注解时,我们可以定义一个注解类,该类本身可配置多个采用不同配置的 Preview注解。将此注解添加到一个可组合函数后,系统会自动同时呈现所有不同的预览。例如,我们可以定义一个同时支持预览多个设备、字体大小或主题的注解类。

 

比如,我们首先定义一个可以改变字体的FontScalePreviews类。

@Preview(
    name = "small font",
    group = "font scales",
    fontScale = 0.5f
)
@Preview(
    name = "large font",
    group = "font scales",
    fontScale = 1.5f
)
annotation class FontScalePreviews

然后,在字体中使用这个预览可组合项使用此自定义注解。

@FontScalePreviews
@Composable
fun HelloWorldPreview() {
    Text("Hello World")
}

最终的效果如下图。

image.png

当然,我们也可以将多个 MultiPreview 注解和普通 preview 注解结合进行使用,从而创建一个更完整的预览集。结合使用 MultiPreview 注解并不意味着所有不同的组合都会得以呈现。实际上,每个 MultiPreview 注解会独立运行,并且仅会呈现自己的变体。

@Preview(
    name = "dark theme",
    group = "themes",
    uiMode = UI_MODE_NIGHT_YES
)
@FontScalePreviews
@DevicePreviews
annotation class CombinedPreviews
@CombinedPreviews
@Composable
fun HelloWorldPreview() {
    MyTheme { Surface { Text("Hello world") } }
}

image.png

通过右键点击呈现的每个预览,即可将其作为图像来复制。

image.png

默认情况下,我们的可组合项是以透明背景来显示的。如果需要添加背景,那么需要用到showBackground 和 backgroundColor 两个参数,比如:

@Preview(showBackground = true, backgroundColor = 0xFF00FF00)
@Composable
fun WithGreenBackground() {
    Text("Hello World")
}

如需手动设置尺寸,可以添加 heightDp 和 widthDp 参数。

@Preview(widthDp = 50, heightDp = 50)
@Composable
fun SquareComposablePreview() {
    Box(Modifier.background(Color.Yellow)) {
        Text("Hello World")
    }
}

有时候,我们需要对状态栏和操作栏进行一些修改,那么可以使用showSystemUi参数。

@Preview(showSystemUi = true)
@Composable
fun DecoratedComposablePreview() {
    Text("Hello World")
}

当然,我们也可以使用@PreviewParameter注解来添加参数,用来将示例数据传递给某个可组合项预览函数。

@Preview
@Composable
fun UserProfilePreview(
    @PreviewParameter(UserPreviewParameterProvider::class) user: User
) {
    UserProfile(user)
}

然后,创建一个可实现 PreviewParameterProvider 并以序列形式返回示例数据的类。

class UserPreviewParameterProvider : PreviewParameterProvider<User> {
    override val values = sequenceOf(
        User("Elise"),
        User("Frank"),
        User("Julia")
    )
}

接着,序列中的每个数据元素都会呈现一个预览。

image.png

当需要为多个预览使用相同的提供程序类时。如有必要,可通过设置 limit 参数来限制呈现的预览数量,比如。

@Preview
@Composable
fun UserProfilePreview(
    @PreviewParameter(UserPreviewParameterProvider::class, limit = 2) user: User
) {
    UserProfile(user)
}

3.2 编辑器

为了提高使用 Jetpack Compose 时的工作效率,Android Studio在编辑器区域提供了一些功能,如实时模板、边线图标、颜色选择器等。

 

3.2.1 实时模板

Android Studio 提供了Compose 相关的实时模板,开发者可以通过输入相应的模板缩写来输入代码段,以实现快速插入。

  • comp:用于设置 @Composable 函数
  • prev:用于创建 @Preview 可组合函数
  • paddp:用于以 dp 为单位添加 padding 修饰符
  • weight:用于添加 weight 修饰符
  • W、WR、WC:用于通过 Box、Row 或 Column 容器设置当前可组合项的呈现效果

 

3.2.2 边线图标

边线图标是边栏中可见的上下文操作,位于行号旁边。Android Studio 引入了多个 Jetpack Compose 专用边线图标,以便开发者更轻松地使用。比如,可以直接通过边线图标将 @Preview 部署到模拟器或实体设备上。

tooling-preview-deploy-gutter-icon.gif  

而对于颜色选择器来说,我们可以点击颜色,然后更改选中的眼神,如下所示。

image.png

为了方便对图像进行选择,Android Studio也支持图形选择器,可以通过图像资源选择器更改选择图片,如下所示:

tooling-resource-picker.gif

3.3 迭代开发

作为移动开发者,移动应用界面开发并不是一次性开发完所有的内容的。Android Studio 通过提供不需要完整 build 即可检查、修改值和验证最终结果的工具,支持使用 Jetpack Compose 进行逐步开发。

3.3.1 实时修改字面量

Android Studio 可以实时更新在预览、模拟器和实体设备中的可组合项中使用的一些常量字面量,如Int、String、Color、Dp、Boolean等。

tooling-live-literals-demo.gif

 

通过“Live Edit of Literals”界面指示器启用字面量修饰功能,无需进行编译即可查看触发实时更新的常量字面量。

live-editing-of-literals.gif

3.3.2 实时编辑

我们可以打开 Android Studio Electric Eel 的 Canary 版本,然后使用实时编辑功能加快 Compose 开发过程。相较于实时编辑字面量功能,“实时编辑”是具备更强大功能的版本。开发者可以自动将代码更改部署到模拟器或设备上,从而实时查看可组合项更新后的效果。

live-edit-only-device.gif

3.3.3 Apply Changes

Apply Changes 支持更新代码和资源,并且不需要在模拟器或实体设备上重新运行代码。每当开发者添加、修改或删除可组合项时,只需要点击一下此按钮,即可更新应用,而不必重新部署。

image.png

 

3.4 布局检查器

通过布局检查器,开发者可以在模拟器或实体设备上检查正在运行的应用中的 Compose 布局。

image.png

如需跟踪重组,需要在视图选项中启用【Show Recomposition Counts】。

image.png 启用后,布局检查器会在左侧显示重组次数,在右侧显示跳过重组的次数。

image.png

3.5 动画

Android Studio 允许开发者从动画预览中检查动画,我们可以在组合项预览中描述了动画效果,检查每个动画值在给定时间点的确切值,并且可以暂停、循环播放、快进或放慢动画,以便在动画过渡过程中调试动画。

animation-preview.gif

当然,我们也可以使用动画预览以图形方式呈现动画曲线,这对于确保正确编排动画值非常有用。

image.png

四、Kotlin与Jetpack Compose配合使用

Jetpack Compose使用Kotlin构建而成,在某些情况下,Kotlin 提供了一些特殊的惯用语,可以帮助开发者编写良好的 Compose 代码。如果使用另一种编程语言,那么很可能会错失 Compose 的一些优势。

 

4.1 默认参数

编写 Kotlin 函数时,我们可以指定函数参数的默认值;如果调用方未明确传递相应的值,系统就会使用这些默认值。在 Kotlin 中,编写一个函数并指定参数的默认值的方式和Java是类似的。

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) { ... }

当然,上面的代码也可以简写成下面的方式,Kotlin的编译器也是可以识别的。

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

 

4.2 高阶函数和 lambda 表达式

所谓高阶函数,指的是接收其他函数作为参数的函数,Kotlin也是支持高阶函数的。例如,Button 可组合函数提供了一个 onClick lambda 参数。

Button(
    // ...
    onClick = myClickFunction
)

高阶函数与 lambda 表达式是自然配对的。如果您只需要使用该函数一次,则不必在其他位置进行定义以将其传递给高阶函数,而只需使用 lambda 表达式在该位置定义该函数即可。

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

4.3 委托属性

Kotlin支持委托属性,这些属性可以像字段一样被调用,但它们的值是通过对表达式动态确定的。

class DelegatingClass {
    var name: String by nameGetterFunction()
}


val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

当执行 println() 函数时,系统会调用 nameGetterFunction() 以返回字符串的值。同时,使用状态支持的属性时,这些委托属性特别有用。

var showDialog by remember { mutableStateOf(false) }
// Updating the var automatically triggers a state change
showDialog = true

 

4.4 协程

在 Kotlin 中,协程在语言级别提供了异步编程的支持。协程可以挂起执行,而不会阻塞线程。自适应界面本质上是异步的,而 Jetpack Compose 会在 API 级别引入协程而非使用回调来解决此问题。

 

Jetpack Compose 提供了可在界面层中安全使用协程的 API,rememberCoroutineScope 函数会返回一个 CoroutineScope,可以用它在事件处理脚本中创建协程并调用 Compose Suspend API。

val composableScope = rememberCoroutineScope()
Button(
    // ...
    onClick = {
            composableScope.launch {
            scrollState.animateScrollTo(0) // This is a suspend function
            viewModel.loadData()
        }
    }
) { /* ... */ }

默认情况下,协程会依序执行代码块。正在运行且调用挂起函数的协程会挂起其执行,直到挂起函数返回,即使挂起函数将执行移至其他 CoroutineDispatcher,也是如此。若要同时执行代码,则需要创建新的协程。在上述示例中,如需在滚动到屏幕顶部的同时从 viewModel 加载数据,则需要两个协程。

val composableScope = rememberCoroutineScope()
Button( 
    onClick = {   
        composableScope.launch {
            scrollState.animateScrollTo(0)
        }
        composableScope.launch {
            viewModel.loadData()
        }
    }
) { /* ... */ }

协程可帮助您更轻松地合并异步 API。在以下示例中,我们会将 pointerInput 修饰符与动画 API 结合,以便在用户点按屏幕时在元素的位置呈现动画效果。

@Composable
fun MoveBoxWhereTapped() {
    // Creates an `Animatable` to animate Offset and `remember` it.
    val animatedOffset = remember {
        Animatable(Offset(0f, 0f), Offset.VectorConverter)
    }
    Box(
        // The pointerInput modifier takes a suspend block of code
        Modifier.fillMaxSize().pointerInput(Unit) {
            // Create a new CoroutineScope to be able to create new
            // coroutines inside a suspend function
            coroutineScope {
                while (true) {
                    // Wait for the user to tap on the screen
                    val offset = awaitPointerEventScope {
                        awaitFirstDown().position
                    }
                    // Launch a new coroutine to asynchronously animate to where
                    // the user tapped on the screen
                    launch {
                        // Animate to the pressed position
                        animatedOffset.animateTo(offset)
                    }
                }
            }
        }
    ) {
        Text("Tap anywhere", Modifier.align(Alignment.Center))
        Box(
            Modifier
                .offset {
                    // Use the animated offset as the offset of this Box
                    IntOffset(
                        animatedOffset.value.x.roundToInt(),
                        animatedOffset.value.y.roundToInt()
                    )
                }
                .size(40.dp)
                .background(Color(0xff3c1361), CircleShape)
        )
    }
}