一、空安全概念
总结一下,Kotlin引入了空安全的概念,并在编译时开展变量是否为空的校验。相关的操作符说明概括如下:
- 类型后加?:声明可空类型(如String?)
- 变量后加?:安全调用(如user?.updateProfile(),为空则返回null)
- ?:运算符:空合并操作符(a ?: b,a为空时取b)
- !!运算符:非空断言(跳过校验,运行时为空抛异常)
// 不使用let
if (user != null) {
user.updateProfile()
user.saveToDatabase()
}
// 使用let更简洁
user?.let {
it.updateProfile()
it.saveToDatabase()
}

二、const有无修饰添加的区别
(1)const val 修饰的属性相当于java中的public final static修饰的常量,可以通过类名直接访问。
(2)val 修饰的属性相当于java中private final static修饰的常量,由于可见行为private,所以只能通过生成getter方法访问。
(3)出于性能考虑,使用const val方式可以避免频繁函数调用。
(4)const只能修饰val,不能修饰var类型变量。const 只允许在top-level级别和object(伴生对象companion也是obejct)中声明。

runBlocking 和 launch 是 Kotlin 协程(Coroutines)中用于启动和管理协程的两个重要函数,但它们有不同的用途和适用场景。
三、object 和 伴生对象(companion object)
在 Kotlin 中,object 和 伴生对象(companion object)都是用于定义单例或静态成员的方式,但它们在用途、语法和行为上有一些关键区别。下面详细说明它们的区别:
- 伴生对象可以访问外部类的 private 成员(即使没有实例)——这是它和普通 object 的一个重要区别。
如果你需要一个 全局唯一的工具对象或单例,用 object。如果你想为某个类提供 类似 Java 的静态方法/属性,用 companion object。- 两者都利用了 Kotlin 的单例机制,但 伴生对象是类的一部分,而 普通 object 是独立的。




四、协程与Flow流
在 Android 开发中,协程(Coroutines) 和 Handler 都用于解决异步任务调度问题,但协程通过更简洁的语法、结构化并发模型和强大的生态整合,逐渐成为 Handler 的现代替代方案。
而 Flow 则是协程库中用于处理异步数据流的工具,二者紧密关联——Flow 基于协程实现,是协程在流式数据处理场景下的延伸。Flow 是协程的“数据流版本”,协程是 Flow 的“运行载体”。
runBlocking与launch
-
runBlocking
特性:阻塞当前线程,直到内部所有协程完成
场景:测试、桥接阻塞代码与非协程代码
示例
import kotlinx.coroutines.*
fun main() = runBlocking {
// 在新的协程上下文中启动一个协程
val job = launch {
delay(1000L) // 非阻塞的等待1秒
println("World!") // 延迟后打印
}
job.join() // 等待协程完成
println("Done!") // 协程完成后打印
}
-
launch
launch 是一个非阻塞函数,用于在协程作用域(CoroutineScope)中启动一个新的协程。它不会阻塞当前线程,而是立即返回,允许其他代码继续执行。
使用场景
特性:非阻塞启动协程,立即返回Job对象
场景:并发执行任务、结构化并发
示例
import kotlinx.coroutines.*
fun main() = runBlocking {
// 在runBlocking的协程上下文中启动一个新的协程
launch {
delay(1000L) // 非阻塞的等待1秒
println("World!") // 延迟后打印
}
println("Hello,") // 立即打印,不会等待上面的协程完成
// 等待一段时间以确保上面的协程有机会完成(仅用于示例,实际使用中应避免这种做法)
delay(2000L)
}

suspend关键字
作用:标记可挂起函数(不阻塞线程,暂停执行)
-
挂起函数:用
suspend关键字标记的函数,可以在不阻塞线程的情况下暂停执行。 - 挂起点:挂起函数内部可以包含挂起点(如调用其他挂起函数),在这些点上函数会暂时停止执行,并允许其他任务在同一线程上运行。
- 恢复执行:当挂起的原因解除后(例如网络请求完成、延迟时间到达等),函数会从挂起点继续执行。
特性:
- 只能在协程作用域(如launch/async)中调用
- 非阻塞:挂起函数不会阻塞线程,而是将控制权交还给调度器,使得其他任务可以继续执行。
- 状态保存:挂起函数的状态会被保存,以便在恢复时能够继续执行。

示例
suspend fun fetchData(): String {
delay(1000) // 挂起点(非阻塞延迟),模拟耗时操作
return "Data"
}
fun main() = runBlocking {
val data = fetchData() // 在协程作用域中调用
println(data)
}
withContext函数
作用:withContext 是一个挂起函数,用于切换协程调度器(如IO/Main),执行代码块后自动恢复原调度器
常用调度器:
- Dispatchers.IO:I/O操作(网络/文件)
- Dispatchers.Main:主线程(UI更新)
- Dispatchers.Default:CPU密集型任务
//数据请求函数,挂起函数
suspend fun fetchUrl(url: String): String = withContext(Dispatchers.IO) {
URL(url).readText() // 在IO线程执行
}
// ViewModel中使用
fun loadData() {
viewModelScope.launch {
val data = fetchUrl("https://example.com")
_uiState.value = data // 自动切回Main线程,这里可以利用
}
}
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel._uiState.observe(this, Observer { data ->
// 主线程更新UI
textView.setText("Received data: $data")
})
viewModel.loadData( )
}
}
当你在一个协程作用域内使用 withContext 切换到 Dispatchers.IO 执行网络请求后,该协程会自动返回到原来的调度器(通常是 Dispatchers.Main)继续执行后续代码。因此,你可以利用这一点来确保在主线程上更新 UI。
五、协程的一般使用
在 Android 开发中,协程(Coroutines) 和 Handler 都用于解决异步任务调度问题,但协程通过更简洁的语法、结构化并发模型和强大的生态整合,逐渐成为 Handler 的现代替代方案。
核心组件
- 协程作用域:管理协程生命周期(如runBlocking、viewModelScope)
- Job:协程任务句柄(可取消/等待)
- 调度器:指定协程运行线程(Dispatchers)
// 启动协程
runBlocking {
launch(Dispatchers.IO) { /* 后台任务 */ }
launch(Dispatchers.Main) { /* UI更新 */ }
}
// 结构化并发(父子协程)
val parentJob = launch {
val childJob = launch { /* 子任务 */ }
childJob.join() // 等待子协程完成
}
最佳实践
- 避免GlobalScope(易导致内存泄漏)
- 使用viewModelScope/lifecycleScope绑定生命周期
- 用try-catch或CoroutineExceptionHandler处理异常


- 例子:网络请求 + 本地数据库缓存(线程切换+结构化并发)
场景:先查本地数据库,若无缓存则请求网络,成功后存数据库并更新 UI。
Handler 写法(痛点:多层回调嵌套、线程混乱)
// 查本地数据库(子线程)
thread {
val cache = db.dao().queryData()
if (cache != null) {
handler.post { updateUI(cache) } // 有缓存,切主线程更新
} else {
// 无缓存,请求网络(子线程)
val networkData = apiService.fetchData()
// 存数据库(子线程)
thread { db.dao().insert(networkData) }
handler.post { updateUI(networkData) } // 切主线程更新
}
}
协程写法(优势:线性代码、结构化并发、异常统一处理)
lifecycleScope.launch {
try {
// 1. 查本地数据库(切 IO 线程)
val cache = withContext(Dispatchers.IO) {
db.dao().queryData()
}
if (cache != null) {
updateUI(cache) // 有缓存,直接更新(自动主线程)
} else {
// 2. 无缓存,请求网络(切 IO 线程)
val networkData = withContext(Dispatchers.IO) {
apiService.fetchData()
}
// 3. 存数据库(切 IO 线程,并行执行不影响 UI)
withContext(Dispatchers.IO) {
db.dao().insert(networkData)
}
updateUI(networkData) // withContext任务执行完后自动切回主线程,更新 UI
}
} catch (e: Exception) {
showError(e) // 统一捕获异常(网络错误、数据库错误等)
}
}
六、Flow和协程结合使用
Flow 是 Kotlin 协程库(kotlinx.coroutines)中用于处理异步数据流的响应式编程工具,它基于协程实现,是协程在“连续数据流”场景下的延伸。二者的联系可概括为:Flow 是协程的“数据流版本”,协程是 Flow 的“运行载体”。
它与协程结合使用,可以让你以响应式编程的方式处理一系列值,并且可以在这些值上应用各种操作符(如转换、过滤等)。 Flow 特别适合处理需要连续发出多个值的场景,例如网络请求、传感器数据读取等。
-
1、Flow 基础概念
Flow 被设计为“冷流(Cold Stream)”:只有被收集(collect)时才会执行发射数据的逻辑,且每次收集都会重新执行整个流程(类似函数调用)。其核心依赖协程的挂起函数实现非阻塞数据流传输
Flow:表示一个可以发出多个值的数据流。
emit:从 Flow 中发出一个值。
collect:收集 Flow 发出的值并进行处理。
-
2、Flow 的所有操作(发射、转换、收集)都必须在协程作用域内进行,因为:
emit和 collect是挂起函数,需协程调度器支持挂起/恢复。
Flow 的操作符(如 map、filter、debounce)本质是挂起函数,可在协程中串行/并行执行。
// 定义一个 Flow:每秒发射一个递增数字(冷流,仅 collect 时执行)
fun numberFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000) // 挂起 1s(协程的 delay,不阻塞线程)
emit(i) // 发射数据(挂起函数)
}
}
// 在协程中收集 Flow 数据
lifecycleScope.launch {
numberFlow()
.filter { it % 2 == 0 } // 过滤偶数(挂起操作符)
.map { "Number: $it" } // 转换格式(挂起操作符)
.collect { value -> // 收集数据(挂起函数)
Log.d("Flow", value) // 输出:Number: 2, Number: 4
}
}
-
3. Flow 扩展了协程的应用场景
协程擅长处理单个异步任务(如网络请求、文件读写),而 Flow 擅长处理连续的异步数据流(如实时搜索、传感器数据、数据库监听)。二者结合可覆盖几乎所有异步场景:

-
4. Flow 与协程的协同:操作符与上下文
Flow 提供丰富的操作符(如 map、filter、combine、flatMapConcat),这些操作符本质是挂起函数,依赖协程实现:
切换调度器:通过 flowOn(Dispatchers.IO)指定 Flow 发射数据的线程(不影响收集线程)。
背压处理:通过 buffer()、conflate()、collectLatest()处理上游发射速度 > 下游处理速度的问题(协程原生支持挂起等待)。
异常处理:通过 catch { ... }捕获 Flow 执行中的异常(类似协程的 try-catch)。
-
5. 总结:协程与 Flow 的关系
依赖关系:Flow 是协程库的组成部分,必须运行在协程作用域内,其挂起函数特性完全依赖协程的调度和执行机制。
分工关系:协程处理“离散的异步任务”,Flow 处理“连续的数据流”,二者共同构成 Kotlin 异步编程的完整生态。
目标一致:都是通过声明式语法、非阻塞挂起、结构化并发,简化异步代码,避免回调地狱和线程管理复杂度。
协程与 Flow 协同的例子(数据流场景)
Flow 基于协程,用于处理 连续异步数据流(如实时搜索、传感器数据、数据库监听),以下是高频场景例子。
例子1:实时搜索(输入防抖 + 取消过时请求)
场景:搜索框输入时,防抖 300ms 后请求接口,若输入变化则取消上一次未完成的请求。
Flow 实现(核心:debounce防抖 + flatMapLatest取消过时请求)
核心逻辑:
debounce(300):输入停止 300ms 后才发射数据(避免频繁请求)。
flatMapLatest:当新输入到来时,取消上一个未完成的 flow { emit(apiService.search(q)) },只处理最新输入(避免过时结果覆盖新结果)。
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
val query = remember { mutableStateOf("") }
val searchResult = viewModel.searchResult.collectAsState(initial = emptyList())
TextField(
value = query.value,
onValueChange = { newQuery ->
query.value = newQuery
viewModel.onQueryChanged(newQuery) // 通知 ViewModel 输入变化
}
)
LazyColumn { items(searchResult.value) { item -> Text(item) } }
}
// ViewModel 中用 Flow 处理搜索逻辑
class SearchViewModel : ViewModel() {
private val _searchResult = MutableStateFlow<List<String>>(emptyList())
val searchResult: StateFlow<List<String>> = _searchResult.asStateFlow()
// 输入变化触发 Flow
fun onQueryChanged(query: String) {
viewModelScope.launch {
// 用 channelFlow 将输入转为 Flow(或用 stateIn 包装)
flowOf(query)
.debounce(300) // 防抖:300ms 内无新输入才继续
.filter { it.isNotEmpty() } // 过滤空输入
.flatMapLatest { q -> // 取消上一次未完成的请求,只保留最新
// 发起网络请求(返回 Flow)
flow { emit(apiService.search(q)) }
.catch { emit(emptyList()) } // 捕获异常,返回空列表
}
.collect { result -> // 收集结果,更新 UI
_searchResult.value = result
}
}
}
}
例子2:倒计时功能(Flow 发射序列数据)
场景:实现 60 秒倒计时,每秒更新 UI(如验证码倒计时)。
Flow 实现(用 flow发射递减序列)
优势:用 flow手动发射序列数据,delay(1000)控制节奏,onStart/onCompletion处理边界状态,代码简洁直观。
// 定义倒计时 Flow(从 totalSeconds 递减到 0)
fun countdownFlow(totalSeconds: Int): Flow<Int> = flow {
var remaining = totalSeconds
while (remaining >= 0) {
emit(remaining) // 发射当前剩余秒数
if (remaining > 0) delay(1000) // 挂起 1 秒(非阻塞)
remaining--
}
}
// 在协程中收集并显示
lifecycleScope.launch {
countdownFlow(60)
.onStart { button.isEnabled = false } // 开始时禁用按钮
.onCompletion { button.isEnabled = true } // 结束时启用按钮
.collect { seconds ->
button.text = if (seconds > 0) "剩余 ${seconds}s" else "重新发送"
}
}
例子3:数据库数据监听(Room + Flow 实时更新 UI)
场景:数据库表数据变化时,自动通知 UI 更新(替代 LiveData或手动轮询)。用 Flow/StateFlow(现代方案,推荐 Kotlin 项目)
核心:Room 的 @Query方法返回 Flow<T>(冷流),数据变化时发射新值。ViewModel 中用 stateIn将 Flow 转为 StateFlow(热流,有初始值),UI 层用 collectAsState收集。
//---Step 1:DAO 层定义(返回 Flow)
@Dao
interface UserDao {
// 返回 Flow,数据变化时自动发射新值(冷流,collect 时才监听)
@Query("SELECT * FROM user")
fun getAllUsersFlow(): Flow<List<User>> // 注意返回类型是 Flow
}
//---Step 2:ViewModel 暴露 StateFlow
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val db = AppDatabase.getInstance(application)
private val userDao = db.userDao()
// 用 stateIn 将 Flow 转为 StateFlow(热流,有初始值,自动管理生命周期)
val usersStateFlow: StateFlow<List<User>> = userDao.getAllUsersFlow()
.stateIn(
scope = viewModelScope, // 绑定 ViewModel 生命周期(自动取消)
started = SharingStarted.WhileSubscribed(5000), // 停止订阅 5s 后取消 Flow
initialValue = emptyList() // 初始值(空列表)
)
// (可选)如果想兼容旧代码,也可转为 LiveData:
val usersLiveData: LiveData<List<User>> = usersStateFlow.asLiveData()
}
//---Step 3:UI 层收集 Flow(Activity/Fragment 用协程)
class UserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: UserViewModel by viewModels()
// 用 lifecycleScope 启动协程收集 Flow
lifecycleScope.launch {
//通过 repeatOnLifecycle(Lifecycle.State.STARTED),确保 Flow 仅在 Activity 处于前台
// 可见状态(STARTED 及以上) 时收集,后台时自动取消,回到前台时重新收集(冷流会
// 重新发射数据,热流如 SharedFlow会从最新状态恢复)。避免了后台无效收集,浪费资源,状态错乱(重复收集导致 UI 异常)
repeatOnLifecycle(Lifecycle.State.STARTED) { // 页面可见时才收集
viewModel.usersStateFlow.collect { userList ->
adapter.submitList(userList) // 更新 UI
}
}
}
}
}
//---Step 3(Compose 版):用 collectAsStateWithLifecycle收集 Flow
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
// 将 StateFlow 转为 Compose 可观察的 State(自动感知生命周期)collectAsStateWithLifecycle
// 内部已集成 repeatOnLifecycle逻辑,自动绑定组件生命周期(默认 STARTED状态)
val userList by viewModel.usersStateFlow.collectAsStateWithLifecycle(
initialValue = emptyList(),
lifecycle = LocalLifecycleOwner.current.lifecycle
)
LazyColumn {
items(userList) { user ->
Text(text = user.name)
}
}
}
例子4:事件总线(全局事件通知,替代 EventBus)
场景:跨页面发送事件(如登录成功通知个人中心刷新)。
Flow 实现(单例 SharedFlow)
优势:基于协程的 SharedFlow实现事件总线,类型安全(密封类),无第三方库依赖,支持多订阅者和背压控制。
// 全局事件总线(SharedFlow 是热流,可多订阅者接收)
object EventBus {
// 用 MutableSharedFlow 发射事件( replay=0 不缓存历史事件, extraBufferCapacity=1 缓冲 1 个事件)
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
// 发送事件
suspend fun sendEvent(event: Event) {
_events.emit(event) // 挂起函数,需协程环境
}
// 事件类型密封类
sealed class Event {
object LoginSuccess : Event()
data class UpdateProfile(val userId: String) : Event()
}
}
// 发送事件(如在登录页)
lifecycleScope.launch {
EventBus.sendEvent(EventBus.Event.LoginSuccess)
}
// 接收事件(如在个人中心页)
lifecycleScope.launch {
EventBus.events.collect { event ->
when (event) {
is EventBus.Event.LoginSuccess -> refreshProfile()
is EventBus.Event.UpdateProfile -> updateUI(event.userId)
}
}
}
-
6.flowOn是 Flow 提供的全局上下文切换操作符
flowOn用于改变上游所有操作的线程(对下游操作符无影响)。但它的局限性是:只能设置一个全局上下文,无法为 Flow 中的不同阶段设置不同上下文。此时 withContext更灵活,提供更细粒度的线程控制。
结合使用的核心价值在于:在 Flow 的数据流处理过程中,为不同阶段的操作精确分配最优线程资源,避免线程滥用或阻塞。
只在必要时切换上下文,优先用 flowOn做全局切换,用 withContext做局部细粒度切换。这样既能保证性能,又能保持代码可读性。
用 flowOn(全局切换上游上下文):
flow {
// 此代码块在调用协程的上下文执行(如 Main)
val data = fetchData() // 若在 IO 线程执行,需 flowOn(IO)
emit(data)
}.map { process(it) } // 在调用协程上下文执行(如 Main)
.flowOn(Dispatchers.IO) // 上游(flow{} 和 map 前)切换到 IO 线程
用 withContext(细粒度切换):
flow {
// 仅 fetchData 用 IO 线程,其他操作保持原上下文
val data = withContext(Dispatchers.IO) { fetchData() }
emit(data)
}.map {
// 仅 process 用 Default 线程
withContext(Dispatchers.Default) { process(it) }
}
完整示例:电商应用数据流
class ProductRepository @Inject constructor(
private val api: ApiService,
private val db: ProductDatabase
) {
fun getProductDetails(productId: String): Flow<ProductDetail> = flow {
// 1. 尝试从本地缓存获取
var product = withContext(Dispatchers.IO) {
db.productDao().getById(productId)
}
if (product != null) {
emit(ProductDetail.Cached(product))
}
// 2. 从网络获取最新数据
try {
val networkProduct = withContext(Dispatchers.IO) {
api.getProduct(productId)
}
// 3. 更新本地缓存
withContext(Dispatchers.IO) {
db.productDao().insert(networkProduct)
}
// 4. 处理图片URL(CPU密集型)
val processedImages = withContext(Dispatchers.Default) {
processImageUrls(networkProduct.images)
}
// 5. 组合最终结果
val detail = networkProduct.copy(images = processedImages)
emit(ProductDetail.Fresh(detail))
} catch (e: Exception) {
if (product == null) emit(ProductDetail.Error(e))
}
}.catch { e ->
emit(ProductDetail.Error(e))
}.flowOn(Dispatchers.IO) // 设置默认上下文
}
七、类
- 密封类和枚举类:密封类是特殊的枚举类,密封类有着枚举的类型功能还能像对象一样携带数据。
密封类 = 枚举的类型安全优势 + 对象的灵活数据承载能力
当需要固定常量集合 → 用枚举类
当需要异构状态表示 → 用密封类
关键差异详解
- 数据存储能力(您提到的核心区别)
// 枚举:所有实例强制相同数据结构
enum class PaymentStatus(val code: Int) {
SUCCESS(200),
PENDING(100),
FAILED(500)
}
// 密封类:不同子类可携带完全不同数据
sealed class PaymentResult {
data class Success(val transactionId: String, val amount: Double) : PaymentResult()
object Pending : PaymentResult() // 无数据
data class Failed(val errorCode: Int, val reason: String?) : PaymentResult()
}
- 实例创建方式
// 枚举:只能使用预定义实例
val status = PaymentStatus.SUCCESS
// 密封类:可动态创建实例
val result1 = PaymentResult.Success("txn_123", 99.9)
val result2 = PaymentResult.Failed(404, "Not found")
- 类型系统优势
密封类适用场景:UI状态管理
sealed class UiState {
object Loading : UiState()
data class Content(val items: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
@Composable
fun Screen(viewModel: MyViewModel) {
val state = viewModel.state.collectAsState().value
when(state) {
is UiState.Loading -> ShowProgress()
is UiState.Content -> ShowList(state.items)
is UiState.Error -> ShowError(state.message)
}
}
