2. Kotlin 基础回顾:协程与 Flow、密封类与数据类、扩展函数、委托属性、协程上下文与调度器

做离线优先的 Compose 应用,Kotlin 基础必须扎实。说实话,我见过不少项目,代码写得很漂亮,但一遇到协程泄漏或状态管理混乱就崩了。这一节我们快速过一遍最关键的知识点,都是后面实战中天天用的。

2.1 协程与 Flow —— 异步的基石

协程说白了就是 Kotlin 提供的轻量级线程。它不绑定操作系统线程,可以挂起恢复,所以开销极小。我习惯把协程看作「协作式多任务」——你主动挂起,让出执行权,别人跑完了再回来继续。这是它和线程最大的区别。

// 启动一个协程
GlobalScope.launch {
    val data = fetchData() // 挂起函数,不会阻塞线程
    println(data)
}

Flow 是协程世界里处理数据流的搭档。它像序列(Sequence)但支持异步。记得我第一次用 Flow 的时候,把它和 LiveData 搞混了。后来发现 Flow 天生就是给协程用的,冷流(cold stream)的概念也很自然:没人收集,就不发射数据。

小技巧:项目中频繁用 flow { ... } 构造器配合 emit() 发射数据。如果你需要把回调式 API 转成 Flow,试试 callbackFlow,我用它在 Room 监听中处理数据变化,非常方便。

2.2 密封类与数据类 —— 从状态到结果

密封类(sealed class)是我最爱的 Kotlin 特性之一。它让你把有限的状态或者结果类型收拢到一个类里。做离线优先时,网络请求的状态(加载中、成功、失败)最适合用密封类表示。你想想看,用 Int 常量标记状态,漏掉一个 case 编译器都不知道,但用密封类配合 when 表达式,不写 else 分支也没问题。

sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val exception: Throwable) : NetworkResult<Nothing>()
    object Loading : NetworkResult<Nothing>()
}

数据类(data class)天生就是为数据容器准备的。它自动生成 equals()、hashCode()、copy() 等方法。我在项目里用数据类配合 Room 实体,几乎零样板代码。

注意:数据类的 copy() 是浅拷贝。如果里面有 MutableList 这样的可变引用,修改副本会影响原对象。我曾经在更新 UI 状态时踩过这个坑,后来统一改用不可变集合 + copy() 替换整个字段。

2.3 扩展函数 —— 给类添砖加瓦

扩展函数不是真的修改类,而是给类新增成员函数的样子。Kotlin 编译后其实是一个静态方法,第一个参数是被扩展类的实例。我个人习惯把常用工具写成扩展函数,比如在 Context 上添加 Toast 显示、在 View 上添加 dp 转换。

fun Context.showToast(message: String) {
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

// 使用:在 Activity 中直接 showToast("保存成功")

不过要注意,扩展函数无法被重写(不是虚函数)。如果你在子类中定义了同名同签名的扩展函数,那是另一个静态方法,不会覆盖。我曾经误以为可以像继承那样处理,结果排了半天错才意识到。

2.4 委托属性 —— 偷懒的利器

委托属性让你把属性读写的逻辑交给另一个对象。Kotlin 标准库提供了 lazy、observable、vetoable 等。我最常用的是 by lazy —— 延迟初始化,第一次访问时计算值,后面直接缓存结果。

val config by lazy { loadConfig() } // 直到第一次使用 loadConfig 才会执行

by Delegates.observable 可以监听属性变化。写 MVVM 时,如果不想用 LiveData,可以用 observable 配合回调来更新 UI。不过 Compose 里更推荐状态 + 协程,委托属性在 ViewModel 层依然有用武之地。

避坑指南:我曾经在 ViewModel 里用 by lazy 初始化一个依赖类,结果因为 ViewModel 被销毁重建,那个 lazy 对象还引用旧的 ViewModel 实例,导致内存泄漏。后来改成 by lazy 时确保锁是全局的,或者直接用 hiltViewModel() 管理生命周期。

2.5 协程上下文与调度器 —— 掌握运行环境

协程上下文(CoroutineContext)是一个接口,里面包含 Job、CoroutineDispatcher、CoroutineName 等元素。调度器决定了协程在哪类线程上运行。三个核心调度器:

调度器用途示例
Dispatchers.Main主线程,适合 UI 更新Compose 启动协程默认用这个
Dispatchers.IO磁盘/网络 I/O 操作读取文件、网络请求
Dispatchers.DefaultCPU 密集型任务图像处理、排序

withContext 可以临时切换调度器。我经常用在 Repository 里:用 withContext(Dispatchers.IO) 做数据库写入,返回后自动切回原来的上下文。不过要注意,不要嵌套多层 withContext,会影响性能。

suspend fun saveUser(user: User) = withContext(Dispatchers.IO) {
    userDao.insert(user)
}
重要原则:协程的泄漏通常是由于没有取消 Job 或 CoroutineScope。在 Compose 中,使用 rememberCoroutineScope() 或 LaunchedEffect 可以自动跟随 Composable 的生命周期。不要直接使用 GlobalScope,否则协程可能存活到应用退出。

2.6 本章知识体系

我把这些基础知识点串成一张图,方便你理解它们之间的关联:协程和 Flow 是并发的骨架,密封类和数据类规范了状态和数据结构,扩展函数和委托属性让我们写更简洁的代码,上下文与调度器则控制了协程的运行时环境。把这些组合好,离线优先 Compose 应用的地基就稳了。

Kotlin 基础回顾知识体系 语言特性 密封类 有限状态集合 数据类 数据容器 扩展 & 委托 扩展函数 新增行为 委托属性 懒加载/监听 并发相关 协程 轻量级线程 Flow 异步数据流 上下文与调度器 Main/IO/Default 离线优先 Compose 应用 💡 使用流程图: 语言特性(密封类、数据类、扩展函数、委托属性)提供数据结构与代码简洁性;并发相关(协程、Flow、上下文调度器)提供异步能力。两者结合,构建离线优先的数据驱动应用。