下面我们选取 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 如何进行线程间通信?有何痛点?
比如: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 等
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(比如横竖屏的布局不同)代码简洁:无需绑定的样板代码
布局专属:不混乱、布局文件为单位的专属类
一般来说,将数据反映到 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 RoomAndroid 上开发数据库有哪些痛点?
- 需要实现 SQLite 相关的 Helper 实例并实装初始化和 CRUD 等命令
- 自行处理异步操作
- Cursor实例需要小心处理
- 字段对应关系
- index 对齐
- 关闭
官方推出的 Room 是在 SQLite 上提供了一个抽象层,通过注解简化数据库的开发。以便在充分利用 SQLite 的强大功能的同时,能够高效地访问数据库。