Google I/O :Android Jetpack 最新变化(四)Compose

5,725 阅读7分钟

前言

本系列文章从 Architecture,UI,Performance 和 Compose 这四个方向带大家了解本次 I/O 上 Jetpack 的最新内容。

本文是最终篇:Compose 篇。

今年 I/O 大会上关于 Compose 的主题分享明显增多了,这也表明了谷歌对于 Compose 推广之重视。目前 GooglePlay Top1000 的应用中使用 Compose 的已经超过了 100 个,其中不乏一些领域头部应用,Compose 的稳定性和成熟度也借机得到了验证。

让我们看看 Compose 最新的 1.2 Beta 版本带来哪些新内容。

1. Material 3

新增的 Compose.M3 库,可以帮助我们开发符合 Material You 设计规范的的 UI 界面。

implementation "androidx.compose.material3:material3:1.0.0-alpha10"
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha10"

Material3 强调颜色的个性化和动态切换,Compose.M3 引入 ColorScheme 类自定义配色方案:

val AppLightColorScheme = lightColorScheme (
    primary = Color(...),
    // secondary、tertiary 等等
    // 具有浅色基准值的 ColorScheme 实例
)
 
val AppDarkColorScheme = darkColorScheme(
    // primary、secondary、tertiary 等等
    // 具有深色基准值的 ColorScheme 实例val dark = isSystemInDarkTheme()
val colorScheme = if (dark) AppDarkColorScheme else AppLightColorScheme
 
// 将 colorScheme 作为参数传递给 MaterialTheme。
MaterialTheme (
    colorScheme = colorScheme,
    // 字型
) {
    // 应用内容
}

上面是 MaterialTheme 通过 ColorScheme 配置不同主题颜色的例子,可以看到这与 Compose.M2 中 Colors 用法区别不大, 但是 ColorScheme 可定义的颜色槽(Primary,Secondary,Error 等MD颜色常量)种类更多,而且还可以支持 DynamicColor 动态配色。

DynamicColor 是 Material3 的重要特色,在 Android12 及以上设备中,可以实现应用的颜色跟随壁纸变化。如今 Compose 中也可以实现这个效果

// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
    dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
    dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)    
    darkTheme -> DarkColorScheme    
    else -> LightColorScheme
}

如上,Compose 通过 dynamicXXXColorScheme 设置的颜色,无论是亮色还是暗色主题,都可以跟随用户设置的壁纸而变化:

更多参考:juejin.cn/post/706441…

2. Nested Scrolling Interop

Compose 支持与传统视图控件进行互操作,便于我们阶段性的引入 Compose 到项目中。但是在涉及到带有 Nested Scrolling 事件分发的场景中(例如 CoordinatorLayout ),会发生事件无法正常传递的兼容性问题,在 1.2 中对于此类问题进行了修复,无论是 CoordinatorLayout 内嵌 Composable , 或者在 Composable 中使用 Scrolling View 控件,事件传递都会更加平顺:

android-review.googlesource.com/c/platform/…
android-review.googlesource.com/c/platform/…

3. Downloadable Fonts

Android 8.0(API level 26)起支持了对可下载的谷歌字体的使用,允许通过代码动态请求一个非内置字体文件。在 Compose 1.2 对此功能也进行了支持,注意这个功能需要基于 GMS 服务。

implementation "androidx.compose.ui:ui-text-google-fonts:1.1.1"

使用时,首先使用 FontProvider 定义字体请求信息

@OptIn(ExperimentalTextApi::class)
val provider = GoogleFont.Provider(
   providerAuthority = "com.google.android.gms.fonts",
   providerPackage = "com.google.android.gms",
   certificates = R.array.com_google_android_gms_fonts_certs
)
然后,使用此 Provider 定义 FontFamily,接着在 Composable 应用即可,
val fontName = GoogleFont(“Lobster Two”)

val fontFamily = FontFamily(
   Font(googleFont = GoogleFont(name), fontProvider = provider)
)

Text(
    fontFamily = fontFamily,
    text = "Hello World!"
)

4. Lazy Grid

Compose 1.2 中进一步优化了 LazyRow 和 LazyColumn 的性能,并在此基础上新增了 LazyGrid 用来实现需求中常见的网格布局效果。Lazy Grid 在 1.0.2 就已经引入,如今 1.2 中对 API 进行调整并使之达到稳定。

image.png

以 LazyVerticalGrid 为例,我们可以通过 GridCells.Fixed 设置每行单元格的数量:

val data = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    contentPadding = PaddingValues(8.dp)
) {//this: LazyGridScope
    items(data.size) { index ->
        Card(
            modifier = Modifier.padding(4.dp),
            backgroundColor = Color.LightGray
        ) {
            Text(
                text = data[index],
                textAlign = TextAlign.Center
            )
        }
    }
}

此外,也可以通过 GridCells.Adaptive() 通过制定单元格大小决定每行的数量。此时,所有单元格都会以 Adaptive 中的值设置统一的 width。

LazyGridScope 像 LazyListScope 一样也提供了 item, items, itemsIndexed 等方法布局子项。另外 LazyGridState 中的方法也基本上对齐了 LazyListState。

5. Tools

在工具方面,Android Studio 为 Compose 的开发调试提供了更多实用功能。

@Preview & Live Edit

1.2.0 中的 @Preview 可以作为元注解使用,修饰其他自定义注解

@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
annotation class MyDevices()

@MyDevices
@Composable
fun Greeting() {
    ...
}

如上,我们可以通过自定义注解可以复用 @Preview 中的各种配置,减少为了预览而写的模板代码。

说到预览,Android Studio 一直致力于提升预览效率,Android Studio Arctic Fox 曾引入 Live literals 功能,对于代码中 Int,String,Color,Dp,Boolean 等常见类型的字面值的修改,无需编译即可在预览画面中实时更新。本次大会上带来了升级版的 Live Edit,它需要使用最新的 Android Studio Electric Eel 中开启。不仅仅是字面值,它可以让任意代码的修改(函数签名变动之类的修改不行),在预览窗口或者你的设备上立即生效,几乎实现了前端一般的开发体验,是本次大会令我惊喜的功能,它将大幅提高 Compose 的开发和调试效率。

ezgif.com-gif-maker (19).gif

Layout Inspector & Recomposition Counts

我们在传统视图开发中经常使用 Layout Inspector 观察视图结构, Compose 虽然基于 Composable 函数构建 UI ,但同样也得到了 layout Inspector 的支持,它可以帮助我们查看 Composition 视图树的布局。

此外,本次 I/O 还介绍了 Layout Inspector 的一个新功能 Recomposition Counts,我们知道不必要的重组会拖慢 Compose UI 的刷新性能,借助这个新工具,我们可以在 IDE 中调试和观察 Composable 重组次数,帮助我们及时发现和优化不符合预期的多余重组。

Animation Preview

Android Studio 增加了对 Compose 动画效果实时预览。在动画预览窗口中,每个动画的状态值会以多轨道的形式呈现,我们可以查看特定时间点下的每个动画值的确切值,并且可以暂停、循环播放动画、快进或放慢动画,以便在动画过渡过程中调试动画。

Compose 的动画 API 数量众多,目前并非所有的 API 都支持预览,IDE 会自动检查代码中可以进行预览的动画,并添加 Start Animation Inspection 图标,便于开发者发现和使用

6. 适应多种屏幕尺寸

Compose 正逐渐成为 Android 的首选 UI 开发方案,所以为了覆盖尽可能多的使用场景,Compose 第一时间对各种屏幕尺寸下(手机,平板,电脑,折叠屏)的 UI 开发进行了支持。 在具体开发中,我们需要先定义 WindowSizeClass 对各种屏幕类型分类,推荐分为三类:

当屏幕尺寸因为设备折叠等发生变化时,Compose 会自动响应 onConfigurationChanged 触发重组,重组中我们根据当前屏幕尺寸转换为对应的 WindowSizeClass

@Composable
fun Activity.rememberWindowSizeClass(): Pair<WindowWidthSizeClass, WindowHeightSizeClass> {
    val configuration = LocalConfiguration.current
    val windowMetrics = remember(configuration) {
        WindowMetricsCalculator.getOrCreate()
            .computeCurrentWindowMetrics(this)
    }
    val windowDpSize = with(LocalDensity.current) {
        windowMetrics.bounds.toComposeRect().size.toDpSize()
    }
    val widthWindowSizeClass = when {
        windowDpSize.width < 600.dp -> WindowWidthSizeClass.Compact
        windowDpSize.width < 840.dp -> WindowWidthSizeClass.Medium
        else -> WindowWidthSizeClass.Expanded
    }

    val heightWindowSizeClass = when {
        windowDpSize.height < 480.dp -> WindowHeightSizeClass.Compact
        windowDpSize.height < 900.dp -> WindowHeightSizeClass.Medium
        else -> WindowHeightSizeClass.Expanded
    }

    return widthWindowSizeClass to heightWindowSizeClass
}

接下来,我们就可以面向 WindowSizeClass 进行 Composable 布局了,这样做的好处是,无需关心具体的 width/height 数值,更不需要关心当前设备类型是平板还是手机,因为未来,硬件种类的界限将越来越模糊,所以最合理的分类方式是 WindowSizeClass。

@Composable
fun MyApp(widthSizeClass: WindowWidthSizeClass) {
    // 非 Compact 类型屏幕时,不显示 AppBar
    val showTopAppBar = widthSizeClass != WindowWidthSizeClass.Compact

    // MyScreen 不依赖 WindowSizeClass,只需要知道是否显示 showTopAppBar,关注点分离
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

当然我们可以使用 Android Studio 便利的预览功能,同时查看多种屏幕尺寸下的显示效果

最佳实践: Now In Android

最后推荐一个谷歌刚刚开源的新项目 Now In Android。 Now in Android 是 Android 官方的技术博客,分享技术文章和视频,如今这个博客有了自己的客户端,并在 Github 进行了开源,github.com/android/now…

开发者通过 App 可以更好地追踪 Android 最新的技术动向,更重要的是它本身就是一个 Android Jetpack 的最佳实践,在技术上它具有以下特点:

  • 基于 Jetpack Compose 实现 UI
  • 基于 Material3 的视觉样式和主题
  • 对不同尺寸的屏幕进行了支持,能够自适应布局
  • 整体架构遵循官方文档 UDF 范式
  • 基于 Kotlin Flow 实现响应式编程模型
  • 遵循 Offline first 设计原则,基于 Room 以及 Proto DataSotre 实现本地数据源,
  • 基于 WorkManager 实现远程/本地数据源之间的同步

另外,GIthub 上还贴心了附上了架构设计文档,方便你了解它的开发思路,Now in Android 已经预定上架 GooglePlay, 相对于 Jetpack 的其他 Demo,它是更加真实和完善,非常值得大家研究和学习。