![cover](https://img-blog.csdnimg.cn/cb422059461248299b18921005df2f8a.png)
MAD,现代安卓开发技术:Android 领域开发方式的重大变革~
Android 诞生已久,其开发方式保持着高频更迭,相较于早期的开发方式已大不相同,尤其是近几年 Google 热切推崇的 MAD 开发技术。其实很多开发者已经有意或无意地正在使用这门技术,借着 2022 开年探讨技术趋势的契机,想要完整地总结 MAD 的愿景、构成、优势以及一些学习建议。MAD,全称 Modern Android Development:是 Google 针对 Android 平
Android 诞生已久,其开发方式保持着高频更迭,相较于早期的开发方式已大不相同,尤其是近几年 Google 热切推崇的 MAD 开发技术。其实很多开发者已经有意或无意地正在使用这门技术,借着 2022 开年探讨技术趋势的契机,想要完整地总结 MAD 的愿景、构成、优势以及一些学习建议。
MAD,全称 Modern Android Development
:是 Google 针对 Android 平台提出的全新开发技术。旨在指导我们利用官方推出的各项技术来进行高效的 App 开发。有的时候 Google 会将其翻译成现代安卓开发
,有的时候又翻译成新式安卓开发
,个人觉得前者的翻译虽然激进、倒也贴切。
下面按照 MAD 的构成要点逐步展开,帮助大家快速了解 MAD 的技术理念。如果大家对其中的语言、工具包或框架产生了兴趣,一定要在日后的开发中尝试和掌握。
内容前瞻
- 【Modern Android Development】讲述 Android 全新开发技术的由来和构成
- 【Android Studio】演示 Android 官方 IDE 的重要特性
- 【Android App Bundle】简要普及 Google 推崇的 App 新格式
- 【Kotlin】解读 Android 首推的开发语言的优点
- 【Jetpack】讲述 Android 持续更新的重大框架集合,并逐个演示重要框架解决的问题和优势
- 【Jetpack Compose】带领大家感受 Android 上 UI 开发方式的重大变革
1.Modern Android Development
![](https://s2.loli.net/2022/01/23/ZwlW6kY2xcfOuJF.png)
官方一直在优化 App 的开发体验:从 IDE 到语言再到框架,这些新技术愈发完善也愈发琐碎。提出一个全新的概念来整合这些松散的技术方便介绍和推广,也方便开发者们理解。
MAD 便是提出的全新理念,期望在语言、工具、框架等多个层面提供卓越的开发体验,其愿景和优势:
- 倾力打造:汇聚 Google 在 Android 行业十余年的前言开发经验
- 入门简单:提供大量 Demo 和详尽文档,适用于各阶段各规模的项目
- 迅速起步:提供显著降低样板代码的开发框架 Jetpack 和 UI 工具包 Jetpack Compose
- 自由选择:框架丰富多样,可与传统语言、原生开发、开源框架自由搭配
- 统合一致:兼容不同设备的开发框架达到的一致性开发体验
其涵盖的内容:
- Android Studio :持续改进的官方 IDE
- Android App Bundle :先进的应用打包和分发方式
- Kotlin :首推的编程语言
- Jetpack :独立于 AOSP 以外,汇集了大量开发框架的开发套件
- Jetpack Compose:Android 平台重大变革的 UI 工具包
同时,官方针对 MAD 技术提供了认证考试和技能的计分插件,大家在实践一段时间之后可以体验一下:
- MAD 资格认证
- Android Studio 的
MAD Skills
计分插件
![](https://s2.loli.net/2022/01/23/G4RwNIdvsAQUpBY.png)
2.Android Studio
![](https://s2.loli.net/2022/01/23/Ddg8SNyqRxQi6LA.jpg)
Android Studio 刚推出的初期饱受批评,吃内存、Bug 多、不好用,开发者一度对 Eclipse 恋恋不舍。随着 Google 和开发者的不断协力,AS 愈加稳定、功能愈加强大,大家可以活用 AS 的诸多特性以提高开发效率。和 Chrome 一样,针对不同需求,AS 提供了三个版本供开发者灵活选择。
版本 | 说明 |
---|---|
Stable Release | 稳定发行版,最新版为 Arctic Fox|2020.3.1 |
Release candidate | 即将发布的下一代版本,可以提前体验新特性和优化,最新版为 Bunblebee|2021.1.1 |
Canary | 试验版本,不稳定但可以试用领先的实验功能,最新版为 Chipmunk|2021.2.1 |
接下来介绍 AS 其中几个好用的特性。
2.1 Database Inspector
![](https://s2.loli.net/2022/01/23/kMHNy5DrAnKQ17e.png)
Database Inspector
可以实时查看 Jetpack Room
框架生成的数据库文件,同时也支持实时编辑和部署到设备当中。相较之前需要的 SQLite
命令或者额外导出并借助 DB 工具的方式更为高效和直观。
2.2 Layout / Motion Editor
![](https://img-blog.csdnimg.cn/img_convert/f993d5dc7a17477c9eb7cd6eb78c9342.png)
Layout Editor
拥有诸多优点,不知大家熟练运用了没有:
- 可以直观地编辑 UI:随意拖动视图控件和更改约束指向
- 在不同配置(设备、主题、语言、屏幕方向等)下灵活切换预览,免去实机调试
- 搭配
Tools
标签自由定制 UI,确保只面向调试而不影响实际逻辑。比如:布局中有上下两个控件,上面的默认为invisible
,想确认下上面的控件如果可见的话对整体布局的影响。无需更改控件的visibility
属性,添加 Tools:visibility=true 即可预览布局的变化
Motion Editor
则是支持 MotionLayout 类型布局的视觉设计编辑器,可让更轻松地创建和预览和调试动画。
Layout Inspector
则可以查看某进程某画面的详细布局,完整展示 View 树的各项属性。在不方便代码调试或剖析其他 App 的情况下非常好用。同时已经支持直接检查 Compose 编写的 UI 布局了,喜极而泣。
![](https://s2.loli.net/2022/01/23/yrRCQa9s1bWEVzw.png)
2.3 Realtime Profilers
![](https://img-blog.csdnimg.cn/img_convert/cfe6db016126ffa2ebdbb906cb0e7e31.png)
AS 的 Realtime Profilers 工具可以帮助我们在如下四个方面监测和发现问题,有的时候在没有其他 App 代码的情况下通过 Memory Profilers 还可以查看其内部的实例和变量细节。
- CPU:性能剖析器检查 CPU 活动,切换到 Frames 视图还可以界面卡顿追踪
- Memory:识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配以定位内存方面的问题
- Battery:会监控 CPU、网络无线装置和 GPS 传感器的使用情况,并直观地显示其中每个组件消耗的电量,了解应用在哪里耗用了不必要的电量
- Network:显示实时网络活动,包括发送和接收的数据以及当前的连接数。这便于您检查应用传输数据的方式和时间,并适当优化代码
2.4 APK Analyzer
![](https://s2.loli.net/2022/01/23/AFEwJe1D8bGH4gQ.png)
Apk 的下载会耗费网络流量,安装了还会占用存储空间。其体积的大小会对 App 安装和留存产生影响,分析和优化其体积显得尤为必要。
借助 AS 的 APK Analyzer
可以帮助完成如下几项工作:
- 快速分析 Apk 构成,包括 DEX、Resources 和 Manifest 的 Size 和占比,助力我们优化代码或资源的方向
- Diff Apk 以了解版本的前后差异,精准定位体积变大的源头
- 分析其他 Apk,包括查看大致的资源和分析代码逻辑,进而拆解、Bug 定位
2.5 其他特性
篇幅原因只介绍了少部分特性,其他的还有很多,需要各位自行探索:
- 性能提升、内嵌到 AS 界面内的的
Fast Emulator
- 实时预览和编辑 Compose 布局,并支持直接交互的
Compose Preview
- 针对
Jetpack WorkManager
的Background Task Inspector
- 。。。
相比之下,Google 官方的这篇「Android Studio 新特性详解」介绍得更新、更全,大家可以一看。
3.Android App Bundle
![](https://s2.loli.net/2022/01/23/GygTJuQbwx6DXh5.jpg)
android app bundle 是一种发布格式,其中包含您应用的所有经过编译的代码和资源,它会将 APK 生成及签名交由 Google Play 来完成。
这个新格式对面向海外市场的 3rd Party App 影响较大,对面向国内市场的 App 影响不大。但作为未来的构建格式,了解和适配是迟早的事。
- 其针对目标设备优化 Apk 的构建,比如只预设对应架构的
so
文件、图片和语言资源。得以压缩体积,进而提升安装成功率并减少卸载量 - 支持便捷创建
Instant App
,可以免安装、直接启动、体验试用 - 满足模块化应用开发,提升大型项目的编译速度和开发效率
Google 对 .aab
格式非常重视,也极力推广:从去年也就是 2021 年 8 月起,规定新的 App 必须采用该格式才能在 Google Play 上架。
fun 神的「AAB 扶正!APK 将退出历史舞台」文章针对 AAB 技术有完整的说明,可以进一步了解。
4.Kotlin
![](https://s2.loli.net/2022/01/23/cPlR7tQq2GAuKD3.png)
A modern programming language that makes developers happier.
Kotlin
是 大名鼎鼎的 JetBrains
公司于 2011 年开发的面向 JVM
的新语言,对于 Android 开发者来说,选择 Kotlin 开发 App 有如下理由:
Google IO
2019 宣布 Kotlin 成为了官方认定的 Android 平台首选编程语言,这意味着会得到 Google 巨佬在 Android 端的鼎力支持以实现超越 Java 的优秀编程体验- 通过
KMM
(Kotlin Multiplatform Mobile)实现跨移动端的支持 Server-side
,天然支持后端开发- 通过
Kotlin/JS
编译成JavaScript
,支持前端开发 - 和 Java 几乎同等的编译速度,增量编译下性能甚至超越 Java
4.1 Kotlin 在 Android上优秀的编程体验
-
Kotlin 代码简洁、可读性高:缩减了大量样板代码,以缩短编写和阅读代码的时间
-
可与 Java 互相调用,灵活搭配
-
容易上手,尤其是熟悉 Java 的 Android 开发者
-
代码安全,编译器严格检查代码错误
-
专属的协程机制,大大简化异步编程
-
提供了大量 Android 专属的
KTX
扩展 -
唯一支持 Android 全新 UI 编程方式
Compose
的开发语言
很多知名 App 都已经采用 Kotlin 进行开发,比如 Evernote、Twiiter、Pocket、WeChat 等。
![](https://z3.ax1x.com/2021/05/10/gtKtzV.png)
下面我们选取 Kotlin 的几个典型特性,结合代码简单介绍下其优势。
4.2 简化函数声明
Kotlin 语法的简洁体现在很多地方,就比如函数声明的简化。
如下是一个包含条件语句的 Java 函数的写法:
String generateAnswerString(int count, int countThreshold) {
if (count > countThreshold) {
return "I have the answer.";
} else {
return "The answer eludes me.";
}
}
Java 支持三元运算符可以进一步简化。
String generateAnswerString(int count, int countThreshold) {
return count > countThreshold ? "I have the answer." : "The answer eludes me.";
}
Kotlin 的语法并不支持三元运算符,但可以做到同等的简化效果:
fun generateAnswerString(count: Int, countThreshold: Int): String {
return if (count > countThreshold) "I have the answer." else "The answer eludes me."
}
它同时还可以省略大括号和 return 关键字,采用赋值形式进一步简化。这样子的写法已经很接近于语言的日常表达,高级~
fun generateAnswerString(count: Int, countThreshold: Int): String =
if (count > countThreshold) "I have the answer." else "The answer eludes me."
反编译 Class 之后发现其实际上仍采用的三元运算符的写法,这种语法糖会体现在 Kotlin 的很多地方😅。
public final String generateAnswerString2(int count, int countThreshold) {
return count > countThreshold ? "I have the answer." : "The answer eludes me.";
}
4.3 高阶函数
介绍高阶函数之前,我们先看一个向函数内传入回调接口的例子。
一般来说,需要先定义一个回调接口,调用函数传入接口实现的实例,函数进行一些处理之后执行回调,借助Lambda 表达式可以对接口的实现进行简化。
interface Mapper {
int map(String input);
}
class Temp {
void main() {
stringMapper("Android", input -> input.length() + 2);
}
int stringMapper(String input, Mapper mapper) {
// Do something
...
return mapper.map(input);
}
}
Kotlin 则无需定义接口,直接将匿名回调函数作为参数传入即可。(匿名函数是最后一个参数的话,方法体可单独拎出,增加可读性)
这种接受函数作为参数或返回值的函数称之为高阶函数,非常方便。
class Temp {
fun main() {
stringMapper("Android") {input -> input.length + 2}
}
fun stringMapper(input: String, mapper: (String) -> Int): Int {
// Do something
...
return mapper(input)
}
}
事实上这也是语法糖,编译器会预设默认接口来帮忙实现高阶函数。
4.4 Null 安全
可以说 Null 安全是 Kotlin 语言的一大特色。试想一下 Java 传统的 Null 处理无非是在调用之前加上空判断或卫语句,这种写法既繁琐,更容易遗漏。
void function(Bean bean) {
// Null check
if (bean != null) {
bean.doSometh();
}
// 或者卫语句
if (bean == null) {
return;
}
bean.doSometh();
}
而 Kotlin 要求变量在定义的时候需要声明是否可为空:带上 ?
即表示可能为空,反之不为空。作为参数传递给函数的话也要保持是否为空的类型一致,否则无法通过编译。
比如下面的 functionA() 调用 functionB() 将导致编译失败,但 functionB() 的参数在声明的时候没有添加 ? 即为非空类型,那么函数内可直接使用该参数,没有 NPE 的风险。
fun functionA() {
var bean: Bean? = null
functionB(bean)
}
fun functionB(bean: Bean) {
bean.doSometh()
}
为了通过编译,可以将变量 bean 声明中的 ? 去掉, 并赋上正常的值。
但很多时候变量的值是不可控的,我们无法保证它不为空。那么为了通过编译,还可以选择将参数 bean 添加上 ? 的声明。这个时候函数内不就不可直接使用该参数了,需要做明确的 Null 处理,比如:
- 在使用之前也加上 ? 的限定,表示该参数不为空的情况下才触发调用
- 在使用之前加上
!!
的限定也可以,但表示无论参数是否为空的情况下都触发调用,这种强制的调用即会告知开发者此处有 NPE 的风险
fun functionB(bean: Bean?) {
// bean.doSometh() // 仍然直接调用将导致编译失败
// 不为空才调用
bean?.doSometh()
// 或强制调用,开发者已知 NPE 风险
bean!!.doSometh()
}
总结起来将很好理解:
- 参数为非空类型,传递的实例也必须不为空
- 参数为可空类型,内部的调用必须明确地 Null 处理
反编译一段 Null 处理后可以看到,非空类型本质上是利用 @NotNull
的注解,可空类型调用前的 ? 则是手动的 null 判断。
public final int stringMapper(@NotNull String str, @NotNull Function1 mapper) {
...
return ((Number)mapper.invoke(str)).intValue();
}
private final void function(String bean) {
if (bean != null) {
boolean var3 = false;
Double.parseDouble(bean);
}
}
4.5 协程 Coroutines
介绍 Coroutines
之前,先来回顾下 Java 或 Android 如何进行线程间通信?有何痛点?
![](https://s2.loli.net/2022/01/23/ZwjE3bplmTsYXAv.png)
比如:AsyncTask
、Handler
、HandlerThread
、IntentService
、RxJava
、LiveData
等。它们都有复杂易错、不简洁、回调冗余的痛点。
比如一个请求网络登录的简单场景:我们需要新建线程去请求,然后将结果通过 Handler 或 RxJava 回传给主线程,其中的登录请求必须明确写在非 UI 线程中。
void login(String username, String token) {
String jsonBody = "{ username: \"$username\", token: \"$token\"}";
Executors.newSingleThreadExecutor().execute(() -> {
Result result;
try {
result = makeLoginRequest(jsonBody);
} catch (IOException e) {
result = new Result(e);
}
Result finalResult = result;
new Handler(Looper.getMainLooper()).post(() -> updateUI(finalResult));
});
}
Result makeLoginRequest(String jsonBody) throws IOException {
URL url = new URL("https://example.com/login");
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("POST");
...
httpURLConnection.connect();
int code = httpURLConnection.getResponseCode();
if (code == 200) {
// Handle input stream ...
return new Result(bean);
} else {
return new Result(code);
}
}
Kotlin 的 Coroutines 则是以顺序的编码方式实现异步操作、同时不阻塞调用线程的简化并发处理的设计模式。
其具备如下的异步编程优势:
- 挂起线程不阻塞原线程
- 支持取消
- 通过 KTX 扩展对 Jetpack 组件更好支持
采用协程实现异步处理的将变得清晰、简洁,同时因为指定耗时逻辑运行在工作线程的缘故,无需管理线程切换可直接更新 UI。
fun login(username: String, token: String) {
val jsonBody = "{ username: \"\$username\", token: \"\$token\"}"
GlobalScope.launch(Dispatchers.Main) {
val result = try {
makeLoginRequest(jsonBody)
} catch(e: Exception) { Result(e) }
updateUI(result)
}
}
@Throws(IOException::class)
suspend fun makeLoginRequest(jsonBody: String): Result {
val url = URL("https://example.com/login")
var result: Result
withContext(Dispatchers.IO) {
val httpURLConnection = url.openConnection() as HttpURLConnection
httpURLConnection.run {
requestMethod = "POST"
...
}
httpURLConnection.connect()
val code = httpURLConnection.responseCode
result = if (code == 200) {
Result(bean)
} else {
Result(code)
}
}
return result
}
4.6 KTX
KTX
是专门为 Android 库设计的 Kotlin 扩展程序,以提供简洁易用的 Kotlin 代码。
比如使用 SharedPreferences
写入数据的话,我们会这么编码:
void updatePref(SharedPreferences sharedPreferences, boolean value) {
sharedPreferences
.edit()
.putBoolean("key", value)
.apply();
}
引入 KTX 扩展函数之后将变得更加简洁。
fun updatePref(sharedPreferences: SharedPreferences, value: Boolean) {
sharedPreferences.edit { putBoolean("key", value) }
这只是 KTX 扩展的冰山一角,还有大量好用的扩展以及 Kotlin 的优势值得大家学习和实践,比如:
- 大大简洁语法的
let
,also
等扩展函数 - 节省内存开销的
inline
函数 - 灵活丰富的
DSL
特性 - 异步获取数据的
Flow
等
5.Jetpack
![](https://s2.loli.net/2022/01/23/d7xaUFvB63gu5Dp.jpg)
Jetpack
单词的本意是火箭人,框架的 Logo 也可以看出来是个绑着火箭的 Android。Google 用它命名,含义非常明显,希望这些框架能够成为 Android 开发的助推器:助力 App 开发,体验飞速提升。
Jetpack 分为架构、UI、基础功能和特定功能等几个方面,其中架构板块是全新设计的,涵盖了 Google 花费大量精力开发的系列框架,是本章节着力讲解的方面。
架构以外的部分实际上是 AOSP
本身的一些组件进行优化之后集成到了Jetpack 体系内而已,这里不再提及。
- 架构:全新设计,框架的核心
- 以外:AOSP 本身组件的重新设计
- UI
- 基础功能
- 特定功能
![](https://s2.loli.net/2022/01/23/9VL87XaUTpmdKBf.png)
Jetpack 具备如下的优势供我们在实现某块功能的时候收腰选择:
- 提供 Android 平台的最佳实践
- 消除样板代码
- 不同版本、厂商上达到设备一致性的框架表现
- Google 官方稳定的指导、维护和持续升级
如果对 Jetpack 的背景由来感兴趣的朋友可以看我之前写的一篇文章:「从Preference组件的更迭看Jetpack的前世今生」。下面,我们选取 Jetpack 中几个典型的框架来了解和学习下它具体的优势。
5.1 View Binding
通常的话绑定布局里的 View 实例有哪些办法?又有哪些缺点?
通常做法 | 缺点 |
---|---|
findViewById() | NPE 风险、大量的绑定代码、类型转换危险 |
@ButterKnife | NPE 风险、额外的注解代码、不适用于多模块项目(APT 工具解析 Library 受限) |
KAE 插件 | NPE 风险、操作其他布局的风险、Kotlin 语言独占、已经废弃 |
AS 现在默认采用 ViewBinding
框架帮我们绑定 View。
来简单了解一下它的用法:
<!--result_profile.xml-->
<LinearLayout ... >
<TextView android:id="@+id/name" />
</LinearLayout>
ViewBinding 框架初始化之后,无需额外的绑定处理,即可直接操作 View 实例。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val binding = ResultProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.name.text = "Hello world"
}
}
原理比较简单:编译器将生成布局同名的绑定类文件,然后在初始化的时候将布局里的 Root View 和其他预设了 ID 的 View 实例缓存起来。事实上无论是上面的注解,插件还是这个框架,其本质上都是通过 findViewById 实现的 View 绑定,只是进行了封装。
ViewBinding 框架能改善通常做法的缺陷,但也并非完美。特殊情况下仍需使用通常做法,比如操作布局以外的系统 View 实例 ContentView,ActionBar 等。
优势 | 局限 |
---|---|
Null 安全:预设 ID 的 View 才会被缓存,否则无法通过 ViewBinding 使用,在编译阶段就阻止了 NPE 的可能 | 绑定布局以外的 View 仍需借助 findViewById |
类型安全:ViewBinding 缓存 View 实例的时候已经处理了匹配的类型 | 依赖配置采用不同布局仍需处理 Null(比如横竖屏的布局不同) |
代码简洁:无需绑定的样板代码 | |
布局专属:不混乱、布局文件为单位的专属类 |
5.2 Data Binding
一般来说,将数据反映到 UI 上需要经过如下步骤:
- 创建 UI 布局
- 绑定布局中 View 实例
- 数据逐一更新到 View 的对应属性
而 DataBinding
框架可以免去上面的步骤 2 和 3。它需要我们在步骤 1 的布局当中就声明好数据和 UI 的关系,比如文本内容的数据来源、是否可见的逻辑条件等。
<layout ...>
<data>
<import type="android.view.View"/>
<variable
name="viewModel" type="com.example.splash.ViewModel" />
</data>
<LinearLayout ...>
<TextView
...
android:text="@{viewModel.userName}"
android:visibility="@{viewModel.age >= 18 ? View.VISIBLE : View.GONE}"/>
</LinearLayout>
</layout>
上述 DataBinding 布局展示的是当 ViewModel 的 age 属性大于 18 岁才显示文本,而文本内容来自于 ViewModel 的 userName 属性。
val binding = ResultProfileBinding.inflate(layoutInflater)
binding.viewModel = viewModel
Activity 中无需绑定和手动更新 View,像 ViewBinding 一样初始化之后指定数据来源即可,后续的 UI 展示和刷新将被自动触发。DataBinding 还有诸多妙用,大家可自行了解。
5.3 Lifecycle
监听 Activity 的生命周期并作出相应处理是 App 开发的重中之重,通常有如下两种思路。
通常思路 | 具体 | 缺点 |
---|---|---|
基础 | 直接覆写 Activity 对应的生命周期函数 | 繁琐、高耦合 |
进阶 | 利用 Application#registerLifecycleCallback 统一管理 | 回调固定、需要区分各 Activity、逻辑侵入到 Application |
而 Lifecycle
框架则可以高效管理生命周期。
使用 Lifecycle 框架需要先定义一个生命周期的观察者 LifecycleObserver
,给生命周期相关处理添加上 OnLifecycleEvent
注解,并指定对应的生命状态。比如 onCreate
的时候执行初始化,onStart
的时候开始连接,onPause
的时候断开连接。
class MyLifecycleObserver(
private val lifecycle: Lifecycle
) : LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun init() {
enabled = checkStatus()
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
if (enabled) {
connect()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun stop() {
if (connected) {
disconnect()
}
}
}
然后在对应的 Activity 里添加观察:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle) {
...
MyLifecycleObserver(lifecycle).also { lifecycle.addObserver(it) }
}
}
Lifecycle 的简单例子可以看出生命周期的管理变得很清晰,同时能和 Activity 的代码解耦。
继续看上面的小例子:假使初始化操作 init() 是异步耗时操作怎么办?
init 异步的话,onStart 状态回调的时候 init 可能没有执行完毕,这时候 start 的连接处理 connect 可能被跳过。这时候 Lifecycle 提供的 State
机制就可以派上用场了。
使用很简单,在异步初始化回调的时候再次执行一下开始链接的处理,但需要加上 STARTED
的 State 条件。这样既可以保证 onStart 时跳过连接之后能手动执行连接,还能保证只有在 Activity 处于 STARTED 及以后的状态下才执行连接。
class MyLifecycleObserver(...) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun init() {
checkStatus { result ->
if (result) {
enable()
}
}
}
fun enable() {
enabled = true
// 初始化完毕的时候确保只有在 STARTED 及以后的状态下执行连接
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
if (!connected) {
connect()
}
}
}
...
}
5.4 Live Data
LiveData
是一种新型的可观察的数据存储框架,比如下面的使用示例,数据的封装和发射非常便捷:
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
// 将请求到的数据发射出去
value = price
}
// 画面活动状态下才请求
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
// 非活动状态下移除请求
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 注册观察
StockLiveData("Tesla").run { observe(this@MainActivity, Observer { ... })}
}
}
支持异步传递数据以外,LiveData 还有很多优势:
- 与
Lifecycle
框架深度绑定 - 具有生命周期感知能力,数据不会发射给非活动状态的观察者
- 观察者销毁了自动释放数据,避免内存泄露
- 支持
Room
、Retrofit
框架 - 支持合并多个数据源统一观察的
MediatorLiveData
(省去多个 LiveData 多次 observe 的丑陋处理))
但必须要说 LiveData 的定位和使用有这样那样的问题,官方的态度也一直在变,了解之后多使用 Flow 来完成异步的数据提供。
5.5 Room
Android 上开发数据库有哪些痛点?
- 需要实现 SQLite 相关的 Helper 实例并实装初始化和 CRUD 等命令
- 自行处理异步操作
- Cursor实例需要小心处理
- 字段对应关系
- index 对齐
- 关闭
官方推出的 Room
是在 SQLite 上提供了一个抽象层,通过注解简化数据库的开发。以便在充分利用 SQLite 的强大功能的同时,能够高效地访问数据库。
![](https://developer.android.google.cn/images/training/data-storage/room_architecture.png?hl=zh-cn)
需要定义 Entity,Dao 以及 Database 三块即可完成数据库的配置,其他的数据库实现交由框架即可。
@Entity
class Movie() : BaseObservable() {
@PrimaryKey(autoGenerate = true)
var id = 0
@ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
lateinit var name: String
...
}
@Dao
interface MovieDao {
@Insert
fun insert(vararg movies: Movie?): LongArray?
@Delete
fun delete(movie: Movie?): Int
@Update
fun update(vararg movies: Movie?): Int
@get:Query("SELECT * FROM movie")
val allMovies: LiveData<List<Movie?>?>
}
@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
abstract fun movieDao(): MovieDao
companion object {
@Volatile
private var sInstance: MovieDataBase? = null
private const val DATA_BASE_NAME = "jetpack_movie.db"
@JvmStatic
fun getInstance(context: Context): MovieDataBase? {
if (sInstance == null) {
synchronized(MovieDataBase::class.java) {
if (sInstance == null) {
sInstance = createInstance(context)
}
}
}
return sInstance
}
private fun createInstance(context: Context): MovieDataBase {
return Room.databaseBuilder(context.applicationContext,
MovieDataBase::class.java, DATA_BASE_NAME).build()
}
}
}
在 ViewModel 初始化 DataBase 接口之后即可利用其提供的 DAO 接口执行操作,接着利用 LiveData 将数据发射到 UI。
class MovieViewModel(application: Application) : AndroidViewModel(application) {
private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
private val db: MovieDataBase?
init {
db = MovieDataBase.getInstance(application)
if (db != null) {
mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
if (db.databaseCreated.value != null) {
mediatorLiveData.postValue(movieList)
}
}
};
}
fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
if (owner != null && observer != null)
mediatorLiveData.observe(owner, observer)
}
}
Room 具备很多优势值得选作数据库的开发首选:
- 简洁高效,通过简单注解即可完成数据库的创建和 CRUD 封装
- 直接返回目标 POJO 实例,避免自行处理 Cursor 的风险
- 支持事务处理、数据库迁移、关系数据库等完整功能
- 支持 LiveData、Flow 等方式观察式查询
- AS 的 Database Inspector 可以实时查看、编辑和部署 Room 的数据库
- 内置异步处理
5.6 View Model
ViewModel
框架和 AppCompat、Lifecycle 框架一样,可谓是 Jetpack 框架最重要的几个基础框架。虽功能不仅限于此,但我们想要借此探讨一下它在数据缓存方面的作用。
通常怎么处理横竖屏切换导致的 Activity 重绘?一可以选择自生自灭,只有部分 View 存在自行恢复的处理、也可以配置 ConfigurationChange
手动复原重要的状态、或者保存数据至 BundleState
,在 onCreate 等时机去手动恢复。
得益于 ViewModel 实例在 Activity 重绘之后不销毁,其缓存的数据不受外部配置变化的影响,进而确保数据可以自动恢复数据,无需处理。
![](https://developer.android.google.cn/images/topic/libraries/architecture/viewmodel-lifecycle.png?hl=zh-cn)
这里定义一个 ViewModel,其中提供一个获取数据的方法,用来返回一个 30 岁名叫 Ellison 的朋友。Activity 取得 vm 实例之后观察数据的变化,并将数据反映到 UI 上。当屏幕方向变化后,名字和年龄的 TextView 可自动恢复,无需额外处理。
class PersonContextModel(application: Application) : AndroidViewModel(application) {
val personLiveData = MutableLiveData<Person>()
val personInWork: Unit
get() {
val testPerson = Person(30, "Ellison")
personLiveData.postValue(testPerson)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val model = ViewModelProvider(this).get(
PersonContextModel::class.java
)
model.personLiveData.observe(this, Observer { person: Person ->
binding.name.setText(person.name)
binding.age.setText(person.age.toString())
})
binding.get.setOnClickListener({ view -> model.personInWork })
}
}
ViewModel 的众多优势:
- 基于 Lifecycle 实现以注重生命周期的方式存储和管理界面相关的数据
- 画面销毁前存储 vm 实例并在重建后恢复,让数据可在发生屏幕旋转等配置更改后继续留存
- 可用于 Fragment 之间共享数据
- 作为数据和 UI 交互的媒介,用作 MVVM 架构的 VM 层
- 。。。
5.7 CameraX
完成一个相机预览的功能,使用 Camera2
的话需要如下诸多流程,会比较繁琐:
![](https://z3.ax1x.com/2021/05/13/g0XeTf.png)
而采用 CameraX 进行开发的话,几十行代码即可完成预览功能。
private void setupCamera(PreviewView previewView) {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
mCameraProvider = cameraProviderFuture.get();
bindPreview(mCameraProvider, previewView);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(this));
}
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView) {
mPreview = new Preview.Builder().build();
mCamera = cameraProvider.bindToLifecycle(this,
CameraSelector.DEFAULT_BACK_CAMERA, mPreview);
mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}
![](https://developer.android.google.cn/images/training/camera/camerax-architecture.png)
上面是 CameraX 的架构,可以看到其底层仍然是 Camera2,外加高度封装的接口,以及 Vendor 自定义的功能库。
使用它来作为全新的相机使用框架,具备很多优势:
- 代码简单,易用
- 自动绑定 Lifecycle,自动确定打开相机、何时创建拍摄会话以及何时停止和关闭
- 多设备的相机开发体验统一:国内外主流平台的设备都支持,国内的华米 OV 都在对这个框架支持和贡献
- 完美支持人像、HDR、夜间和美颜模式等拍摄模式的 Extensions
Monzo 利用 CameraX 缩减了 9,000 多行代码并使注册流程中的访问者流失率降低了 5 倍
这是一家银行服务公司并提供了同名应用,仅在移动设备上提供数字金融服务。他们的使命是向每个人传授生财之道。为了完成新客户注册,Monzo 应用会拍摄身份证明文件(例如护照、驾照或身份证)的图片,并拍摄自拍视频来证明身份证明文件属于申请者。
早期版本使用的是 camera2 API。在某些设备上会随机发生崩溃和异常行为,这导致 25% 的潜在客户无法继续进行身份证明拍摄和自拍视频步骤。
5.8 其他框架
篇幅有限,Jetpack 集合中还有非常多其他的优质框架等待大家的挖掘。
框架 | 作用 | 竞品 |
---|---|---|
DataStore | 异步、一致性的轻量级数据的存储框架,支持键值对和对象数据 | SharedPreferences、MMKV |
StartUp | 简化应用启动的组件初始化,提高应用启动性能的框架 | - |
Navigation | 简化画面跳转,支持标签导航、抽屉导航等复杂设计的路由框架 | ARouter |
ActivityResult | Activity、Fragment 之间传递数据的新框架 | onActivityResult/Intent |
Paging3 | 按需加载节省网络流量和内存消耗的分页加载框架 | - |
WorkManager | 调度退出应用或重启设备后仍可运行的可延期异步任务框架。 | JobService、Alarm、Broadcast |
Hilt | Android 专用的DI框架,快速建立之间的依赖关系和生命周期 | Dagger2、Koil |
AppCompat | 提供Activity、Dialog 和 View 的 Base 类,兼容 Jetpack 的大量处理 | - |
ViewPager2 | 实现经典的标签导航设计的新框架 | ViewPager |
… |
在开发某个功能的时候,看看是否有轮子可用,尤其是官方的。
5.9 官方推荐的应用架构
我在官方的推荐架构上做了些补充,一般的 App 推荐采用如下的架构组件。
![](https://s2.loli.net/2022/01/23/KrE9oiwYp3UZQz2.jpg)
- 尝试单 Activity 多 Fragment 的 UI 架构
- 通过
Navigation
导航 ViewModel
完成数据和 UI 交互LiveData
观察数据Room
和DataStore
负责本地数据Retrofit
负责网络数据- 整体通过
Hilt
注入依赖
架构绝非固定模式,依实际需求和最佳实践自由搭配~
6.Jetpack Compose
![](https://s2.loli.net/2022/01/23/kS8oMV6uIbvyJdH.png)
Jetpack Compose 是 Google 耗费五年倾力打造,用于构建 Android 原生界面的全新 UI 工具包。Android 诞生多年,UI 体系早已成熟,为什么这么要重造一个轮子?🤔
原因:
- XML 布局冗长、繁琐:遇到复杂的布局,把屏幕竖过来都看不全
- View 编程方式的嵌套会带来性能影响:不合理的布局导致测量性能翻倍
- 手动更新视图复杂、易错
- 声明性界面模型逐渐流行:这种方式可以简化 UI 的构建和更新步骤,仅执行必要的更改
其发展历程:
- 17 年立项
- 之后长达三年的内部调查和实验
- 20 年初 dev 版公开,年中 alpha 版推出
- 21 年初 beta 版发布
- 21 年 4 月全球挑战推广
- 21 年 7 月正式发布
![](https://s2.loli.net/2022/01/23/SNub6saR2HAM1Tr.png)
6.1 Compose 挑战赛
去年上半年 Google 启动了为期四周的全球 Compose 挑战赛,提供了 500 多份乐高联名积木,十几部 Pixel 手机奖品,引发数万计Android开发者尝鲜,提交作品。
- 第一周的挑战做一个宠物领养 App,我花了一个周末做了个 LovePet 并拿到了这个飘洋过海的乐高积木,在推特上提交作品截图之后还有好多老外点赞,是很不错的体验。
- 后面的挑战还有定时器 App,复刻 App 设计作品,发挥想象做个天气 App 等
![](https://s2.loli.net/2022/01/23/WJYIsg29uAvzCLp.png)
这些比赛内容其实涵盖了 Compose 所需要用到的大部分技术。Google 的大力推广也足见其决心和重视程度,日后必将成为Android平台上重要的UI编写方式,早日上车!💪
6.2 编程思想
我们通过一个展示 “Hello World” 文本的小例子,来直观感受一下 Compose 编程思想的明显差异。
![](https://developer.android.google.cn/images/jetpack/compose/tooling-decorated-preview.png)
-
传统的 UI 编程方式
我们再熟悉不过了。常见的操作是先定义一个 xml,然后通过 Activity 的 setContentView() 将 xml 放进去,之后就交给系统来加载。
<androidx.constraintlayout.widget.ConstraintLayout ...>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World" ... />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(R.layout.activity_main)
}
}
- Compose 编程方式
Compose UI 工具包则依赖 Composable
注解将展示 UI 的函数声明为可组合函数,Compose 编译器负责标记可组合函数内的组件,并进行展示。
布局的部分均需要放在该函数内交由 Compose 组合。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
SimpleComposable()
}
}
@Composable
fun SimpleComposable() {
Text("Hello World")
}
}
6.3 进阶示例
来看一下下面这个简单的动态效果,并思考一下:如果采用传统的 View 编程方式来实现,你需要多少代码量?
![](https://developer.android.google.cn/images/jetpack/compose/landing-preview-animation.gif)
采用传统的 View 方式,无非是如下的思路,布局加上逻辑少于 100 行代码不太容易实现:
- 布局:CardView + LinearLayout(ImageView + TextView)
- 代码:监听、展开和隐藏 TextView,并考虑阴影和淡出动画)
那如果采用 Compose 来实现呢?只需要 10 行即可。
- Composable 函数组合圆角组件
Card
+ 垂直布局组件Column
- Column 嵌套图片组件
Image
和动画组件AnimatedVisibility
包裹的文本组件Text
- Column 的 click 事件更新展开或隐藏的
state
,进而触发 AnimatedVisibility 的重组,刷新 Text 的展示与否
@Composable
fun JetpackCompose() {
Card {
var expanded by remember { mutableStateOf(false) }
Column(Modifier.clickable { expanded = !expanded }) {
Image(painterResource(R.drawable.jetpack_compose))
AnimatedVisibility(expanded) {
Text(
text = "Jetpack Compose",
style = MaterialTheme.typographt.h2,
)
}
}
}
}
6.4 优势
![](https://s2.loli.net/2022/01/23/mBt6UabJlwe9ZVr.png)
篇幅有限,事实上 Compose 具备非常多的优势,亟待大家的挖掘:
-
声明式 UI:只负责描述界面,Compose 系统负责其余工作
-
状态驱动:界面随着状态自动更新
-
高效渲染:固定测量,层级嵌套性能仍是 O(n)
-
结合 AS 的
Preview
视图可实时查看和直接交互 UI -
兼容传统 View 树编程方式,可混合使用
-
支持
Material Design
设计语言 -
拥有 Jetpack 框架的大力配合
-
基于 Kotlin,代码简洁,大量 Kotlin 专属 API
-
跨平台亦有布局:
Desktop
、Web
大家可以利用 Compose 先来实现一个新画面,或者改造一个现有画面,逐步推进 Compose 的学习和实践。但是 Compose UI 工具包目前在部分场景下的组件支持有限,比如 WebView
、CameraView
等,这些场景下仍需要配合 Android 原生的 View 方式来完成。
6.5 Sample
- 官方 Sample:
完全使用 Compose 设计的八大主流场景的 App:官方出品,专业、全面。
https://github.com/android/compose-samples
![](https://s2.loli.net/2022/01/23/pE3Pd4rRjxIqnbv.png)
-
Movie 客户端
本人使用 Compose 的大部分 UI 组件、视图切换和数据刷新重构的电影搜索 App。
https://github.com/ellisonchan/ComposeMovie -
俄罗斯方块
fun 神将自定义 Compose 组件和状态管理发挥到了极致,搭配定时器和各式动画实现,非常值得用来深入学习 Compose 技术。
https://github.com/vitaviva/compose-tetris
-
ComposeBird
本人在 fun 神的俄罗斯方块游戏的激励下使用 Compose 复刻了风靡一时的 Flappy Bird,感兴趣的也可以学习实现思路。
https://github.com/ellisonchan/ComposeBird
未来展望
![](https://s2.loli.net/2022/01/23/RjcMwYgkeS6oH7v.png)
本次介绍了 MAD 涵盖的诸多新技术,大家可以感受到 Google 在一刻不停地革新技术。从工具到语言、框架到发行方式都在进行全方位地改良,之前耕耘多年的技术说废就废,绝不手软。
究其原因,绕不开产品生命的两大角色:开发者和消费者。
- 提升开发者的开发效率
- 改善消费者的产品体验
然而新事物的出现必然伴随着旧事物的衰落,开发者该如何对待老技术、如何看待层出不穷、前途不明的新技术?光跨平台这一项,Google 和 Jetbrains 就推出了 Flutter、KMM、Compose Multiplatform 三个技术,任何人都卷不过来的。
我总结了几句四字短语,与你分享我的感受和态度:
- 不可无视,适当了解,跟上形势:保持关注,防止日后看不懂人家用了什么技术,甚至无法理解别人的代码
- 拥抱变化,勇于尝鲜,有备无患:找个感兴趣的切入点虚心学习、体会新技术的动机
- 不可依赖,了解原理,学习模仿:光使用还不够,需要深入了解其实现,确保坑来临的时候游刃有余
- 是否深入,见仁见智,自行评估:适当取舍、甚至观望,一些技术是昙花一现的
资料资源
官方资料
各类学习点的文档主页,主页和分支页面从背景、思想、API到使用方法等层面进行了充分说明。可以帮助你快速了解和掌握相关技术。
官方 Sample
Google优秀的开发者关系工程师的诚心之作,针对语言、工具和框架开发和持续维护着详尽的Sample。辅助大家学习这些技术,并进行适当地借鉴。
学习对象 | 地址 |
---|---|
Kotlin | https://github.com/MindorksOpenSource/from-java-to-kotlin |
Compose | https://github.com/android/compose-samples |
Jetpack | https://github.com/android/sunflower |
我的文章
Compose 系列:
Jetpack 系列:
- 「深度解读 Jetpack 框架的基石-AppCompat」
- 「为什么推荐使用 Jetpack CameraX?」
- 「Android 上数据库的新选择,Jetpack Room」
- 「Jetpack Hilt 有哪些改善又有哪些限制 」
- 「从Preference组件的更迭看Jetpack的前世今生」
- 「CameraX + 华为 ScanKit:二维码扫描的终极解决方案」
- 「Jetpack新成员SplashScreen:打造全新的App启动画面」
- 「Jetpack 叒一新成员 DragAndDrop 框架:大大简化拖放手势开发」
我的 Sample
内容 | 地址/名称 |
---|---|
Jetpack Demo | https://github.com/ellisonchan/JetpackDemo |
Compose Movie | https://github.com/ellisonchan/ComposeMovie |
ComposeBird | https://github.com/ellisonchan/ComposeBird |
更多推荐
所有评论(0)