探索 Google Now in Android 开源项目
谷歌宣布了一个新的开源项目,Now in Android。在本文中,您将涵盖Now in Android项目中使用的整体架构、各层和技术栈,例如Material3、Jetpack Compose和应用程序性能。
Now in Android
https://github.com/android/nowinandroid
Material3
https://m3.material.io/
Jetpack Compose
https://developer.android.com/jetpack/compose
在您开始之前,我们建议您在本地设备上克隆Now in Android 并使用您的 Android Studio 打开该项目。
https://github.com/android/nowinandroid
如果您在 Android Studio中构建 Now应用,结果将如下所示:
Now in Android 中使用的内容是Google 团队在 Youtube 上的视频播放列表和Android Developers上的文章,因此您可以通过此应用程序学习与 Android 相关的技能。
Youtube 上的视频播放列表
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9AtgKMBBdphI-mrx8XzW56
Android Developers上的文章
https://medium.com/androiddevelopers
技术栈
我们看看在Android项目中的Now中使用了哪些库来构建UI层:
- Compose:现在在 Android 中使用 100% Jetpack Compose构建 UI 元素。此外,该项目使用了Activity、Foundation、Material3和Accompanist等 Compose 相关库。
https://developer.android.com/jetpack/compose
- Navigation:该项目使用Navigation Compose来导航屏幕,使用Hilt Navigation Compose来注入依赖项。
https://developer.android.com/jetpack/compose/navigation
[Hilt Navigation compose] https://developer.android.com/jetpack/compose/libraries#hilt
- WindowManager:Jetpack WindowManager用于支持响应式布局。
https://developer.android.com/large-screens
- Coil:Coil用于在 UI 元素上加载图像。
https://github.com/coil-kt/coil
接下来我们看看用了哪些库来处理业务工作:
- DataStore:将数据以键值对的形式异步存储到本地数据存储中。
https://developer.android.com/topic/libraries/architecture/datastore
- Room Database:通过在 SQLite 上提供抽象层来构建本地数据库,以允许流畅的数据库访问。
https://developer.android.com/jetpack/androidx/releases/room
- Retrofit:Retrofit 是一个类型安全的 REST 客户端,旨在使用 REST API。
https://github.com/square/retrofit
- Kotlin 序列化:序列化和反序列化数据格式,例如 JSON 和协议缓冲区,可以通过网络传输或存储在数据库中。
- Kotlin Coroutines:提供语言层面的异步或非阻塞解决方案。本项目完全使用协程来异步处理任务。
https://kotlinlang.org/docs/serialization.html
- WorkManager:WorkManager是后台运行任务的推荐解决方案,在Android Now中用于同步本地数据和网络资源。
https://developer.android.com/topic/libraries/architecture/workmanager
此外,Android 中的 Now是使用以下库构建的,以改进应用程序架构和性能:
- Hilt:Hilt 是一个依赖注入库,可让您轻松构建依赖注入容器并自动管理它们的生命周期。
https://developer.android.com/training/dependency-injection/hilt-android
- App Startup:App Startup 允许您在应用程序启动时初始化组件。
https://developer.android.com/topic/libraries/app-startup
基线配置文件:基线配置文件允许您通过在 APK 中包含可供Android 运行时使用的类和方法规范列表来提高应用程序性能。
在本文中,我们将探索 2022 年 Google I/O 中涵盖的上述一些库和技术堆栈,以及应用架构。
应用架构
Google 推出了
应用架构指南
https://developer.android.com/topic/architecture
以涵盖构建稳健、高质量应用程序的最佳实践和推荐架构。
Now in Android是使用
应用架构指南
https://developer.android.com/topic/architecture
构建的,因此这将是一个很好的示例来展示该架构如何在实际项目中工作。
现在让我们探索应用程序架构。
架构概述
整体架构由两层组成:数据层和UI层。
https://developer.android.com/topic/architecture/data-layer
https://developer.android.com/topic/architecture/ui-layer
该架构遵循以下概念:
应用架构遵循单向数据流。所以UI层向下转发事件,数据层向上转发结果。
https://developer.android.com/topic/architecture/ui-layer#udf
数据层使用Kotlin Flows将数据公开为流,UI 元素通过观察流来配置屏幕。
https://kotlinlang.org/docs/flow.html
现在,让我们查看每一层以获取更多详细信息。
界面层
UI 层由 UI 元素组成,例如可以与用户交互的按钮和保存应用程序状态并在配置更改时恢复数据的ViewModel 。
https://developer.android.com/topic/libraries/architecture/viewmodel
UI层的主要作用如下:
- 建模 UI 状态:UI 状态被建模为密封的类/接口,表示遵循单一真实来源的业务数据。
https://en.wikipedia.org/wiki/Single_source_of_truth
- 将流转换为 UI 状态:视图模型将数据流转换为 UI 状态,表示业务数据。UI 元素观察 UI 状态并根据所有可能的情况将它们呈现在画布上。
- 处理用户交互:用户操作是与用户的通信,从 UI 元素流向视图模型,视图模型执行适当的业务逻辑。事件必须从 UI 元素向下流到数据层,这个概念称为单向数据流。
数据层
数据层由存储库组成,其中包括业务逻辑,例如从数据库持久化和查询数据以及从网络请求远程数据。它被实现为业务逻辑的离线优先来源,并遵循单一事实来源原则。
https://en.wikipedia.org/wiki/Single_source_of_truth
数据层的主要作用如下:
- 向 UI 层公开数据:数据层将数据公开为流,UI 元素通过观察流来配置屏幕。
- 保证单一事实来源:现在在 Android 中保证从多个数据源(如本地数据库和网络)聚合应用程序数据的单一事实来源。存储库使用数据同步来保证单一事实来源原则。
- 数据同步:同步本地数据库和网络数据之间的数据。同步工作由App Startup在应用程序初始化时启动。
https://developer.android.com/topic/libraries/app-startup
架构流程
架构的整体流程如下图所示:
让我们逐一查看每个步骤:
- 应用程序启动运行执行数据同步的WorkManager 。
https://developer.android.com/topic/libraries/architecture/workmanager
- WorkManager 在后台线程启动数据同步。worker从网络请求远程数据源,将app数据同步到本地数据库。
- 存储库将内部数据模型转换为外部数据流,并将流暴露给外部层。
- UI 元素通过观察流来配置屏幕。
使用 Compose 的 UI 层
现在在 Android 中使用 100% Jetpack Compose 来配置屏幕。Jetpack Compose 在生产级别的使用变得越来越稳定,Android 中的 Now展示了如何在您的项目中使用 Compose。
让我们探讨一下Now in Android如何利用 Jetpack Compose 组件。
Material You
去年,Google 的设计团队发布了 Material You,这是一种指导应用程序主题的新设计语言。Material You 提供动态配色方案,这是一种颜色提取算法,可让您从用户设备的壁纸中获取配色方案。
https://m3.material.io/styles/color/dynamic-color/overview
通过在您的项目中添加以下依赖项,您可以使用 Material Theme、Material 组件和动态配色方案:
https://gist.github.com/skydoves/f59d294d12ad08190fd45d23fbe13e8b#file-compose_material_library-groovy
接下来,无论您的系统主题是否处于深色模式,您都可以创建不同的动态配色方案:
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
https://gist.github.com/skydoves/436bf5e2386dd4222c34db1aa91ddda9#file-dynamic_color-kt
有关 Material You 的更多信息,请查看Exploring Material You for Jetpack Compose。
https://getstream.io/blog/material-you-jetpack-compose/
结果,您将看到不同颜色的组件,如下所示:
Theme
主题是现代 Android 开发的重要组成部分之一,要在每个基于 XML 的 UI 元素中应用一致的主题并不容易。但是,使用 Jetpack Compose,定义主题并将它们应用到 UI 元素很容易,而且大多数属性都是可自定义的。
[Material Theme]
https://developer.android.com/jetpack/compose/themes/material
现在在 Android 中使用Material Theme来定义配色方案和自定义主题。正如您在下面的示例中看到的,您可以使用lightColorSceme
和darkColorSceme
方法定义您的配色方案:
private val LightDefaultColorScheme = lightColorScheme(
primary = Purple40,
onPrimary = Color.White,
primaryContainer = Purple90,
..
private val DarkDefaultColorScheme = darkColorScheme(
primary = Purple80,
onPrimary = Purple20,
primaryContainer = Purple30,
..
https://gist.github.com/skydoves/029faecf182dcd802cd9ef4ccdc66b45#file-nia_color_scemes-kt
此外,现在在 Android 中使用dynamicLightColorScheme
和dynamicDarkColorScheme
方法获取从用户设备的壁纸生成的动态配色方案。
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
https://gist.github.com/skydoves/30a72b713a5426607ba28c5e8505edf3#file-nia_dynamic_color-kt
最后,NiaTheme
可组合函数如下所示:
@Composable
fun NiaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
androidTheme: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
androidTheme && darkTheme -> DarkAndroidColorScheme
androidTheme -> LightAndroidColorScheme
darkTheme -> DarkDefaultColorScheme
else -> LightDefaultColorScheme
}
https://gist.github.com/skydoves/267c6b020ef77434e1cbeee5c8b720aa#file-nia_theme-kt
正如您在上面的主题示例中看到的,您可以应用自定义或动态配色方案,并在特定情况下轻松地在它们之间切换。
Large Screen
最近,随着各种大屏设备的发布,支持大屏是现代Android开发的重要组成部分之一。现在在 Android 中支持使用Jetpack WindowManager
的自适应布局。
首先,您需要将以下依赖项添加到您的build.gradle
文件中:
dependencies {
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha10"
implementation "androidx.window:window:1.0.0"
}
https://gist.github.com/skydoves/eeae289485b3d67d8c64d0c006168964#file-windowmanager_lib-groovy
calculateWindowSizeClass
接下来,您可以使用以下方法根据用户设备的宽度大小计算窗口大小类别:
val windowSizeClass: WindowSizeClass = calculateWindowSizeClass(activity = this)
https://gist.github.com/skydoves/12dede790e05be69043ea2a243bf5e83#file-calculatewindowsizeclass-kt
WindowSizeClass
由三种类型的断点组成:Compact
、Medium
和Expanded
。正如您在下面的内部代码中看到的,这三个断点是根据 Material design guide 的设计规范计算的:
value class WindowWidthSizeClass private constructor(private val value: String) {
companion object {
/** Represents the majority of phones in portrait. */
val Compact = WindowWidthSizeClass("Compact")
/**
* Represents the majority of tablets in portrait and large unfolded inner displays in
* portrait.
*/
val Medium = WindowWidthSizeClass("Medium")
/**
* Represents the majority of tablets in landscape and large unfolded inner displays in
* landscape.
*/
val Expanded = WindowWidthSizeClass("Expanded")
/** Calculates the [WindowWidthSizeClass] for a given [width] */
internal fun fromWidth(width: Dp): WindowWidthSizeClass {
require(width >= 0.dp) {
"Width must not be negative" }
return when {
width < 600.dp -> Compact
width < 840.dp -> Medium
else -> Expanded
}
}
}
}
https://gist.github.com/skydoves/1ed195d2bb2d9c45d5ff2d2da16ec820#file-windowsizeclass-kt
计算后WindowSizeClass
,现在在 Android 中绘制不同的 UI 元素WindowSizeClass
:
Scaffold(
modifier = Modifier,
containerColor = Color.Transparent,
bottomBar = {
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
NiABottomBar(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination
)
}
}
) {
padding -> {
..
if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) {
NiANavRail(
onNavigateToTopLevelDestination = niaTopLevelNavigation::navigateTo,
currentDestination = currentDestination,
modifier = Modifier.safeDrawingPadding()
)
}
..
}
https://gist.github.com/skydoves/aaaf2895138ac68973ba54ddc6e87519#file-adaptive_layout-kt
在上面的示例中,如果是WindowSizeClassCompact
则WindowSizeClass
主屏幕绘制底栏,如果是Medium或Expanded则绘制侧边栏。
因此,您将看到根据用户设备的宽度大小不同的屏幕,如下图所示:
有关自适应布局的更多信息,请查看探索适用于可折叠设备的 Jetpack WindowManager。
https://medium.com/proandroiddev/exploring-jetpack-windowmanager-for-foldable-devices-2a28fa25fe93
App Performance
Android 设备使用的资源非常有限,因此提高 App 性能是 Android 开发提供出色用户体验的最重要部分。
因此,让我们探索如何使用 Jetpack Compose 提高应用程序性能。
Remember
Jetpack Compose 使用remember
API 将对象存储在内存中。API在Composition
remember
阶段存储计算值,并在Recomposition
期间恢复存储的值。
[remember]https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0)
https://developer.android.com/jetpack/compose/lifecycle#composition-anatomy
https://developer.android.com/jetpack/compose/mental-model#recomposition
让我们看一个例子。每当重组发生时,下面的代码将对整个联系人进行排序:
如果联系人列表中包含的项目很多,则对列表进行排序的开销很大,并且可能会降低应用程序的性能。所以你可以利用 来remember
记住一个需要高成本的计算值,比如排序。
如上例所示,您可以使用remember
API 将排序后的值保存在内存中。当重组发生并重用排序后的值时,将不会执行计算。
此外,它使用联系人列表和比较器作为 中的键remember
,因此只有键值更改时才会重新执行排序。
Lazy lists
在探索Lazy lists之前,让我们看看Jetpack Compose 中Column
和LazyColumn
之间的区别。
它们看起来非常相似,并且Column还可以使用 for 语句显示项目列表。但是,Column无论项目是否可见,都会呈现 Column 范围内的所有项目,如果列表中有很多项目,它可能会降低应用程序性能。
另一方面,采用了与RecyclerView
LazyColumn
类似的概念,只渲染屏幕上可见的项目。因此,如果您使用而不是在有很多项目时使用,则可以提高应用程序性能。LazyColumn
LazyColumn
Column
接下来,让我们假设一个项目的位置发生了变化。目前,LazyColumn
无法区分列表中的不同项目,LazyColumn
将对整个项目进行重组。因此,每当更改项目的位置时,它都会产生不良性能。
LazyColumn
您可以通过提供代表每个项目的唯一 ID 的参数来提高性能,key如下所示:
通过给定一个key
参数,LazyColumn
当一个item
的位置发生变化时,可以区分出专用的item
,不会对整个item
进行推导重组。
如果您想了解有关 Jetpack Compose 性能的更多信息,请查看Compose 性能。
https://developer.android.com/jetpack/compose/performance
Baseline Profiles
Baseline Profiles是 APK 和 AAB 文件中包含的类和方法的规范,可供Android 运行时使用。规范可以在安装或更新应用程序时预编译并预加载到内存中。
Android 运行时执行规范的提前 (AOT) 编译,使应用程序可以优化启动、减少 UI 卡顿并提高应用程序性能。
特别是,Baseline Profiles 为 Jetpack Compose 项目提高了很多应用程序性能。因为,Jetpack Compose 是一个库,这意味着它不参与 Android 操作系统中的系统资源共享。
要使用Baseline Profiles生成方法规范,请将以下依赖项添加到您的build.gradle
文件中:
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.2.0-beta01")
}
https://gist.github.com/skydoves/cb8cc143322da64ef5929f7034deb989#file-dep_baseline_profiles-groovy
现在在 Android 中使用BaselineProfileGenerator
下面的类来生成配置文件:
class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule()
@Test
fun startup() =
baselineProfileRule.collectBaselineProfile(
packageName = "com.google.samples.apps.nowinandroid"
) {
pressHome()
// This block defines the app's critical user journey. Here we are interested in
// optimizing for app startup. But you can also navigate and scroll
// through your most important UI.
startActivityAndWait()
device.waitForIdle()
device.run {
findObject(By.text("Interests"))
.click()
waitForIdle()
}
}
}
https://gist.github.com/skydoves/c450fec8148c3dd9f72c1c74e72c34fc#file-baseline_proflies-kt
因此,配置文件生成器将在一个txt文件中生成以下规范,并将用于 Android Runtime 的预编译:
有关更多信息,请查看baselineprofiles。
https://developer.android.com/topic/performance/baselineprofiles
结论
本期探讨了Now in Android ,涵盖了现代 Android 开发的重要部分,包括应用程序架构、Jetpack 库、带 Compose 的 UI 层以及应用程序性能。