Android Kotlin 全链路系统化指南:从基础语法、类型系统与面向对象,到函数式编程、集合操作、协程并发与 Flow 响应式数据流实战

前言

本文从 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 值调用,而不能对 FloatInt 或者其他数字值调用:

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 when the fractional part does not end with a 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是怎么存储数字的?

  • 基本类型:intdouble 等(直接存数值,高效)

  • 装箱类型:IntegerDouble 等(存对象,用于特殊场景)

  1. 使用可空类型的时候会触发装箱操作:

  2. Int? 中的问号(?)表示可空类型(Nullable Type),这是 Kotlin 类型系统中最重要的特性之一

  3. 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
  1. 注意:这里只是借助 Int? 可空类型以及 === 来说明装箱后的缓存现象:

  2. 除非必要,不要用 Int?,一定是优先使用基本类型

  3. 始终使用 == 比较数字,而不是使用 === 比较内存地址,因为数字比较更关心值本身是否相等

2.1.5 显式数字转换
  1. 较小的类型不会隐式地转换为较大的类型,反之亦然。例如,将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()

问题根源

10Int 类型,直接赋值给 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)

Kotlinif 不仅是语句,还可以是表达式 (有返回值),返回值由每个分支的最后一行的表达式决定:

  1. 注意,这种表达式型的写法, if 必须有 else 分支,否则会报错

  2. Kotlinif 是表达式,因此它天然就会产生值,无论是否被接,比如下面的表达式:

  3. 有变量接受,返回值就有意义。所以,当需要返回值时,必须显式指定接收位置

  4. 没有变量接受,返回值就没有意义(直接丢弃)

作为表达式返回最大值

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
  1. 主构造函数,直接在类名后声明
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 (类似 JavaObject )。使用 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 提供了四种修饰符:publicprivateprotectedinternal

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的集合、列表功能更强大,数组的使用频率相对低

  1. 对象类型数组
kotlin 复制代码
val names = arrayOf("Alice", "Bob", "Charlie")
  1. 原始类型数组(避免装箱开销)
kotlin 复制代码
val intArray = intArrayOf(1, 2, 3)       // 性能更好
val floatArray = floatArrayOf(1.5f, 2.5f)
  1. 固定大小空数组
kotlin 复制代码
val emptyArray = arrayOfNulls<String>(3) // [null, null, null]
  1. 动态初始化
kotlin 复制代码
val squares = Array(5) { i -> (i * i) } // [0, 1, 4, 9, 16]
  1. 访问元素
kotlin 复制代码
val first = names[0]         // "Alice"
names[1] = "Robert"          // 修改元素
  1. 安全访问(避免崩溃)
kotlin 复制代码
val safeValue = names.getOrNull(5) ?: "Default"
  1. 遍历数组
kotlin 复制代码
names.forEach { name ->
    Log.d("Array", name)
}
  1. 转换为集合(推荐后续使用集合操作)
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 关键注意事项
  1. return** 的陷阱**:Lambda 中直接写 return退出外层函数!需用标签返回
kotlin 复制代码
list.forEach {
   if (it == "exit") return@forEach // 只退出 Lambda
}
  1. Lambda 参数类型:若编译器无法推断类型,需显式声明
kotlin 复制代码
val callback: (String) -> Unit = { str -> println(str) }

9. 高阶函数

高阶函数(Higher-Order Function) 是指可以接受函数作为参数、或者返回一个函数的函数。将行为作为参数传递,使代码更灵活且可复用,同时保持类型安全。极大地增强了代码的抽象能力和灵活性。

9.1 语法

定义高阶函数

kotlin 复制代码
fun 函数名(参数名: (参数类型) -> 返回类型) {
    // 调用传入的函数
    参数名(实参)
}

9.2 使用案例

  1. 基础回调函数,把函数当成参数传递使用

定义高阶函数

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等机制,但这些方案普遍存在一些痛点:

  1. 代码结构混乱:多层嵌套的回调形成"回调地狱"

  2. 异常处理复杂:错误传播路径不清晰

  3. 资源管理困难:需要手动控制线程生命周期

10.1 为什么需要协程?

Kotlin协程的诞生标志着异步编程范式的重大变革。kotlin通过以下几种核心特性,直接重塑开发体验:

  1. **同步式编码:**直接用顺序逻辑表达异步操作,彻底告别回调地狱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)
        }
    }
  2. **智能的线程调度:**支持在多个线程间无缝切换,主线程与工作线程的切换成本极低,内置多种调度策略(IO/CPU密集型等)

  3. 极致的资源效率:协程运行在线程上的轻量级任务单元,协程由编程语言层面实现,而非操作系统层面的实现,高效利用线程资源,内存占用仅为几KB级别,单台设备可轻松运行数十万协程(对比传统线程的数百个上限),创建和切换开销几乎可以忽略不计。

  4. **完善的生命周期管理:**支持结构化并发,提供协作式取消机制,自动避免内存泄漏风险。

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 { ... }

这里又开启了一个子协程。asynclaunch 的区别是: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"

整个顺序可以概括成:

  1. 调用 fetchData()
  2. viewModelScope 中启动一个外层协程 launch
  3. 在外层协程里再启动一个带返回值的子协程 async
  4. 子协程先 delay(2000) 挂起 2 秒
  5. 2 秒后返回 "laosun"
  6. 外层协程通过 await() 拿到结果
  7. 结果保存到 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
        }
    }
}

ActivityViewModel、协程和 LiveData 放在一起看,这条链路可以概括为:Activity 负责触发交互和展示结果,ViewModel 负责发起异步任务并持有结果,LiveData 负责把数据变化继续传递给界面。

1. 用户点击按钮

kotlin 复制代码
findViewById<Button>(R.id.btn_start)
    .setOnClickListener { mViewModel.fetchData() }
  • 用户点击按钮
  • Activity 调用 ViewModelfetchData()

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是一个挂起函数,当它调用时,协程需要:
  1. 挂起当前协程(释放原线程资源)

  2. 切换到目标线程(如 IO 线程池)

  3. 执行完代码块后恢复到原始线程

这种"挂起-切换-恢复"的能力必须依赖挂起函数的机制实现。

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就能很好的适应这种需要多次更新数据的场景:

  1. 实时数据更新:数据库变化、传感器数据、WebSocket 消息

  2. 分页加载:分批获取网络数据

  3. 用户输入处理:搜索框连续输入时的实时搜索

  4. 多源数据组合:同时监听网络和数据库变化

10.5.1 flow的设计规则
  1. 异步数据流:按需生产和消费数据

Flow 实现了这种"按需生产-消费"的模型,与传统的一次性异步操作不同,Flow 允许数据在时间维度上持续产生和消费。数据生产者只在有收集者有需要时才产生数据,且每次只发射单个值,消费者收到值后立即处理。

  1. 结构化并发:自动跟随协程作用域取消

Flow 深度集成了 Kotlin 协程的生命周期感知能力。当启动 Flow 的协程作用域被取消时,Flow 会自动取消所有相关操作。例如在分页加载场景中,当用户快速滑动离开当前页面时,Flow 会自动取消尚未完成的网络请求,而开发者无需编写复杂的取消逻辑,大大减少了"回调地狱"和内存泄漏的风险。

  1. 链式操作:类似集合 API 的直观处理

Flow 提供了丰富的声明式操作符 ,允许开发者以函数式编程风格处理数据流。这些操作符(如 mapfiltertransform)形成可读性极高的链式调用,这种模式将原本分散的回调逻辑整合成自顶向下的线性流程,显著提升代码可维护性,同时保持异步操作的并发能力。

  1. 线程切换:轻松切换 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 在分页、数据库监听和多源数据处理中的组合方式。把这些能力串起来之后,很多业务代码都会顺手很多。

相关推荐
dapeng28703 小时前
分布式系统容错设计
开发语言·c++·算法
恋猫de小郭3 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
qq_417695053 小时前
代码热修复技术
开发语言·c++·算法
badhope7 小时前
Mobile-Skills:移动端技能可视化的创新实践
开发语言·人工智能·git·智能手机·github
码云数智-园园9 小时前
微服务架构下的分布式事务:在一致性与可用性之间寻找平衡
开发语言
C++ 老炮儿的技术栈9 小时前
volatile使用场景
linux·服务器·c语言·开发语言·c++
hz_zhangrl9 小时前
CCF-GESP 等级考试 2026年3月认证C++一级真题解析
开发语言·c++·gesp·gesp2026年3月·gespc++一级
Liu628889 小时前
C++中的工厂模式高级应用
开发语言·c++·算法
IT猿手9 小时前
基于控制障碍函数的多无人机编队动态避障控制方法研究,MATLAB代码
开发语言·matlab·无人机·openclaw·多无人机动态避障路径规划·无人机编队