前言
本文从 Kotlin 的基础语法开始,按知识实际推进顺序依次展开变量、基本类型、控制语句、函数、空安全、类与对象、集合、Lambda、高阶函数,再一路过渡到协程与 Flow。这样组织的目的,不是把概念拆成孤立词条,而是让前面的语言基础能够自然支撑后面的异步编程与数据流处理。
前半部分重点解决类型系统、表达式写法、集合操作和对象建模的问题,后半部分则把协程作用域、构建器、调度器、挂起函数和 Flow 常见场景串起来。读完之后,可以形成一条比较完整的 Kotlin 学习路径:先把语法和类型系统站稳,再进入函数式写法,最后理解现代 Android 开发里最常见的并发模型。
目录
- [Kotlin 从基础语法到协程与 Flow:按知识推进顺序串起类型系统、函数式写法与异步编程](#Kotlin 从基础语法到协程与 Flow:按知识推进顺序串起类型系统、函数式写法与异步编程)
- [1. 基本概述](#1. 基本概述)
- [1.1 包的定义与导入](#1.1 包的定义与导入)
- [1.2 程序入口点](#1.2 程序入口点)
- [1.2.1 输入](#1.2.1 输入)
- [1.3 变量](#1.3 变量)
- [2. 基本类型](#2. 基本类型)
- [2.1 数字](#2.1 数字)
- [2.1.1 整数类型](#2.1.1 整数类型)
- [2.1.2 浮点类型](#2.1.2 浮点类型)
- [2.1.3 数字字面常量](#2.1.3 数字字面常量)
- [2.1.4 装箱与缓存](#2.1.4 装箱与缓存)
- [2.1.5 显式数字转换](#2.1.5 显式数字转换)
- [2.2 布尔](#2.2 布尔)
- [2.3 字符](#2.3 字符)
- [2.3.1 特殊字符](#2.3.1 特殊字符)
- [2.4 字符串](#2.4 字符串)
- [2.4.1 基础特性](#2.4.1 基础特性)
- [2.4.2 常用操作](#2.4.2 常用操作)
- [2.4.3 扩展函数增强](#2.4.3 扩展函数增强)
- [2.4.4 性能优化技巧](#2.4.4 性能优化技巧)
- [2.4.5 与Java互操作](#2.4.5 与Java互操作)
- [3. 控制语句](#3. 控制语句)
- [3.1 条件语句(if 与 when)](#3.1 条件语句(if 与 when))
- [3.2 循环语句](#3.2 循环语句)
- [3.2.1 遍历范围(Range)](#3.2.1 遍历范围(Range))
- [3.2.2 遍历数组](#3.2.2 遍历数组)
- [3.2.3 遍历集合(List/Set)](#3.2.3 遍历集合(List/Set))
- [3.2.4 遍历 Map](#3.2.4 遍历 Map)
- [3.2.5 遍历字符串字符](#3.2.5 遍历字符串字符)
- [3.2.6 自定义迭代器](#3.2.6 自定义迭代器)
- [3.2.7 控制流操作](#3.2.7 控制流操作)
- [3.2.8 标签控制](#3.2.8 标签控制)
- [3.2.9 与 Java 对比](#3.2.9 与 Java 对比)
- [4. 函数](#4. 函数)
- [4.1 函数的定义](#4.1 函数的定义)
- [4.2 扩展函数](#4.2 扩展函数)
- [4.2.1 使用案例](#4.2.1 使用案例)
- [5. 空安全机制](#5. 空安全机制)
- [5.1 问号(?)的作用与用法](#5.1 问号(?)的作用与用法)
- [5.2 为什么需要这些符号?](#5.2 为什么需要这些符号?)
- [5.3 最佳实践示例](#5.3 最佳实践示例)
- [5.4 关键总结](#5.4 关键总结)
- [6. 类与对象](#6. 类与对象)
- [6.1 定义与使用](#6.1 定义与使用)
- [6.2 继承](#6.2 继承)
- [6.3 抽象](#6.3 抽象)
- [6.4 接口](#6.4 接口)
- [6.5 数据类](#6.5 数据类)
- [6.6 伴生对象](#6.6 伴生对象)
- [6.7 可见性修饰符](#6.7 可见性修饰符)
- [6.7.1 不同修饰符的区别](#6.7.1 不同修饰符的区别)
- [6.8 与 Java 的主要区别](#6.8 与 Java 的主要区别)
- [6.9 Kotlin vs Java 对比表](#6.9 Kotlin vs Java 对比表)
- [7. 列表与集合](#7. 列表与集合)
- [7.1 用法](#7.1 用法)
- [7.1.1 创建集合](#7.1.1 创建集合)
- [7.1.2 高频操作](#7.1.2 高频操作)
- [7.1.3 常见用法和技巧](#7.1.3 常见用法和技巧)
- [8. Lambda表达式](#8. Lambda表达式)
- [8.1 语法定义](#8.1 语法定义)
- [8.2 常见用法](#8.2 常见用法)
- [8.2.1 事件监听(取代 Java 匿名内部类)](#8.2.1 事件监听(取代 Java 匿名内部类))
- [8.2.2 集合操作(链式调用 + Lambda)](#8.2.2 集合操作(链式调用 + Lambda))
- [8.2.3 作用域函数(let, apply, also)](#8.2.3 作用域函数(let, apply, also))
- [8.2.4 关键注意事项](#8.2.4 关键注意事项)
- [9. 高阶函数](#9. 高阶函数)
- [9.1 语法](#9.1 语法)
- [9.2 使用案例](#9.2 使用案例)
- [9.2.1 带返回值的函数参数](#9.2.1 带返回值的函数参数)
- [9.2.2 返回函数的函数](#9.2.2 返回函数的函数)
- [10. 协程](#10. 协程)
- [10.1 为什么需要协程?](#10.1 为什么需要协程?)
- [10.2 协程与线程的关系](#10.2 协程与线程的关系)
- [10.3 协程的核心组件](#10.3 协程的核心组件)
- [10.3.1 协程作用域(CoroutineScope)](#10.3.1 协程作用域(CoroutineScope))
- [10.3.2 协程构建器(Builders)](#10.3.2 协程构建器(Builders))
- [10.3.3 调度器(Dispatchers)](#10.3.3 调度器(Dispatchers))
- [10.3.4 挂起函数(Suspend Functions)](#10.3.4 挂起函数(Suspend Functions))
- [10.3.5 协程上下文(CoroutineContext)](#10.3.5 协程上下文(CoroutineContext))
- [10.3.6 协程任务(Job)](#10.3.6 协程任务(Job))
- [10.4 常见用法场景](#10.4 常见用法场景)
- [10.4.1 网络请求 + UI 更新](#10.4.1 网络请求 + UI 更新)
- [10.4.2 并发执行多任务](#10.4.2 并发执行多任务)
- [10.4.3 超时控制](#10.4.3 超时控制)
- [10.4.4 异常统一处理](#10.4.4 异常统一处理)
- [10.5 Flow(流)](#10.5 Flow(流))
- [10.5.1 flow的设计规则](#10.5.1 flow的设计规则)
- [10.5.2 flow的基本用法](#10.5.2 flow的基本用法)
- [10.5.3 flow操作符](#10.5.3 flow操作符)
- [10.5.4 flow的常见用法](#10.5.4 flow的常见用法)
- [10.5.5 网络请求分页加载](#10.5.5 网络请求分页加载)
- [10.5.6 数据库实时监听](#10.5.6 数据库实时监听)
- [11. 小结](#11. 小结)
1. 基本概述
1.1 包的定义与导入
包的声明应处于源文件顶部:
kotlin
package my.demo //包名
1.2 程序入口点
Kotlin 应用程序的入口点是 main 函数:
kotlin
fun main() {
println("Hello world!")
}
main 的另一种形式接受可变数量的 String 参数,当程序需要处理命令行参数时可以使用这个形式,感兴趣可自行扩展(如读取文件名、配置选项等):
kotlin
fun main(args: Array<String>) {
println(args.contentToString())
}
1.2.1 输入
先输出一段提示语,引导用户开始输入内容:
kotlin
println("请输入: ")
接着使用一个不可变的 yourWord 变量读取并保存输入结果:
kotlin
val yourWord = readln()
最后把刚才输入的内容原样输出出来,形成一个完整的输入输出闭环:
kotlin
print("你输入了: ")
print(yourWord)
1.3 变量
kotlin具有强大的类型推断,我们可以让编译器通过推断变量的类型来完成工作。
声明一个可变变量并初始化它
kotlin
var a: String = "initial"
println(a)
声明一个不可变变量并初始化它。
kotlin
val b: Int = 1
声明不可变变量并在不指定类型的情况下对其进行初始化。编译器推断Int类型。
kotlin
val c = 3
变量必须初始化后才可以使用,否则会报错,只要在第一次使用它之前做初始化就可以
声明一个没有初始化的变量。
kotlin
var e: Int
尝试使用该变量会导致编译器错误:变量'e'必须初始化。
kotlin
println(e)
初始化后再用
kotlin
e = 666
2. 基本类型
Kotlin 中使用的类型:数字、布尔、字符、字符串、数组。
2.1 数字
2.1.1 整数类型
Kotlin 提供了一组表示数字的内置类型。 对于整数,有四种不同大小与取值范围的类型:
| 类型 | 大小(比特数) | 最小值 | 最大值 |
|---|---|---|---|
| Byte | 8 | -128 | 127 |
| Short | 16 | -32768 | 32767 |
| Int | 32 | -2,147,483,648 | 2,147,483,647 |
| Long | 64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
当初始化一个没有显式指定类型的变量时,编译器会自动推断为自 Int 起足以表示该值的最小类型。 如果不超过 Int 的取值范围,那么类型是 Int。 如果超出了该范围,那么类型是 Long。 如需显式指定 Long 值,请给该值追加后缀 L。 显式指定类型会触发编译器检测该值不会超出指定类型的取值范围。
kotlin
val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1
2.1.2 浮点类型
对于实数,Kotlin 提供了浮点类型 Float单精度与 Double双精度两种类型
| 类型 | 大小(比特数) | 10进制位数 | 最小值 | 最大值 |
|---|---|---|---|---|
| Float | 32 | 6-7 | 3.4028235 × 10³⁸ | 1.7976931348623157 × 10³⁰⁸ |
| Double | 64 | 15-16 | 1.4 × 10⁻⁴⁵ | 4.9 × 10⁻³²⁴ |
对于以小数初始化的变量,编译器会推断为 Double 类型:
kotlin
val pi = 3.14 // Double
val one: Double = 1 //Double
val oneDouble = 1.0 // Double
如需将一个值显式指定为 Float 类型,请添加 f 或 F 后缀。 如果以这种方式提供的值包含多于 7 位十进制数,那么会将其舍入:
kotlin
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float,实际值为 2.7182817
与一些其他语言不同,Kotlin 中的数字没有隐式拓宽转换。 例如,具有 Double 参数的函数只能对 Double 值调用,而不能对 Float、 Int 或者其他数字值调用:
kotlin
fun main() {
sampleStart
kotlin
fun printDouble(x: Double) { print(x) }
val x = 1.0
val xInt = 1
val xFloat = 1.0f
printDouble(x)//参数类型匹配
printDouble(xInt) // 参数类型不匹配
printDouble(xFloat) // 参数类型不匹配
//如果需要让xInt或者xFloat使用这个函数,需要先进行显式类型转换
xInt.toDouble()
}
2.1.3 数字字面常量
数值常量(直接在代码中表示固定值的符号)字面值有以下几种:
-
十进制:123
-
Long类型,以大写 L 结尾:123L -
十六进制:0x0F
-
二进制:0b00001011
Kotlin不支持八进制。
Kotlin 同样支持浮点数的常规表示方法:
-
Doubles (default
whenthe fractional part does not endwitha letter): 123.5, 123.5e10 -
Float,以字母 f 或 F 结尾:123.5f
你可以使用下划线使数字常量更易读:
kotlin
val oneMillion = 1_000_000 //实际值仍然是 1000000(一百万),下划线 _ 作为视觉分隔符,使数字更易读.在编译阶段会被完全忽略,不影响实际值
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010
val bigFractional = 1_234_567.7182818284
2.1.4 装箱与缓存
JVM是怎么存储数字的?
-
基本类型:int 、double 等(直接存数值,高效)
-
装箱类型:Integer 、
Double等(存对象,用于特殊场景)
-
使用可空类型的时候会触发装箱操作:
-
Int? 中的问号(?)表示可空类型(Nullable Type),这是Kotlin类型系统中最重要的特性之一 -
Kotlin通过问号语法,强制开发者显式处理 null 的情况,避免 NullPointerException
kotlin
val basic: Int = 10 // 基本类型(高效) 不可为 null 的整数(等价于 Java 的 int 基本类型)
val boxed: Int? = 10 // 装箱类型(对象) 可以为 null 的整数(等价于 Java 的 Integer 包装类,但更安全)
JVM 对 -128 到 127 的小数字做了特殊优化:
-
这些数字装箱后会共用同一个缓存对象
-
超出范围的数字每次装箱都会创建新对象
假如是1个127以内的数,jvm会对其进行缓存,哪怕装箱以后,也都会指向同一个对象
kotlin
val b: Int = 100
val boxedB: Int? = b // 装箱
val anotherBoxedB: Int? = b // 再次装箱
println(boxedB === anotherBoxedB) // 因为100没超出缓存范围,两个引用指向同一个对象 输出:true
println(boxedB == anotherBoxedB) // == 比较的是数值内容,两者都是100 输出:true
但如果b是的范围不属于 -128 到 127,那么就不再是同一个对象
kotlin
val b: Int = 10000
println(boxedB === anotherBoxedB) // 因为10000超出缓存范围,每次装箱都创建新对象 输出:false
-
注意:这里只是借助
Int?可空类型以及===来说明装箱后的缓存现象: -
除非必要,不要用
Int?,一定是优先使用基本类型 -
始终使用
==比较数字,而不是使用===比较内存地址,因为数字比较更关心值本身是否相等
2.1.5 显式数字转换
- 较小的类型不会隐式地转换为较大的类型,反之亦然。例如,将Byte类型的值赋给Int变量需要显式转换
kotlin
fun main() {
val byte: Byte = 1
val intAssignedByte: Int = byte //会报错
val intConvertedByte: Int = byte.toInt()//需要显示转换
println(intConvertedByte)
}
所有数字类型都支持转换为其他类型,只需要调用数字下的 toxxx函数方法:toByte()、toShort()、toInt()、toLong()、toFloat()、toDouble()。
也有很多不需要显式类型转换的情况,因为程序会根据上下文推断类型, 而且算术运算符也会有重载自动处理类型转换。例如:
kotlin
fun main() {
// 场景1:根据赋值自动推断
val name = "Kotlin" // 自动推断为 String
val score = 95 // 自动推断为 Int
val ratio = 0.9 // 自动推断为 Double
//场景2:算术运算符重载处理
val l = 1L + 3 // Long + Int => 得到的是Long
println(l is Long) // 使用is判断l的类型 输出:true
}
Kotlin 拒绝隐式转换
Kotlin 刻意设计 为不支持隐式类型转换(Implicit Conversions),这与 Java、C++ 等语言不同。以下是关键原因和典型案例:
案例1:整数与浮点数
kotlin
val price: Double = 10 // 编译错误!必须显式转换
正确做法:
kotlin
val correctPrice: Double = 10.toDouble()
问题根源:
10 是 Int 类型,直接赋值给 Double 会导致精度意外丢失 的风险。Kotlin 强制要求开发者明确转换意图。
对比 Java 的隐式转换:
java
double price = 10; // Java 自动隐式转换(可能掩盖潜在问题)
案例2:数字超范围
kotlin
val bigNumber: Long = 10000000000 // 编译错误!超出 Int 范围
必须显式声明为 Long:
kotlin
val correctNumber = 10000000000L
设计哲学 :Kotlin 要求开发者明确知晓自己在处理大数字,避免数值溢出等隐蔽问题。
数字运算
Kotlin支持数字运算的标准集:+、 -、 *、 /、 %
kotlin
fun main() {
println(1 + 2)
println(2_500_000_000L - 1L)
println(3.14 * 2.71)
println(10.0 / 3)
}
整数除法
整数间的除法总是返回整数。会丢弃任何小数部分。
kotlin
fun main() {
val x = 5 / 2
println(x == 2.5) //会提示:操作符'=='不能应用于'Int'和'Double'
println(x == 2) // 只保留整数部分 输出:true
}
对于任何两个整数类型之间的除法来说都是如此:
kotlin
fun main() {
val x = 5L / 2
println (x == 2) //提示错误,因为x此时是Long类型,因为不能将Long (x)与Int(2)进行比较
println(x == 2L) // true
}
如需返回带小数部分的除法结果,请将其中的一个参数显式转换为浮点类型:
kotlin
fun main() {
val x = 5 / 2.toDouble() //这种做法会让x被推测为Double类型
println(x == 2.5)
}
2.2 布尔
Boolean 类型表示可以有 true 与 false 两个值的布尔对象
kotlin
fun main() {
val myTrue: Boolean = true
val myFalse: Boolean = false
val boolNull: Boolean? = null
println(myTrue || myFalse) // true
println(myTrue && myFalse) // false
println(!myTrue) // false
println(boolNull) // null
}
||和&&操作符是惰性的,这意味着:
-
如果第一个操作数为真,
||运算符不计算第二个操作数。 -
如果第一个操作数为假,
&&操作符不对第二个操作数求值。
2.3 字符
字符用 Char 类型表示。 字符字面值用单引号括起来: '1'
2.3.1 特殊字符
特殊字符可以以转义反斜杠 \ 开始。 支持这几个转义序列:
-
\t------制表符
-
\b------退格符
-
\n------换行(LF)
-
\r------回车(CR)
-
'------单引号
-
"------双引号
-
\------反斜杠
-
$------美元符
-
如果需要编码其他字符,也可以用 Unicode 转义序列语法:'\uFF00'。
kotlin
fun main() {
val aChar: Char = 'a'
println(aChar)
println('\n') // 输出一个额外的换行符
println('\uFF00')
}
如果字符变量的值是数字,也可以使用 digitToInt() 函数将其显式转换为 Int 数字。
2.4 字符串
Kotlin 中字符串用 String 类型表示,既保留了 Java 的强大功能,又通过扩展函数和语言特性大幅提升了易用性
2.4.1 基础特性
不可变性:与 Java 相同,Kotlin 字符串是不可变的
kotlin
val s = "hello"
s[0] = 'H' // 编译错误!String不可变
两种字面量
- 转义字符串:支持转义字符
kotlin
val path = "C:\\Windows\\System32"
- 原始字符串(Raw
String):三引号包裹,保留所有格式
kotlin
val json = """
{
"name": "Kotlin",
"age": 10
}
""".trimIndent()
2.4.2 常用操作
比 Java 更强大的插值功能
kotlin
val name = "Alice"
val greeting = "Hello, $name!" // 插入简单变量
val lengthInfo = "Name length: ${name.length}" // 插入表达式
println("1 + 2 = ${1 + 2}") // 输出:1 + 2 = 3
拼接与重复
kotlin
val combined = "Hello" + " " + "World" // 传统方式
val repeated = "Na".repeat(3) + " Batman!" // 输出:NaNaNa Batman!
多行字符串处理
java
val poem = """
|床前明月光,
|疑是地上霜。
|举头望明月,
|低头思故乡。
""".trimMargin("|") // 自动去除前导|和缩进
分割与连接
kotlin
val langs = "Kotlin,Java,Swift".split(",") // 拆分为List
val merged = langs.joinToString(" | ") // 合并为"Kotlin | Java | Swift"
2.4.3 扩展函数增强
除了常用操作以外,Kotlin 为字符串添加了大量实用扩展(展示部分,感兴趣可以查API文档或者问AI):
| 函数 | 作用 | 示例 |
|---|---|---|
| substringBefore/After | 截取子串 | "file.txt".substringBefore(".") → "file" |
| padStart/padEnd | 填充对齐 | "5".padStart(2, '0') → "05" |
| toIntOrNull | 安全转换 | "123".toIntOrNull() → 123 |
| isBlank | 空值检查 | " ".isBlank() → true |
| replace | 正则替换 | "123-456".replace(Regex("\D"), "") → "123456" |
2.4.4 性能优化技巧
字符串构建器
kotlin
val sb = buildString {
append("Hello")
append(" ")
append("World")
}
比连续"+"拼接效率更高
避免重复创建
- 错误做法(每次循环创建新对象)
kotlin
var result = ""
for (i in 1..1000) {
result += i.toString()
}
- 正确做法
kotlin
val betterResult = StringBuilder().apply {
for (i in 1..1000) {
append(i)
}
}.toString()
2.4.5 与Java互操作
- 无缝调用Java字符串方法
kotlin
val javaStyle = "Kotlin".toLowerCase(Locale.ROOT)
- 消除空指针隐患
kotlin
val maybeString: String? = getJavaString()
val safeLength = maybeString?.length ?: 0 // 安全访问
输出结果如下: 当getJavaString() 返回 null 时 → safeLength = 0 当返回空串"" 时 → safeLength = 0 当返回有效字符串时 →safeLength = 字符串长度
3. 控制语句
kotlin拥有比Java更智能的流程控制
3.1 条件语句(if 与 when)
Kotlin 的 if 不仅是语句,还可以是表达式 (有返回值),返回值由每个分支的最后一行的表达式决定:
-
注意,这种表达式型的写法,
if必须有 else 分支,否则会报错 -
Kotlin的if是表达式,因此它天然就会产生值,无论是否被接,比如下面的表达式: -
有变量接受,返回值就有意义。所以,当需要返回值时,必须显式指定接收位置
-
没有变量接受,返回值就没有意义(直接丢弃)
作为表达式返回最大值
kotlin
val max = if (a > b) {
println("a 更大")
a // 最后一行的表达式会自动成为该分支的返回值
} else {
b
}
替代 Java 的三元运算符,
kotlin
val status = if (score >= 60) "通过" else "未通过"
when 是增强版 switch,支持任意类型和复杂条件:
常量值匹配,类似于switch
kotlin
when (status) {
200 -> println("成功")
404 -> println("未找到")
500 -> println("服务器错误")
else -> println("未知状态")
}
类型检查、智能转换:使用 is 或 !is 来检查对象的类型,并且在分支内自动智能转换为该类型
kotlin
when (shape) {
is Circle -> println("半径: ${shape.radius}") // 自动转为 Circle
is Square -> println("边长: ${shape.side}") // 自动转为 Square
!is Int -> println("不是整数")
}
范围匹配
kotlin
when (score) {
in 90..100 -> println("A")
in 80..89 -> println("B")
!in 60..100 -> println("不及格")
else -> println("未知类型")
}
作为表达式使用,直接输出结果,并且使用变量接收
kotlin
val description = when {
temperature > 30 -> "炎热"
temperature < 10 -> "寒冷"
else -> "舒适"
}
3.2 循环语句
kotlin的循环语句简介且强大
3.2.1 遍历范围(Range)
基础、常规
kotlin
for (i in 1..5) {
print("$i ") // 输出: 1 2 3 4 5
}
反向
kotlin
for (i in 5 downTo 1) {
print("$i ") // 输出: 5 4 3 2 1
}
带步长
kotlin
for (i in 1..10 step 2) {
print("$i ") // 输出: 1 3 5 7 9
}
半开范围(不包含结束值)
kotlin
for (i in 1 until 5) {
print("$i ") // 输出: 1 2 3 4
}
3.2.2 遍历数组
kotlin
val numbers = arrayOf(10, 20, 30)
遍历元素
kotlin
for (num in numbers) {
print("$num ") // 输出: 10 20 30
}
带索引遍历
kotlin
for (index in numbers.indices) {
println("索引 $index 的值是 ${numbers[index]}")
}
输出: 索引 0 的值是 10 索引 1 的值是 20 索引 2 的值是 30
3.2.3 遍历集合(List/Set)
kotlin
val fruits = listOf("Apple", "Banana", "Orange")
基础遍历
kotlin
for (fruit in fruits) {
print("$fruit ") // 输出: Apple Banana Orange
}
带索引遍历 val iterable = fruits.withIndex() 是kotlin当中的一个可迭代对象 当我们在for循环中使用了iterable对象,实际上是发生了下面这几件事:
kotlin
1、隐式调用iterable.iterator(),内部会产生index
2、循环开始时:检查iterator.hasNext()
3、每次迭代:获取item = iterator.next()
val iterable = fruits.withIndex()
for ((index, fruit) in iterable) {
println("$index: $fruit")
}
输出: 0: Apple 1: Banana 2: Orange
3.2.4 遍历 Map
kotlin
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
遍历键值对
kotlin
for ((key, value) in map) {
println("$key -> $value")
}
输出: a -> 1 b -> 2 c -> 3
仅遍历键
kotlin
for (key in map.keys) {
print("$key ") // 输出: a b c
}
仅遍历值
kotlin
for (value in map.values) {
print("$value ") // 输出: 1 2 3
}
3.2.5 遍历字符串字符
java
val text = "Kotlin"
遍历字符
java
for (char in text) {
print("$char-") // 输出: K-o-t-l-i-n-
}
带索引遍历
java
for ((i, char) in text.withIndex()) {
println("$i: $char")
}
输出: 0: K 1: o 2: t 3: l 4: i 5: n
3.2.6 自定义迭代器
系统提供的功能能应对多数场景,但kotlin也提供了自定义迭代器,实现 Iterable 接口的类可被遍历,对自定义迭代器感兴趣,后续可自行扩展学习
定义了一个名为 Counter 的自定义迭代器类,它实现了 Iterable<Int> 接口
kotlin
class Counter(val max: Int) : Iterable<Int> {
override fun iterator(): Iterator<Int> = object : Iterator<Int> {
private var current = 0 // 当前计数值
// 检查是否还有下一个值
override fun hasNext() = current < max
// 获取下一个值并移动
override fun next() = current++
}
}
使用时,指定max的值是3,然后自定义迭代器就会根据你定义的条件进行遍历
kotlin
for (i in Counter(3)) {
print("$i ") // 输出: 0 1 2
}
3.2.7 控制流操作
使用continue、break进行流程控制
跳过当前迭代(continue)
kotlin
for (i in 1..5) {
if (i == 3) continue
print("$i ") // 输出: 1 2 4 5
}
中断循环(break)
kotlin
for (i in 1..10) {
if (i > 5) break
print("$i ") // 输出: 1 2 3 4 5
}
标签跳转(控制外层循环)
kotlin
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i == 2 && j == 2) break@outer
print("($i,$j) ")
}
}
输出: (1,1) (1,2) (1,3) (2,1)
3.2.8 标签控制
精准控制循环跳转:
kotlin
outer@ for (i in 1..3) {
inner@ for (j in 1..3) {
if (i == 2 && j == 2) break@outer
print("($i,$j) ") // 输出: (1,1) (1,2) (1,3) (2,1)
}
}
3.2.9 与 Java 对比
| 特性 | Java | Kotlin | 优势 |
|---|---|---|---|
| 条件返回值 | 三元运算符 | if (a>b) a else b | 支持多行逻辑 |
| 多分支条件 | switch-case | when 表达式 | 支持任意类型和复杂条件 |
| 范围循环 | for (int i=0; i<10; i++) | for (i in 0 until 10) | 避免索引越界错误 |
4. 函数
4.1 函数的定义
函数声明使用 fun 关键字定义:
kotlin
fun add(a: Int, b: Int): Int {
return a + b
}
-
参数格式:参数名: 类型
-
返回值类型在参数后声明(如 :
Int)
表达式函数(单表达式)
省略 {} 和 return:
kotlin
fun add(a: Int, b: Int): Int = a + b
类型可自动推断,进一步简化:
kotlin
fun add(a: Int, b: Int) = a + b
参数支持默认值:
kotlin
fun greet(name: String = "Guest") {
println("Hello, $name!")
}
调用:greet() 输出 Hello, Guest!
调用时,明确参数名:
kotlin
fun createUser(name: String, age: Int, isAdmin: Boolean = false) { ... }
指定参数名(可乱序)
kotlin
createUser(age = 25, name = "Alice")
可变参数,类似 Java 的 ...
kotlin
fun sum(vararg numbers: Int): Int {
return numbers.sum()
}
sum(1, 2, 3) // 输出 6
顶层函数,可脱离类直接定义在文件中:
File: MathUtils.kt
kotlin
fun multiply(a: Int, b: Int) = a * b
无需像 Java 的静态工具类。
局部函数,内部嵌套函数:
kotlin
fun outer() {
fun inner() { println("Inner") }
inner()
}
4.2 扩展函数
扩展函数(Extension Functions) 是一种在不修改原类定义的情况下,为现有类添加新功能的机制。它们通过静态解析实现,不会侵入原类代码,但能像类成员一样被调用。
语法结构
kotlin
fun ReceiverType.functionName(params): ReturnType {
// 函数体
// 通过 `this` 访问接收者对象(可省略)
}
-
ReceiverType: 要扩展的类(接收者类型)
-
functionName: 扩展函数的名称
-
this: 在函数体内指向接收者对象实例(隐式可用)
本质是扩展函数是静态解析的静态函数(编译时确定),不会修改原类,而是通过编译器转换为静态方法调用。例如:
编译后实际会生成:
kotlin
fun String.lastChar() → public static char lastChar(String receiver)
4.2.1 使用案例
为String扩展一个函数,获取o在字符串中第一次出现的位置
kotlin
/**
* 获取o在字符串中第一次出现的位置,如果没出现就返回-1
*/
fun String.getOIndex(): Int {
// 函数体
// 如果在这里面调用this,`this` 指代调用该方法的字符串对象,另外,this也可以被省略
return this.indexOf("o")
}
调用
kotlin
val s :String= "hello"
val oIndex = s.getOIndex()
println("o出现的位置是:$oIndex")
View扩展:简化 Android 中的视图控制
kotlin
fun View.show() {
this.visibility = View.VISIBLE
}
fun View.hide() {
this.visibility = View.GONE
}
使用
kotlin
myButton.show() // 等价于 myButton.visibility = View.VISIBLE
可空类型扩展:安全处理空值
kotlin
fun String?.safeLength(): Int {
return this?.length ?: 0 // 若为 null 返回 0
}
fun main() {
val s: String? = null
println(s.safeLength()) // 输出:0(不会抛 NPE)
}
5. 空安全机制
在 Kotlin 中,? 和 !! 都是空安全机制 的核心组成部分,用于处理可能为 null 的值。这是 Kotlin 区别于 Java 的重要特性,能有效减少 NullPointerException(空指针异常)。
5.1 问号(?)的作用与用法
声明可空类型
kotlin
var name: String? = null // 声明一个可能为 null 的字符串
- 不加 ? 的类型默认为非空类型(不能赋 null 值):
kotlin
var age: Int = null // 编译错误:Null can not be a value of a non-null type Int
安全调用操作符(?.)
kotlin
val length: Int? = text?.length // 如果 text 为 null,返回 null 而不报错
- 等价于
Java的:
java
Integer length = text != null ? text.length() : null;
安全转换(as?)
安全的类型转换,失败时返回 null 而非抛异常,避免出现避免 ClassCastException****
kotlin
val num: Int? = obj as? Int // 转换失败时返回 null 而不是抛异常
Elvis 操作符(?:)
左侧表达式为 null 时可以提供一个默认值,简化空值处理****
kotlin
val name = user?.name ?: "Unknown" // 若左边为 null,则使用右侧默认值
-
先计算
?:左侧表达式 -
如果结果非空 -> 直接返回该值
-
如果结果为 null -> 返回右侧表达式结果
非空断言操作符
这个慎用,因为会导致空指针:告诉编译器,我确定这个值不会为null,哪怕真的为null,你也可以直接抛空指针
kotlin
val length: Int = text!!.length // 断言 text 不为 null,若为 null 则立即抛 NPE
5.2 为什么需要这些符号?
通过类型系统强制开发者显式处理空值,将运行时可能出现的 NPE 转为编译期错误
| 场景 | Java | Kotlin(安全) | Kotlin(危险) |
|---|---|---|---|
| 访问可能为 null 的对象 | text.length()→ 可能 NPE | text?.length→ 返回 null | text!!.length→ 可能 NPE |
| 编译时不检查空值 | 开发者主动承担风险 |
5.3 最佳实践示例
安全处理空值
链式安全调用 + Elvis 操作符
kotlin
val cityName = user?.address?.city ?: "China"
配合 let 作用域函数 为对象创建一个临时作用域,在内部安全操作非空对象
kotlin
user?.let {
println("Name: ${it.name}") // 仅在非 null 时执行
}
5.4 关键总结
| 操作符 | 含义 | 风险程度 |
|---|---|---|
| ? | "这可能为 null" | 无风险 |
| ?. | "为 null 时跳过" | 无风险 |
| ?: | "为 null 时使用默认值" | 无风险 |
| !! | 高风险 |
实践建议
-
90% 的场景使用
?.和?:替代!! -
剩余 10% 与
Java交互的代码中,使用!!前务必确认数据源的非空性 -
永远不要为了"快速完成代码"而滥用
!!
6. 类与对象
-
类:类是创建对象的模板。它定义了一类对象共有的属性(状态) 和 行为(方法/函数);
-
对象:对象是类的一个具体实例(类似于java的 new
Object)。它是根据类的定义在内存中创建出来的具体实体,拥有类定义的属性和方法。对象是真实存在的,一旦创建就会在内存中被分配空间。
6.1 定义与使用
基础用法
kotlin
class MyClass {
// 属性(成员变量)
var property: Int = 0
// 方法(成员函数)
fun printValue() {
println("Value: $property")
}
}
使用
初始化
kotlin
val myObject = MyClass()
使用属性
kotlin
myObject.property = 10 // 设置属性值
println(myObject.property) // 获取属性值,输出: 10
调用方法
kotlin
myObject.printValue() // 输出: Value: 10
- 主构造函数,直接在类名后声明
kotlin
class Person(val name: String, var age: Int) {
// 使用 `val/var` 直接声明属性
}
class Person(name: String) {
val firstName: String
//也可以在init方法块中初始化一些属性
init {
firstName = name.uppercase()
}
}
使用
kotlin
直接在类后面声明属性
初始化
kotlin
val person1 = Person("Alice", 25)
使用属性
kotlin
println(person1.name) // 输出: Alice (val 不可变)
println(person1.age) // 输出: 25
person1.age = 26 // 可以修改 (var 可变)
person1.name = "Bob" // 错误,name 是 val 不可变
kotlin
在init块中初始化
初始化
kotlin
val person2 = Person("Bob")
使用属性
kotlin
println(person2.firstName) // 输出: BOB (因为init中有调用uppercase,所以firstName已被转为大写)
次构造函数(Secondary Constructor)用constructor 关键字定义:
kotlin
class Person(val name: String) {
var age: Int = 0
// 次构造函数必须委托给主构造函数
constructor(name: String, age: Int) : this(name) {
this.age = age
}
}
使用
使用主构造函数初始化
kotlin
val person3 = Person("Charlie")
使用次构造函数初始化
kotlin
val person4 = Person("David", 30)
使用属性
kotlin
println(person4.name) // 输出: David
println(person4.age) // 输出: 30
6.2 继承
所有类默认继承 Any (类似 Java 的 Object )。使用 open 允许继承,override 重写成员
父类需要open,才允许继承
kotlin
open class Animal(val name: String) {
//父类函数需要open才能被重写
open fun speak() = "Generic sound"
}
class Dog(name: String) : Animal(name) {
override fun speak() = "Woof!"
}
6.3 抽象
定义部分实现的基类,要求子类实现抽象部分
抽象类(不能实例化,包含抽象和具体实现)
kotlin
abstract class Printer {
abstract fun printDocument(document: String) // 抽象方法,必须由子类实现
fun powerOn() = println("Printer powered on") // 具体方法
}
class LaserPrinter : Printer() {
override fun printDocument(document: String) {
println("Laser printing: $document")
}
}
6.4 接口
接口(定义契约,Kotlin接口可以有默认实现)
kotlin
interface Clickable {
fun click() // 抽象方法
fun showOff() = println("I'm clickable!") // 带默认实现的方法
}
class Button : Clickable {
override fun click() = println("Button clicked!")
}
6.5 数据类
在java中有很多常见的模板代码,但在kotlin中可以快速生成实现
kotlin
data class Point(val x: Int, val y: Int)
上面1句话的代码,等效于下面的java代码
Java 版需要手动实现所有方法
java
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// toString() 手动实现
@Override
public String toString() {
return "Point(x=" + x + ", y=" + y + ")";
}
// equals() 手动实现
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
// hashCode() 手动实现
@Override
public int hashCode() {
return 31 * x + y;
}
// copy() 方法需要自行创建
public Point copyWith(int x, int y) {
return new Point(x != null ? x : this.x, y != null ? y : this.y);
}
// 还需要手动实现 getters...
}
6.6 伴生对象
伴生对象是一个与类关联的特殊对象,主要用于替代 Java 的静态成员(静态方法和静态字段)。它允许在不创建类实例的情况下访问其成员。
声明方式
kotlin
class MyClass {
companion object MyConpanion {
// 属性
val age: Int = 100
//方法
fun loadImage(url: String): Int {
var resultCode = 1
try {
println("加载了一张网络图片,地址是$url")
} catch (e: Exception) {
resultCode = 500
}
return resultCode
}
}
}
访问成员
直接通过类名访问伴生对象成员(无需实例化):
kotlin
MyClass.companionMember // 类似 Java 的静态调用
默认名称
伴生对象默认名为 Companion,也可自定义名称:
kotlin
companion object CustomName { ... }
访问:MyClass.CustomName.foo()
6.7 可见性修饰符
可见性修饰符 用于控制类、函数、属性等元素的访问范围。它们决定了代码中的哪些部分可以访问被修饰的元素。Kotlin 提供了四种修饰符:public、private、protected 和 internal。
6.7.1 不同修饰符的区别
-
public:默认修饰符,随处可见(任何文件、任何模块) -
private:仅限声明它的文件 或类内部访问 -
protected:仅限类内部 和子类中访问(不能用于顶层声明) -
internal:同一模块(Module)内可见(跨文件但同模块)
6.8 与 Java 的主要区别
6.9 Kotlin vs Java 对比表
| 特性 | Kotlin | Java |
|---|---|---|
| 主构造函数 | 声明在类名后(class Person(val name: String)),简洁。参数可直接声明为属性(使用 val/var)。 | 没有单独的主构造语法。构造函数是与类同名的方法(public Person(String name) { ... })。属性需要在类体中单独声明,并通常在构造函数中赋值。 |
| 默认可见性 | public(默认)。Kotlin 的成员默认就是 public。 | 包级私有 default(默认)。Java 不写修饰符时,是包级私有。 |
| final 默认 | 类/方法默认是 final。如果需要可继承/可重写,必须显式使用 open 关键字。 | 类/方法默认是非 final。如果不需要继承/重写,必须显式声明为 final。 |
| 数据类 | 提供 data 类:自动生成 equals()、hashCode()、toString()、copy() 方法。 | 需要手动实现或使用 IDE 生成或 Lombok 等库。没有语言内置支持。 |
| new 关键字 | 创建对象时不需要 new:val myObj = MyClass() | 创建对象时必须使用 new:MyClass myObj = new MyClass(); |
| 单例对象 | 使用 object 声明:object MySingleton { ... },这是创建单例的最简单、推荐方式。 | 需要通过私有构造、静态实例变量和 getInstance() 等方法手动实现单例模式。 |
| 伴生对象 | 使用 companion object:在类内部声明,提供类似 Java 静态成员的作用,但本质是对象实例,更灵活(可实现接口等)。 | 使用 static 成员变量和方法。 |
| 延迟初始化 | 提供 lateinit(用于 var)和 by lazy 委托(用于 val)。 | 需要在构造时初始化 final 变量或使用懒加载模式(如双重检查锁)。 |
| 属性 | 用 val(只读)或 var(可变)声明。底层可能有 getter/setter。 | 需要显式定义 getter/setter 方法(或者 public 字段不推荐)。 |
| 空安全 | 核心语言特性。类型默认非空,允许可空类型 Type?,访问时必须进行空检查或使用安全操作符(?.、!!)。 | 内置的空指针异常(NPE)。没有强制机制,需要开发者小心处理,使用 @Nullable、@NotNull 注解(IDE/框架辅助)。 |
7. 列表与集合
Kotlin 的列表与集合是构建在 Java 集合框架之上的, Kotlin 通过大量的扩展函数 为 Java 集合提供了丰富、流畅、函数式的操作(如 filter, map, groupBy 等)。
7.1 用法
7.1.1 创建集合
只读列表
kotlin
val names = listOf("Alice", "Bob", "Charlie")
println(names[2])//这种写法更简洁,因此更为推荐,但会编译成get(2)的方式,所以两者完全是等价的
println(names.get(2))
可变列表(用于需要修改的场景) 注意:这里val控制变量不可重新赋值、修改,而mutableListOf控制集合内容的可修改性
kotlin
val tasks = mutableListOf("写代码", "修bug", "喝咖啡")
tasks.add("测试")
键值对(代替Java的HashMap)
kotlin
val userInfo = mapOf(
"name" to "小明",
"age" to 25,
"isVIP" to true
)
取值
kotlin
val value = userInfo["name"] // 拿到 "小明"
可变set集合
kotlin
val fruits = mutableSetOf("Apple", "Banana", "Orange")
println(fruits) // 输出: [Apple, Banana, Orange]
fruits.add("Grape") // 添加成功,返回 true
fruits.add("Apple") // 添加失败("Apple" 已存在),返回 false
fruits.remove("Banana") // 删除成功,返回 true
fruits.remove("Mango") // 删除失败(不存在),返回 false
println(fruits) // 输出: [Apple, Orange, Grape]
空集合(避免NullPointerException)
kotlin
val emptyList = emptyList<String>()
假设有个函数需要返回空列表,那就可以这样用
kotlin
fun findUsers(query: String): List<User> {
return database.queryUsers(query) ?: emptyList() // 非 null 安全返回
}
顺便提供一些数组的常见用法,但是kotin的集合、列表功能更强大,数组的使用频率相对低
- 对象类型数组
kotlin
val names = arrayOf("Alice", "Bob", "Charlie")
- 原始类型数组(避免装箱开销)
kotlin
val intArray = intArrayOf(1, 2, 3) // 性能更好
val floatArray = floatArrayOf(1.5f, 2.5f)
- 固定大小空数组
kotlin
val emptyArray = arrayOfNulls<String>(3) // [null, null, null]
- 动态初始化
kotlin
val squares = Array(5) { i -> (i * i) } // [0, 1, 4, 9, 16]
- 访问元素
kotlin
val first = names[0] // "Alice"
names[1] = "Robert" // 修改元素
- 安全访问(避免崩溃)
kotlin
val safeValue = names.getOrNull(5) ?: "Default"
- 遍历数组
kotlin
names.forEach { name ->
Log.d("Array", name)
}
- 转换为集合(推荐后续使用集合操作)
kotlin
val nameList = names.toList()
7.1.2 高频操作
遍历集合
在 Lambda 表达式中,如果只有一个参数,就可以直接使用 it 指代当前遍历到的对象。
使用下标遍历集合
列表遍历的 Lambda 写法如下:
kotlin
names.forEach { name ->
println "用户名: $name")
}
如果 Lambda 只有一个参数,可以省略显式参数名,直接用 it 指代当前被遍历到的对象:
kotlin
names.forEach { println(it)}
带索引的遍历:
kotlin
tasks.forEachIndexed { index, task ->
println("任务${index + 1}: $task")
}
Map 遍历:
kotlin
userInfo.forEach { (key, value) ->
println("TAG", "$key -> $value")
}
除此之外,还可以继续配合 for 循环完成遍历,两种写法可以根据场景灵活选择。
数据类型转换
假设我们正在进行网络请求,网络请求接口返回的数据实体类是ApiUser,其次还有User与布局关联,经常就需要把ApiUser转换成User:
API 返回的数据类
kotlin
data class ApiUser(
val id: Int,
val username: String,
val profileUrl: String
)
业务层的领域模型
kotlin
data class User(
val id: Int,
val name: String,
val avatar: String
)
fun main() {
// 模拟 API 响应数据
val apiResponse: List<ApiUser> = listOf(
ApiUser(1, "user1", "https://example.com/avatar1.jpg"),
ApiUser(2, "user2", "https://example.com/avatar2.jpg")
)
//map函数:把apiResponse列表中的每一个元素,通过大括号里面的转换逻辑,转成一个新的列表userList,类型是List<User>
// 将 ApiUser 转换为 User
val userList = apiResponse.map { apiUser ->
User(
id = apiUser.id,
name = apiUser.username,
avatar = apiUser.profileUrl
)
}
println(userList)
}
只要 map 的对象中,属性是一一对应的,可以省略赋值的代码
过滤数据(列表筛选)
假设需要过滤出vip用户:
kotlin
data class User(val id: Int, val name: String, val isVIP: Boolean)
fun main() {
val userList = listOf(
User(1, "Alice", true),
User(2, "Bob", false),
User(3, "Charlie", true)
)
// 过滤出 VIP 用户
//it 是集合元素的隐式参数,在这里我们用it代指 User 对象
val vipUsers = userList.filter { it.isVIP }
println(vipUsers) // 输出: [User(id=1, name=Alice, isVIP=true), User(id=3, name=Charlie, isVIP=true)]
}
filter 也很适合实现搜索筛选:
商品类
kotlin
data class Product(
val id: Int,
val name: String,
val price: Double
)
初始化一些数据
kotlin
val products = listOf(
Product(1, "iPhone 13", 699.0),
Product(2, "Samsung Galaxy S22", 799.0),
Product(3, "Google Pixel 6", 599.0),
Product(4, "Xiaomi 12", 649.0),
Product(5, "OnePlus 10 Pro", 729.0)
)
假设用户搜索 pixel 希望得出"Google Pixel 6"并且忽略大小写:
kotlin
val query = "pixel"
val searchResults = products.filter {
it.name.contains(query, ignoreCase = true)
}
过滤空数据时可以直接使用:
kotlin
val validData = sensorData.filterNotNull()
查找元素(代替循环)
通过 find 可以拿到列表中第一个符合条件的对象,并且只返回这一项:
下面这段代码就是查找第一个符合条件的元素,这里的 it 代指列表中的对象:
kotlin
val currentUser = userList.find { it.id == 2 }
any 检查是否存在至少一个满足条件的元素:
检查是否存在至少一个满足条件的元素(比如用来检查是否有未读消息、未完成的任务)
kotlin
val hasUnfinished = tasks.any { !it.isCompleted }
all 返回所有符合条件的元素
检查是否所有元素都满足条件(比如用来检查是否全选?)
kotlin
val allDone = tasks.all { it.isCompleted }
7.1.3 常见用法和技巧
RecyclerView 数据更新
在 Adapter 中先定义一个内部可变列表,再通过只读属性对外暴露数据:
kotlin
private var _items = mutableListOf<Item>()
val items: List<Item> get() = _items//通过这种形式提供一个对外、可读的items,以确保内部数据安全
fun updateData(newItems: List<Item>) {
_items.clear()//清除列表数据
_items.addAll(newItems)//加入新的数据
notifyDataSetChanged()//刷新
}
数据分组(如按分类显示)
kotlin
data class Product(val id: Int, val name: String, val category: String)
productList 的类型是 List<Product>:
kotlin
val productList = listOf(
Product(1, "手机", "电子产品"),
Product(2, "衬衫", "服装"),
Product(3, "笔记本电脑", "电子产品"),
Product(4, "裤子", "服装")
)
groupBy 会把集合元素分组并返回一个 Map。这里按商品类别分类后,productsByCategory 的类型就是 Map<Category, List<Product>>:
kotlin
val productsByCategory = productList.groupBy { it.category }
输出:
{
"电子产品" -> [Product(1, "手机", "电子产品"), Product(3, "笔记本电脑", "电子产品")],
"服装" -> [Product(2, "衬衫", "服装"), Product(4, "裤子", "服装")]
}
集合去重
distinctBy:根据指定的条件去除集合中的重复元素 去除重复通知:去除msgId相同的对象
kotlin
val uniqueNotifications = notifications.distinctBy { it.msgId }
8. Lambda表达式
一种简洁的、匿名函数的表示方式。不需要显式声明函数名,但可以有参数列表、函数体和返回值。使用lambda写法,可以让代码更简洁。在安卓中lambda写法常用于下面的场景:
-
Android事件监听:setOnClickListener,setOnLongClickListener,TextWatcher等; -
集合操作:使用
filter,map, sortedBy ,groupBy,any,all,find,forEach等标准库高阶函数进行数据处理和转换; -
作用域函数:
let,run,with,apply,also。它们接收一个对象和一个Lambda,在Lambda内部提供一个临时的作用域,用于安全操作可空对象、初始化配置、链式调用等; -
线程操作: 将任务 (Runnable ) 传递给线程 (
Thread{ ... }.start() ) 或线程池 (executor.execute { ... });
8.1 语法定义
-
语法核心: { 参数列表 -> 函数体 }
-
参数列表:在 -> 左侧声明,类型通常可推断。
-
函数体:在 -> 右侧。如果函数体是单个表达式,其值自动作为返回值;如果是代码块,则需要显式使用 return
-
8.2 常见用法
8.2.1 事件监听(取代 Java 匿名内部类)
点击事件
kotlin
button.setOnClickListener { startActivity(Intent(this, NextActivity::class.java)) }
长按事件
kotlin
button.setOnLongClickListener {
showDialog()
true // 返回值(直接写最后一行)
}
8.2.2 集合操作(链式调用 + Lambda)
kotlin
val names = listOf("Alice", "Bob", "Anna")
过滤:保留以 "A" 开头的名字
kotlin
val aNames = names.filter { it.startsWith("A") } // ["Alice", "Anna"]
映射:转换为长度
kotlin
val lengths = names.map { it.length } // [5, 3, 4]
遍历
kotlin
names.forEach { println("Name: $it") }
8.2.3 作用域函数(let, apply, also)
let:处理可空对象 + 安全调用
kotlin
user?.let {
tvName.text = it.name
loadAvatar(it.url)
}
apply:对象初始化(返回自身)
kotlin
val intent = Intent().apply {
putExtra("KEY", value)
setAction("ACTION")
}
also:额外操作(返回自身)
kotlin
File("path").also {
println("创建文件: ${it.name}")
}.writeText("Hello")
8.2.4 关键注意事项
- return** 的陷阱**:Lambda 中直接写 return 会退出外层函数!需用标签返回
kotlin
list.forEach {
if (it == "exit") return@forEach // 只退出 Lambda
}
- Lambda 参数类型:若编译器无法推断类型,需显式声明
kotlin
val callback: (String) -> Unit = { str -> println(str) }
9. 高阶函数
高阶函数(Higher-Order Function) 是指可以接受函数作为参数、或者返回一个函数的函数。将行为作为参数传递,使代码更灵活且可复用,同时保持类型安全。极大地增强了代码的抽象能力和灵活性。
9.1 语法
定义高阶函数
kotlin
fun 函数名(参数名: (参数类型) -> 返回类型) {
// 调用传入的函数
参数名(实参)
}
9.2 使用案例
- 基础回调函数,把函数当成参数传递使用
定义高阶函数
kotlin
fun performRequest(
url: String,
onSuccess: (response: String) -> Unit, // 函数类型参数,Unit表示函数不返回任何有意义的值,类似于java的void
onFailure: (exception: Exception) -> Unit // 函数类型参数
) {
try {
// 模拟网络请求
val response = "响应数据: $url"
onSuccess(response) // 调用成功回调
} catch (e: Exception) {
onFailure(e) // 调用失败回调
}
}
使用
kotlin
fun main() {
performRequest(
url = "https://api.example.com",
onSuccess = { response ->
println("成功: $response")
},
onFailure = { error ->
println("失败: ${error.message}")
}
)
}
9.2.1 带返回值的函数参数
定义高阶函数
kotlin
fun transformData(
data: List<Int>,
transform: (Int) -> String // 接收Int返回String的函数
): List<String> {
return data.map(transform) // 使用传入的转换函数
}
使用
kotlin
fun main() {
val numbers = listOf(1, 2, 3)
val result = transformData(numbers) { num ->
"数字: ${num * 2}" // Lambda 表达式
}
println(result) // 输出: [数字: 2, 数字: 4, 数字: 6]
}
9.2.2 返回函数的函数
- createLogger() 高阶函数,接收
String类型的 prefix,返回一个接收String类型的参数 message,无返回值的函数 createLogger("MainActivity")调用高阶函数,返回值是一个函数,这个函数会打印传入的参数以及高阶函数 createLogger() 的参数 prefix ,命名为 activityLogger,再调用该函数 activityLogger("Activity 已创建");- createLogger("Network")、networkLogger("请求发送") 同理;
定义高阶函数(返回一个函数)
kotlin
fun createLogger(prefix: String): (String) -> Unit {
return { message -> // 返回一个Lambda表达式
println("[$prefix] $message")
}
}
使用
kotlin
fun main() {
val activityLogger = createLogger("MainActivity")
val networkLogger = createLogger("Network")
activityLogger("Activity 已创建") // 输出: [MainActivity] Activity 已创建
networkLogger("请求发送") // 输出: [Network] 请求发送
}
-
返回类型:(
String) -> Unit-
表示它返回一个函数
-
这个返回的函数:接受一个
String参数,返回 Unit
-
10. 协程
在移动开发领域,异步编程是很重要的。无论是网络请求、数据库访问,还是文件读写和定时任务,几乎所有耗时操作都需要在后台线程执行,以避免阻塞主线程造成界面卡顿。这一需求催生了多种异步解决方案的演进。早期的异步处理主要依赖Thread、AsyncTask和Handler等机制,但这些方案普遍存在一些痛点:
-
代码结构混乱:多层嵌套的回调形成"回调地狱"
-
异常处理复杂:错误传播路径不清晰
-
资源管理困难:需要手动控制线程生命周期
10.1 为什么需要协程?
Kotlin协程的诞生标志着异步编程范式的重大变革。kotlin通过以下几种核心特性,直接重塑开发体验:
-
**同步式编码:**直接用顺序逻辑表达异步操作,彻底告别回调地狱Callback
kotlin`viewModelScope`.`launch` { try { val token = login(username, password) // 挂起点1 val user = getUserInfo(token) // 挂起点2 val orders = loadOrders(user.id) // 挂起点3 updateUI(orders) } `catch` (e: Exception) { handleError(e) } } -
**智能的线程调度:**支持在多个线程间无缝切换,主线程与工作线程的切换成本极低,内置多种调度策略(IO/CPU密集型等)
-
极致的资源效率:协程运行在线程上的轻量级任务单元,协程由编程语言层面实现,而非操作系统层面的实现,高效利用线程资源,内存占用仅为几KB级别,单台设备可轻松运行数十万协程(对比传统线程的数百个上限),创建和切换开销几乎可以忽略不计。
-
**完善的生命周期管理:**支持结构化并发,提供协作式取消机制,自动避免内存泄漏风险。
10.2 协程与线程的关系
协程与线程并非替代关系,而是形成了高效的协作体系:
-
运行载体:线程作为底层执行引擎,为协程提供运行环境
-
资源复用:单个线程可同时承载多个协程(类似高速公路与车辆的关系)
-
智能调度:协程可在不同线程间自由迁移(挂起时不阻塞线程)
-
执行模型:协程的挂起/恢复完全由程序控制,不涉及系统内核调度
| 特性 | 线程 | 协程 |
|---|---|---|
| 创建数量 | 数百个 | 数十万个 |
| 内存开销 | 高 (MB级) | 低 (KB级) |
| 切换成本 | 高 (系统调用) | 极低 (用户态) |
| 阻塞代价 | 整个线程阻塞 | 仅当前协程暂停 |
| 代码复杂度 | 回调/同步复杂 | 顺序编写简单 |
这种架构使得开发者既能享受接近同步代码的编写体验,又能获得异步执行的高性能优势。在Android平台上,协程已经成为处理异步任务的事实标准,完美解决了传统方案在可维护性和执行效率上的矛盾。
10.3 协程的核心组件
想要使用协程,我们应该先认识协程中的几个重要组件。掌握这些核心组件,我们就能优雅的处理很多异步场景。
10.3.1 协程作用域(CoroutineScope)
作用域是协程的 "管理者",负责跟踪和管理作用域内部启动的所有协程。
核心作用
限制协程的生命周期:当作用域被取消时,其内部所有协程都会被取消(如 Activity 销毁时取消viewModelScope,避免泄漏)。
提供默认的协程上下文:作用域包含一个CoroutineContext,为协程提供默认的调度器、异常处理器等。
常见的作用域类型
-
GlobalScope:全局作用域**(慎用,生命周期与应用一致)** -
viewModelScope(在Android中才有):ViewModel销毁时自动取消 -
lifecycleScope(在Android中才有):与Activity/Fragment生命周期绑定
ViewModel 中使用
kotlin
class MyViewModel : ViewModel() {
fun fetchData() {
//开辟一个新的协程作用域
viewModelScope.launch { // 作用域绑定 ViewModel,当viewModel被销毁,该作用域会自动取消所有子协程。从而防止内存泄露
val data = repository.loadData()//具体的业务需求,比如加载某个数据
_uiState.value = data
}
}
}
10.3.2 协程构建器(Builders)
构建器是启动协程的入口
常见的核心构建器
| 构建器 | 特点 | 使用场景 |
|---|---|---|
| launch | 启动新协程,不返回结果 | 执行后台任务 |
| async | 启动新协程,返回 | 并发执行任务并获取结果 |
| runBlocking | 阻塞当前线程直到协程完成 | 测试/命令行程序 |
并发请求示例
kotlin
viewModelScope.launch {
val userDeferred = async { getUser() } // 异步获取用户
val newsDeferred = async { getNews() } // 异步获取新闻
// 等待两个请求完成
val user = userDeferred.await()
val news = newsDeferred.await()
showData(user, news) // 主线程更新UI
}
kotlin
val mUserInfo = MutableLiveData<String>()
fun fetchData() {
// 开辟一个新的协程作用域
viewModelScope.launch {
val userInfoDeferred: Deferred<String> = async {
// 异步获取用户信息
delay(2000) // 不阻塞线程
val userInfo = "laosun"
Log.i(TAG, "fetchData 返回网络数据: $userInfo")
return@async userInfo
}
// 等待请求完成
val userInfo: String = userInfoDeferred.await()
// 自动切回主线程,更新到 UI
mUserInfo.value = userInfo
}
companion object {
private const val TAG = "MainViewModel"
}
}
这段代码的执行流程是这样的:
fetchData() 被调用后,会先进入 viewModelScope.launch { ... }。这一步表示在 ViewModel 绑定的协程作用域里启动一个新协程,所以这段逻辑不会阻塞调用它的线程,通常也就是主线程。
进入 launch 之后,代码执行到:
kotlin
val userInfoDeferred: Deferred<String> = async { ... }
这里又开启了一个子协程。async 和 launch 的区别是:launch 主要用于"执行任务",而 async 主要用于"执行任务并返回结果"。所以 async 会返回一个 Deferred<String>,你可以把它理解成"未来会拿到的字符串结果"。
接着 async 里面开始执行:
kotlin
delay(2000)
这里会挂起 2 秒,但不会阻塞线程 。也就是说,这个协程暂停等待,线程可以去做别的事,这是协程比 Thread.sleep() 更高效的地方。
2 秒后恢复执行:
kotlin
val userInfo = "laosun"
Log.i(TAG, "fetchData 返回网络数据: $userInfo")
return@async userInfo
这里模拟拿到了网络返回的数据 "laosun",打印日志,然后把这个值作为 async 的返回结果交出去。
与此同时,外层 launch 协程继续往下走,执行:
kotlin
val userInfo: String = userInfoDeferred.await()
await() 的作用是"等结果"。如果 async 还没执行完,这里就会挂起等待;如果已经执行完了,就直接拿到结果。最终 userInfo 会得到 "laosun"。
整个顺序可以概括成:
- 调用
fetchData() - 在
viewModelScope中启动一个外层协程launch - 在外层协程里再启动一个带返回值的子协程
async - 子协程先
delay(2000)挂起 2 秒 - 2 秒后返回
"laosun" - 外层协程通过
await()拿到结果 - 结果保存到
userInfo
可以把它理解成:
launch:开一个任务async:开一个能产出结果的任务await:等这个结果出来
这段代码的关键点有两个:
- 第一,
delay(2000)是挂起,不是阻塞,所以不会卡死线程。 - 第二,虽然用了
async,但因为后面立刻就await()了,所以这个例子里"并发优势"并不明显,更像是在演示async/await的基本写法。
比如这段代码本质上接近于:
kotlin
viewModelScope.launch {
delay(2000)
val userInfo = "laosun"
}
只有在你要同时发起多个异步任务,再统一等待结果 时,async 才更有价值。
例如:
kotlin
viewModelScope.launch {
val userDeferred = async { getUser() }
val articleDeferred = async { getArticles() }
val user = userDeferred.await()
val articles = articleDeferred.await()
}
这里两个任务会并发执行,总耗时通常比串行更短。
还有一点,这段代码运行在 viewModelScope 中,意味着当 ViewModel 被销毁时,这些协程会自动取消,避免内存泄漏,这也是 Android 里常见的写法。
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var mViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn_start)
.setOnClickListener { mViewModel.fetchData() }
val tvLabel = findViewById<TextView>(R.id.tv_label)
mViewModel = ViewModelProvider(this)
.get(MainViewModel::class.java)
mViewModel.mUserInfo.observe(this) { userInfo ->
tvLabel.text = userInfo
}
}
}
把 Activity、ViewModel、协程和 LiveData 放在一起看,这条链路可以概括为:Activity 负责触发交互和展示结果,ViewModel 负责发起异步任务并持有结果,LiveData 负责把数据变化继续传递给界面。
1. 用户点击按钮
kotlin
findViewById<Button>(R.id.btn_start)
.setOnClickListener { mViewModel.fetchData() }
- 用户点击按钮
Activity调用ViewModel的fetchData()
2. 进入 ViewModel 处理数据
kotlin
fun fetchData() {
viewModelScope.launch {
val userInfoDeferred = async {
delay(2000)
val userInfo = "laosun"
return@async userInfo
}
val userInfo = userInfoDeferred.await()
mUserInfo.value = userInfo
}
}
这里主要发生四件事。
启动协程
kotlin
viewModelScope.launch
- 在主线程安全执行
- 生命周期绑定
ViewModel
开启异步任务
kotlin
async { ... }
- 模拟网络请求
- 2 秒后返回
"laosun"
等待结果
kotlin
await()
- 挂起等待结果
- 拿到
"laosun"
更新 LiveData
kotlin
mUserInfo.value = userInfo
LiveData一旦setValue- 会自动通知所有观察者(Observer)
3. Activity 观察数据变化
kotlin
mViewModel.mUserInfo.observe(this) { userInfo ->
tvLabel.text = userInfo
}
Activity注册一个观察者- 一旦
mUserInfo变化,就执行这里
4. UI 自动更新
当 ViewModel 执行下面这行代码时:
kotlin
mUserInfo.value = "laosun"
LiveData 会通知观察者执行回调中的更新逻辑:
kotlin
tvLabel.text = userInfo
最终 TextView 会显示:
laosun
5. 完整数据流
用户点击按钮
↓
Activity 调用 ViewModel.fetchData()
↓
ViewModel 启动协程
↓
模拟网络请求(delay 2秒)
↓
得到数据 "laosun"
↓
LiveData.setValue()
↓
Activity 的 Observer 被触发
↓
TextView 更新显示
6. 核心理解
为什么用 ViewModel
- 防止
Activity重建丢数据 - 分离 UI 和业务逻辑
为什么用 LiveData
- 自动通知 UI
- 感知生命周期(避免内存泄漏)
为什么用协程
- 异步任务(网络请求)
- 不阻塞主线程
各层职责
| 层级 | 作用 |
|---|---|
| Activity | 监听 + 展示 UI |
| ViewModel | 处理数据逻辑 |
| LiveData | 数据桥梁 |
| Coroutine | 异步执行 |
7. 一个实现细节
你现在的代码是:
kotlin
async { ... }
await()
这更像是"带返回值的异步写法",但因为后面立刻就 await() 了,并没有真正发挥并发组合的优势。
如果这里只是延迟后更新一次数据,可以直接写成更简洁的形式:
kotlin
viewModelScope.launch {
delay(2000)
mUserInfo.value = "laosun"
}
8. 一句话总结
按钮点击后,Activity 调用 ViewModel 发起异步任务,任务完成后更新 LiveData,再由 Activity 的观察者自动刷新界面。
10.3.3 调度器(Dispatchers)
**作用:**指定协程运行的线程。
四大调度器:
| 调度器 | 用途 | 对应线程池 |
|---|---|---|
| Dispatchers.Main | Android UI 操作 | 主线程 |
| Dispatchers.IO | 网络/磁盘 I/O | 动态扩容线程池 |
| Dispatchers.Default | CPU 密集型计算 | 固定大小线程池 |
| Dispatchers.Unconfined | 无约束(不推荐) | 当前线程 → 恢复线程 |
开辟一个作用域,作用域内的操作在主线程完成,同时作用域内还有一个 withContext(Dispatchers.IO),这里面的任务需要在IO线程完成
kotlin
viewModelScope.launch(Dispatchers.Main) {
// 1. Dispatchers.Main指定在主线程启动一个任务
val data = withContext(Dispatchers.IO) {
// 2. 切换到IO线程执行网络请求
api.fetchData()
}
// 3. 切回主线程更新UI(因为本身就是在main主线程)
updateUI(data)
}
withContext:临时切换协程执行上下文 的,比如原先在Dispatchers.Main,可以通过withContext切到Dispatchers.IO,withContext是一个挂起函数,当它调用时,协程需要:
-
挂起当前协程(释放原线程资源)
-
切换到目标线程(如 IO 线程池)
-
执行完代码块后恢复到原始线程
这种"挂起-切换-恢复"的能力必须依赖挂起函数的机制实现。
10.3.4 挂起函数(Suspend Functions)
**作用:**标记可暂停执行、但不阻塞线程的函数。带有 suspend 关键字的函数,可以在不阻塞当前线程的前提下暂停执行,并在适当的时候恢复。。
关键特性:
-
只能在协程或其他挂起函数中调用
-
使用
suspend关键字声明 -
内部可调用其他挂起函数(如
delay,withContext)
声明挂起函数
kotlin
suspend fun fetchUser(): User {
delay(1000) // 模拟耗时,不阻塞线程
return withContext(Dispatchers.IO) {
// 执行真实网络请求
api.getUser()
}
}
10.3.5 协程上下文(CoroutineContext)
**作用:**携带协程运行环境信息的容器。
包含元素:
-
调度器(指定线程)
-
异常处理器(
CoroutineExceptionHandler) -
协程名称(方便调试)
-
父协程的
Job
自定义上下文
kotlin
val customContext = Dispatchers.IO +
CoroutineName("MyCoroutine") +
CoroutineExceptionHandler { _, e ->
println("捕获异常: $e")
}
viewModelScope.launch(customContext) {
// 在此上下文中运行
}
10.3.6 协程任务(Job)
**作用:**控制协程生命周期。
关键操作:
-
job.start():启动协程(默认自动启动)
-
job.cancel():取消协程
-
job.join():等待协程结束
kotlin
val job = viewModelScope.launch {
// 执行任务
}
当页面销毁时取消任务
kotlin
override fun onDestroy() {
super.onDestroy()
job.cancel() // 避免内存泄漏
}
10.4 常见用法场景
10.4.1 网络请求 + UI 更新
这种没有指定调度器的写法,默认为main主线程
kotlin
viewModelScope.launch {
try {
_uiState.value = Loading//显示加载框
//切到io线程发起网络请求
val data = withContext(Dispatchers.IO) {
api.fetchData()
}
//拿到结果进行展示
_uiState.value = Success(data)
} catch (e: Exception) {
//请求异常,显示错误结果
_uiState.value = Error(e)
}
}
10.4.2 并发执行多任务
kotlin
viewModelScope.launch {
val result1 = async { task1() }//启动新协程
val result2 = async { task2() }//启动新协程
val combinedResult = result1.await() + result2.await()
}
10.4.3 超时控制
kotlin
viewModelScope.launch {
try {
val data = withTimeout(5000) { // 5秒超时
fetchSlowData()
}
} catch (e: TimeoutCancellationException) {
showToast("请求超时")
}
}
10.4.4 异常统一处理
kotlin
val handler = CoroutineExceptionHandler { _, e ->
Log.e("Coroutine", "全局异常: $e")
}
viewModelScope.launch(handler) {
throw NetworkException("模拟错误")
}
10.5 Flow(流)
在 Android 开发中,有些场景只需要处理一次,而我们经常遇到需要多次更新数据的场景,用flow就能很好的适应这种需要多次更新数据的场景:
-
实时数据更新:数据库变化、传感器数据、WebSocket 消息
-
分页加载:分批获取网络数据
-
用户输入处理:搜索框连续输入时的实时搜索
-
多源数据组合:同时监听网络和数据库变化
10.5.1 flow的设计规则
- 异步数据流:按需生产和消费数据
Flow 实现了这种"按需生产-消费"的模型,与传统的一次性异步操作不同,Flow 允许数据在时间维度上持续产生和消费。数据生产者只在有收集者有需要时才产生数据,且每次只发射单个值,消费者收到值后立即处理。
- 结构化并发:自动跟随协程作用域取消
Flow 深度集成了 Kotlin 协程的生命周期感知能力。当启动 Flow 的协程作用域被取消时,Flow 会自动取消所有相关操作。例如在分页加载场景中,当用户快速滑动离开当前页面时,Flow 会自动取消尚未完成的网络请求,而开发者无需编写复杂的取消逻辑,大大减少了"回调地狱"和内存泄漏的风险。
- 链式操作:类似集合 API 的直观处理
Flow 提供了丰富的声明式操作符 ,允许开发者以函数式编程风格处理数据流。这些操作符(如 map、filter、transform)形成可读性极高的链式调用,这种模式将原本分散的回调逻辑整合成自顶向下的线性流程,显著提升代码可维护性,同时保持异步操作的并发能力。
- 线程切换:轻松切换 IO/主线程
Flow 通过 flowOn 操作符提供了优雅的线程控制 方案。开发者可以明确指定每个处理阶段运行的线程上下文,Flow 会自动处理线程切换的底层细节。整个过程无需回调嵌套或手动切换线程,既保证了主线程不被阻塞导致的界面卡顿(ANR),又避免了传统多线程编程中的同步陷阱,使异步代码保持简洁直观。
10.5.2 flow的基本用法
假设我们正在开发一个天气应用,需要从多个城市获取温度数据,步骤1:创建 Flow - 模拟从三个城市获取温度:
kotlin
fun fetchCityTemperatures(): Flow<Int> = flow {
// 模拟异步获取数据
delay(100) // 模拟网络延迟
emit(25) // 北京温度
delay(100) // 另一个网络请求
emit(32) // 上海温度
delay(100)
emit(18) // 哈尔滨温度
}
转换数据 - 添加温度单位并过滤高温城市
kotlin
val processedTemperatures = fetchCityTemperatures()
.map { temp -> "$temp°C" } // 添加温度单位
.filter { it.removeSuffix("°C").toInt() > 20 } // 只显示20°C以上的城市
收集数据 - 显示在UI上,在Android的ViewModel中
kotlin
class WeatherViewModel : ViewModel() {
fun displayTemperatures() {
viewModelScope.launch {
processedTemperatures.collect { temperatureString ->
// 更新UI显示
_weatherData.value = "当前城市温度: $temperatureString"
}
}
}
}
在上面这个案例中:
-
flow将多个异步操作整合到单一数据流中,
-
每个温度数据到达时立即处理(
map/filter),而不是等待所有数据到达 -
结果:北京温度 → 显示"25°C" → 上海温度 → 显示"32°C" → 哈尔滨温度被过滤
-
每次有新温度数据时自动更新UI,无需手动管理状态更新
-
如果用户离开页面(ViewModel被清除),
flow自动取消,避免后台继续获取哈尔滨温度数据
10.5.3 flow操作符
Flow 提供了丰富的操作符来处理异步数据流,这些操作符可以分成几大类。以下是全面的 Flow 操作符分类和说明:
创建操作符(Flow Builders)
| 操作符 | 说明 | 示例 |
|---|---|---|
| flow{} | 基础构建器 | flow { emit(1) } |
| flowOf() | 从固定值创建 | flowOf(1, 2, 3) |
| asFlow() | 集合转Flow | listOf(1,2,3).asFlow() |
| channelFlow | 支持复杂发射逻辑 | channelFlow { send(1) } |
| callbackFlow | 封装回调API | callbackFlow { api.registerCallback { trySend(it) } } |
转换操作符(Transformation Operators)
| 操作符 | 说明 | 示例 |
|---|---|---|
| map | 转换每个元素 | .map { it * 2 } |
| transform | 更灵活的转换 | .transform { emit(it*2) } |
| mapLatest | 只处理最新值 | .mapLatest { fetchData(it) } |
| flatMapConcat | 顺序展开流 | .flatMapConcat { getDetails(it) } |
| flatMapMerge | 并发展开流 | .flatMapMerge { getDetails(it) } |
| flatMapLatest | 只处理最新流 | .flatMapLatest { getDetails(it) } |
| scan | 累积计算 | .scan(0) { acc, value -> acc + value } |
过滤操作符(Filtering Operators)
| 操作符 | 说明 | 示例 |
|---|---|---|
| filter | 条件过滤 | .filter { it > 10 } |
| take | 取前N个 | .take(5) |
| drop | 跳过前N个 | .drop(2) |
| debounce | 防抖 | .debounce(300) |
| distinctUntilChanged | 去重 | .distinctUntilChanged() |
| sample | 采样 | .sample(1000) |
组合操作符(Combination Operators)
| 操作符 | 说明 | 示例 |
|---|---|---|
| combine | 组合最新值 | combine(flowA, flowB) { a, b -> a + b } |
| zip | 成对组合 | flowA.zip(flowB) { a, b -> a to b } |
| merge | 合并流 | merge(flowA, flowB) |
| flattenConcat | 顺序展开 | flowOf(flowA, flowB).flattenConcat() |
| flattenMerge | 并发展开 | flowOf(flowA, flowB).flattenMerge() |
异常处理操作符(Exception Handling)
| 操作符 | 说明 | 示例 |
|---|---|---|
| catch | 捕获异常 | .catch { e -> emit(fallback) } |
| onCompletion | 流完成时 | .onCompletion { println("Done") } |
| retry | 重试 | .retry(3) { it is IOException } |
| retryWhen | 条件重试 | .retryWhen { cause, attempt -> attempt < 3 } |
上下文操作符(Context Operators)
| 操作符 | 说明 | 示例 |
|---|---|---|
| flowOn | 指定上游线程 | .flowOn(Dispatchers.IO) |
| buffer | 缓冲处理 | .buffer(10) |
| conflate | 合并快速发射值 | .conflate() |
终端操作符(Terminal Operators)
| 操作符 | 说明 | 示例 |
|---|---|---|
| collect | 收集所有值 | .collect { println(it) } |
| toList | 转为列表 | .toList() |
| toSet | 转为集合 | .toSet() |
| first | 获取第一个值 | .first() |
| single | 获取唯一值 | .single() |
| count | 计数 | .count() |
| reduce | 累积计算 | .reduce { acc, value -> acc + value } |
| fold | 带初始值累积 | .fold(0) { acc, value -> acc + value } |
| launchIn | 在作用域启动 | .launchIn(viewModelScope) |
调试与辅助操作符(Debugging & Utility)
| 操作符 | 说明 | 示例 |
|---|---|---|
| onStart | 流开始前 | .onStart { println("Starting") } |
| onEach | 每个值处理前 | .onEach { println("Emitting $it") } |
| onEmpty | 空流处理 | .onEmpty { emit(0) } |
| runningReduce | 实时累积 | .runningReduce { acc, value -> acc + value } |
特殊流处理(Special Flows)
| 操作符 | 说明 | 示例 |
|---|---|---|
| stateIn | 转为StateFlow | .stateIn(viewModelScope, SharingStarted.Eagerly, 0) |
| shareIn | 转为SharedFlow | .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) |
| callbackFlow | 回调转Flow | 用于封装传统回调API |
操作符选择指南
| 需求场景 | 推荐操作符 |
|---|---|
| 异步数据转换 | map 、transform |
| 复杂异步转换 | flatMapConcat 、 flatMapMerge |
| 响应快速输入 | debounce 、conflate |
| 合并多个数据源 | combine 、zip |
| 异常处理 | catch 、 retry |
| 线程切换 | flowOn |
| 生命周期感知 | stateIn 、shareIn |
| 资源清理 | onCompletion |
掌握这些操作符后,你可以优雅地处理各种异步数据流场景。建议从基础操作符开始练习,逐步掌握更高级的组合使用方式。
10.5.4 flow的常见用法
10.5.5 网络请求分页加载
ViewModel 中定义 Flow
kotlin
class NewsViewModel : ViewModel() {
// 创建分页数据流
val newsFlow = flow {
var page = 1
while (true) {
// 模拟网络请求(实际用 Retrofit)
val news = fetchNewsFromNetwork(page)
emit(news) // 发射数据
if (news.isLastPage) break
page++
}
}.flowOn(Dispatchers.IO) // 指定网络请求在IO线程
private suspend fun fetchNewsFromNetwork(page: Int): List<News> {
delay(1000) // 模拟网络延迟
return List(10) { News("News $it - Page $page") }
}
}
Activity/Fragment 中收集
kotlin
class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 使用 lifecycleScope 确保生命周期安全
lifecycleScope.launch {
viewModel.newsFlow
.catch { e -> showError("加载失败: ${e.message}") } // 异常处理
.collect { newsList ->
// 更新UI(自动切换主线程)
updateNewsList(newsList)
}
}
}
}
10.5.6 数据库实时监听
Room 返回 Flow
kotlin
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<User>> // Room 原生支持 Flow
}
ViewModel 中处理
kotlin
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val users: Flow<List<User>> = userDao.getUsers()
.map { users ->
// 数据处理(例如:排序)
users.sortedBy { it.name }
}
}
Activity 中收集
kotlin
lifecycleScope.launch {
viewModel.users.collect { users ->
// 数据库变化时自动触发更新
recyclerView.adapter = UserAdapter(users)
}
}
11. 小结
学完这一轮内容后,可以把 Kotlin 理解为一门把类型系统、空安全、函数式写法与异步模型整合得很紧密的语言。前半段解决的是"代码怎样写得更安全、更简洁",后半段解决的是"异步任务和持续数据流怎样写得更清晰"。
如果后续继续深入 Android 开发,最值得反复练习的部分通常是集合操作、Lambda、高阶函数、空安全、协程作用域、调度器切换,以及 Flow 在分页、数据库监听和多源数据处理中的组合方式。把这些能力串起来之后,很多业务代码都会顺手很多。