MAD,现代安卓开发技术:Android 领域开发方式的重大变革!

25,355

「2022 年什么会火?什么该学?本文正在参与“聊聊 2022 技术趋势”征文活动 」

Android 诞生已久,其开发方式保持着高频更迭,相较于早期的开发方式已大不相同,尤其是近几年 Google 热切推崇的 MAD 开发技术。其实很多开发者已经有意或无意地正在使用这门技术,借着 2022 开年探讨技术趋势的契机,想要完整地总结 MAD 的愿景、构成、优势以及一些学习建议。

MAD,全称 Modern Android Development:是 Google 针对 Android 平台提出的全新开发技术。旨在指导我们利用官方推出的各项技术来进行高效的 App 开发。有的时候 Google 会将其翻译成现代安卓开发,有的时候又翻译成新式安卓开发,个人觉得前者的翻译虽然激进、倒也贴切。

下面按照 MAD 的构成要点逐步展开,帮助大家快速了解 MAD 的技术理念。如果大家对其中的语言、工具包或框架产生了兴趣,一定要在日后的开发中尝试和掌握。

内容前瞻

  1. 【Modern Android Development】讲述 Android 全新开发技术的由来和构成
  2. 【Android Studio】演示 Android 官方 IDE 的重要特性
  3. 【Android App Bundle】简要普及 Google 推崇的 App 新格式
  4. Kotlin】解读 Android 首推的开发语言的优点
  5. Jetpack】讲述 Android 持续更新的重大框架集合,并逐个演示重要框架解决的问题和优势
  6. Jetpack Compose】带领大家感受 Android 上 UI 开发方式的重大变革

1.Modern Android Development

官方一直在优化 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 计分插件

2.Android Studio

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

Database Inspector 可以实时查看 Jetpack Room 框架生成的数据库文件,同时也支持实时编辑和部署到设备当中。相较之前需要的 SQLite 命令或者额外导出并借助 DB 工具的方式更为高效和直观。

2.2 Layout / Motion Editor

Layout Editor 拥有诸多优点,不知大家熟练运用了没有:

  • 可以直观地编辑 UI:随意拖动视图控件和更改约束指向
  • 在不同配置(设备、主题、语言、屏幕方向等)下灵活切换预览,免去实机调试
  • 搭配 Tools 标签自由定制 UI,确保只面向调试而不影响实际逻辑。比如:布局中有上下两个控件,上面的默认为 invisible,想确认下上面的控件如果可见的话对整体布局的影响。无需更改控件的 visibility 属性,添加 Tools:visibility=true 即可预览布局的变化

Motion Editor 则是支持 MotionLayout 类型布局的视觉设计编辑器,可让更轻松地创建和预览和调试动画。

Layout Inspector 则可以查看某进程某画面的详细布局,完整展示 View 树的各项属性。在不方便代码调试或剖析其他 App 的情况下非常好用。同时已经支持直接检查 Compose 编写的 UI 布局了,喜极而泣。

2.3 Realtime Profilers

AS 的 Realtime Profilers 工具可以帮助我们在如下四个方面监测和发现问题,有的时候在没有其他 App 代码的情况下通过 Memory Profilers 还可以查看其内部的实例和变量细节。

  • CPU:性能剖析器检查 CPU 活动,切换到 Frames 视图还可以界面卡顿追踪
  • Memory:识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配以定位内存方面的问题
  • Battery:会监控 CPU、网络无线装置和 GPS 传感器的使用情况,并直观地显示其中每个组件消耗的电量,了解应用在哪里耗用了不必要的电量
  • Network:显示实时网络活动,包括发送和接收的数据以及当前的连接数。这便于您检查应用传输数据的方式和时间,并适当优化代码

2.4 APK Analyzer

Apk 的下载会耗费网络流量,安装了还会占用存储空间。其体积的大小会对 App 安装和留存产生影响,分析和优化其体积显得尤为必要。

借助 AS 的 APK Analyzer 可以帮助完成如下几项工作:

  • 快速分析 Apk 构成,包括 DEX、Resources 和 Manifest 的 Size 和占比,助力我们优化代码或资源的方向
  • Diff Apk 以了解版本的前后差异,精准定位体积变大的源头
  • 分析其他 Apk,包括查看大致的资源和分析代码逻辑,进而拆解、Bug 定位

2.5 其他特性

篇幅原因只介绍了少部分特性,其他的还有很多,需要各位自行探索:

  • 性能提升、内嵌到 AS 界面内的的 Fast Emulator
  • 实时预览和编辑 Compose 布局,并支持直接交互的 Compose Preview
  • 针对 Jetpack WorkManagerBackground Task Inspector
  • 。。。

相比之下,Google 官方的这篇「Android Studio 新特性详解」介绍得更新、更全,大家可以一看。

3.Android App Bundle

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

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 等。

下面我们选取 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 如何进行线程间通信?有何痛点?

比如:AsyncTaskHandlerHandlerThreadIntentServiceRxJavaLiveData 等。它们都有复杂易错、不简洁、回调冗余的痛点。

比如一个请求网络登录的简单场景:我们需要新建线程去请求,然后将结果通过 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

Jetpack 单词的本意是火箭人,框架的 Logo 也可以看出来是个绑着火箭的 Android。Google 用它命名,含义非常明显,希望这些框架能够成为 Android 开发的助推器:助力 App 开发,体验飞速提升。

Jetpack 分为架构、UI、基础功能和特定功能等几个方面,其中架构板块是全新设计的,涵盖了 Google 花费大量精力开发的系列框架,是本章节着力讲解的方面。

架构以外的部分实际上是 AOSP 本身的一些组件进行优化之后集成到了Jetpack 体系内而已,这里不再提及。

  • 架构:全新设计,框架的核心
  • 以外:AOSP 本身组件的重新设计
    • UI
    • 基础功能
    • 特定功能

Jetpack 具备如下的优势供我们在实现某块功能的时候收腰选择:

  • 提供 Android 平台的最佳实践
  • 消除样板代码
  • 不同版本、厂商上达到设备一致性的框架表现
  • Google 官方稳定的指导、维护和持续升级

如果对 Jetpack 的背景由来感兴趣的朋友可以看我之前写的一篇文章:「从Preference组件的更迭看Jetpack的前世今生」。下面,我们选取 Jetpack 中几个典型的框架来了解和学习下它具体的优势。

5.1 View Binding

通常的话绑定布局里的 View 实例有哪些办法?又有哪些缺点?

通常做法缺点
findViewById()NPE 风险、大量的绑定代码、类型转换危险
@ButterKnifeNPE 风险、额外的注解代码、不适用于多模块项目(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 上需要经过如下步骤:

  1. 创建 UI 布局
  2. 绑定布局中 View 实例
  3. 数据逐一更新到 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 框架深度绑定
  • 具有生命周期感知能力,数据不会发射给非活动状态的观察者
  • 观察者销毁了自动释放数据,避免内存泄露
  • 支持 RoomRetrofit 框架
  • 支持合并多个数据源统一观察的 MediatorLiveData(省去多个 LiveData 多次 observe 的丑陋处理))

但必须要说 LiveData 的定位和使用有这样那样的问题,官方的态度也一直在变,了解之后多使用 Flow 来完成异步的数据提供。

5.5 Room

Android 上开发数据库有哪些痛点?

  • 需要实现 SQLite 相关的 Helper 实例并实装初始化和 CRUD 等命令
  • 自行处理异步操作
  • Cursor实例需要小心处理
    • 字段对应关系
    • index 对齐
    • 关闭

官方推出的 Room 是在 SQLite 上提供了一个抽象层,通过注解简化数据库的开发。以便在充分利用 SQLite 的强大功能的同时,能够高效地访问数据库。

需要定义 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 重绘之后不销毁,其缓存的数据不受外部配置变化的影响,进而确保数据可以自动恢复数据,无需处理。

这里定义一个 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 的话需要如下诸多流程,会比较繁琐:

而采用 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());
    }

上面是 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
ActivityResultActivity、Fragment 之间传递数据的新框架onActivityResult/Intent
Paging3按需加载节省网络流量和内存消耗的分页加载框架-
WorkManager调度退出应用或重启设备后仍可运行的可延期异步任务框架。JobService、Alarm、Broadcast
HiltAndroid 专用的DI框架,快速建立之间的依赖关系和生命周期Dagger2、Koil
AppCompat提供Activity、Dialog 和 View 的 Base 类,兼容 Jetpack 的大量处理-
ViewPager2实现经典的标签导航设计的新框架ViewPager
...

在开发某个功能的时候,看看是否有轮子可用,尤其是官方的。

5.9 官方推荐的应用架构

我在官方的推荐架构上做了些补充,一般的 App 推荐采用如下的架构组件。

  • 尝试单 Activity 多 Fragment 的 UI 架构
  • 通过 Navigation 导航
  • ViewModel 完成数据和 UI 交互
  • LiveData 观察数据
  • RoomDataStore 负责本地数据
  • Retrofit 负责网络数据
  • 整体通过 Hilt 注入依赖

架构绝非固定模式,依实际需求和最佳实践自由搭配~

6.Jetpack Compose

Jetpack Compose 是 Google 耗费五年倾力打造,用于构建 Android 原生界面的全新 UI 工具包。Android 诞生多年,UI 体系早已成熟,为什么这么要重造一个轮子?🤔

原因:

  • XML 布局冗长、繁琐:遇到复杂的布局,把屏幕竖过来都看不全
  • View 编程方式的嵌套会带来性能影响:不合理的布局导致测量性能翻倍
  • 手动更新视图复杂、易错
  • 声明性界面模型逐渐流行:这种方式可以简化 UI 的构建和更新步骤,仅执行必要的更改

其发展历程:

  1. 17 年立项
  2. 之后长达三年的内部调查和实验
  3. 20 年初 dev 版公开,年中 alpha 版推出
  4. 21 年初 beta 版发布
  5. 21 年 4 月全球挑战推广
  6. 21 年 7 月正式发布

6.1 Compose 挑战赛

去年上半年 Google 启动了为期四周的全球 Compose 挑战赛,提供了 500 多份乐高联名积木,十几部 Pixel 手机奖品,引发数万计Android开发者尝鲜,提交作品。

  • 第一周的挑战做一个宠物领养 App,我花了一个周末做了个 LovePet 并拿到了这个飘洋过海的乐高积木,在推特上提交作品截图之后还有好多老外点赞,是很不错的体验。
  • 后面的挑战还有定时器 App,复刻 App 设计作品,发挥想象做个天气 App 等

这些比赛内容其实涵盖了 Compose 所需要用到的大部分技术。Google 的大力推广也足见其决心和重视程度,日后必将成为Android平台上重要的UI编写方式,早日上车!💪

6.2 编程思想

我们通过一个展示 “Hello World” 文本的小例子,来直观感受一下 Compose 编程思想的明显差异。

  • 传统的 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 编程方式来实现,你需要多少代码量?

采用传统的 View 方式,无非是如下的思路,布局加上逻辑少于 100 行代码不太容易实现:

  • 布局:CardView + LinearLayout(ImageView + TextView)
  • 代码:监听、展开和隐藏 TextView,并考虑阴影和淡出动画)

那如果采用 Compose 来实现呢?只需要 10 行即可。

  1. Composable 函数组合圆角组件 Card + 垂直布局组件 Column
  2. Column 嵌套图片组件 Image 和动画组件 AnimatedVisibility 包裹的文本组件 Text
  3. 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 优势

篇幅有限,事实上 Compose 具备非常多的优势,亟待大家的挖掘:

  • 声明式 UI:只负责描述界面,Compose 系统负责其余工作

  • 状态驱动:界面随着状态自动更新

  • 高效渲染:固定测量,层级嵌套性能仍是 O(n)

  • 结合 AS 的 Preview 视图可实时查看和直接交互 UI

  • 兼容传统 View 树编程方式,可混合使用

  • 支持 Material Design 设计语言

  • 拥有 Jetpack 框架的大力配合

  • 基于 Kotlin,代码简洁,大量 Kotlin 专属 API

  • 跨平台亦有布局:DesktopWeb

大家可以利用 Compose 先来实现一个新画面,或者改造一个现有画面,逐步推进 Compose 的学习和实践。但是 Compose UI 工具包目前在部分场景下的组件支持有限,比如 WebViewCameraView 等,这些场景下仍需要配合 Android 原生的 View 方式来完成。

6.5 Sample

  • 官方 Sample

    完全使用 Compose 设计的八大主流场景的 App:官方出品,专业、全面。 github.com/android/com…

  • 俄罗斯方块 fun 神将自定义 Compose 组件和状态管理发挥到了极致,搭配定时器和各式动画实现,非常值得用来深入学习 Compose 技术。 github.com/vitaviva/co…
  • ComposeBird 本人在 fun 神的俄罗斯方块游戏的激励下使用 Compose 复刻了风靡一时的 Flappy Bird,感兴趣的也可以学习实现思路。 github.com/ellisonchan…

未来展望

本次介绍了 MAD 涵盖的诸多新技术,大家可以感受到 Google 在一刻不停地革新技术。从工具到语言、框架到发行方式都在进行全方位地改良,之前耕耘多年的技术说废就废,绝不手软。

究其原因,绕不开产品生命的两大角色:开发者和消费者。

  • 提升开发者的开发效率
  • 改善消费者的产品体验

然而新事物的出现必然伴随着旧事物的衰落,开发者该如何对待老技术、如何看待层出不穷、前途不明的新技术?光跨平台这一项,Google 和 Jetbrains 就推出了 Flutter、KMM、Compose Multiplatform 三个技术,任何人都卷不过来的。

我总结了几句四字短语,与你分享我的感受和态度:

  • 不可无视,适当了解,跟上形势:保持关注,防止日后看不懂人家用了什么技术,甚至无法理解别人的代码
  • 拥抱变化,勇于尝鲜,有备无患:找个感兴趣的切入点虚心学习、体会新技术的动机
  • 不可依赖,了解原理,学习模仿:光使用还不够,需要深入了解其实现,确保坑来临的时候游刃有余
  • 是否深入,见仁见智,自行评估:适当取舍、甚至观望,一些技术是昙花一现的

资料资源

官方资料

各类学习点的文档主页,主页和分支页面从背景、思想、API到使用方法等层面进行了充分说明。可以帮助你快速了解和掌握相关技术。

技术地址
MADdeveloper.android.google.cn/modern-andr…
ASdeveloper.android.google.cn/studio
AAPdeveloper.android.google.cn/platform/te…
Kotlinkotlinlang.org/developer.android.google.cn/kotlin
Jetpackdeveloper.android.google.cn/jetpack
Composedeveloper.android.google.cn/jetpack/com…

官方 Sample

Google优秀的开发者关系工程师的诚心之作,针对语言、工具和框架开发和持续维护着详尽的Sample。辅助大家学习这些技术,并进行适当地借鉴。

学习对象地址
Kotlingithub.com/MindorksOpe…
Composegithub.com/android/com…
Jetpackgithub.com/android/sun…

我的文章

Compose 系列:

Jetpack 系列:

我的 Sample

内容地址/名称
Jetpack Demogithub.com/ellisonchan…
ComposeBirdgithub.com/ellisonchan…