Kotlin语言和协程学习笔记

一、空安全概念

总结一下,Kotlin引入了空安全的概念,并在编译时开展变量是否为空的校验。相关的操作符说明概括如下:

  • 类型后加?:声明可空类型(如String?)
  • 变量后加?:安全调用(如user?.updateProfile(),为空则返回null)
  • ?:运算符:空合并操作符(a ?: b,a为空时取b)
  • !!运算符:非空断言(跳过校验,运行时为空抛异常)
// 不使用let
if (user != null) {
    user.updateProfile()
    user.saveToDatabase()
}

// 使用let更简洁
user?.let {
    it.updateProfile()
    it.saveToDatabase()
}
image.png

二、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)中声明。

image.png

runBlocking 和 launch 是 Kotlin 协程(Coroutines)中用于启动和管理协程的两个重要函数,但它们有不同的用途和适用场景。

三、object 和 伴生对象(companion object)

在 Kotlin 中,object 和 伴生对象(companion object)都是用于定义单例或静态成员的方式,但它们在用途、语法和行为上有一些关键区别。下面详细说明它们的区别:

  • 伴生对象可以访问外部类的 private 成员(即使没有实例)——这是它和普通 object 的一个重要区别。
  • 如果你需要一个 全局唯一的工具对象或单例,用 object。如果你想为某个类提供 类似 Java 的静态方法/属性,用 companion object。
  • 两者都利用了 Kotlin 的单例机制,但 伴生对象是类的一部分,而 普通 object 是独立的。
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) 
}
image.png

suspend关键字

作用:标记可挂起函数(不阻塞线程,暂停执行)

  1. 挂起函数:用 suspend 关键字标记的函数,可以在不阻塞线程的情况下暂停执行。
  2. 挂起点:挂起函数内部可以包含挂起点(如调用其他挂起函数),在这些点上函数会暂时停止执行,并允许其他任务在同一线程上运行。
  3. 恢复执行:当挂起的原因解除后(例如网络请求完成、延迟时间到达等),函数会从挂起点继续执行。

特性:

  • 只能在协程作用域(如launch/async)中调用
  • 非阻塞:挂起函数不会阻塞线程,而是将控制权交还给调度器,使得其他任务可以继续执行。
  • 状态保存:挂起函数的状态会被保存,以便在恢复时能够继续执行。
image.png

示例

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处理异常
image.png
image.png
  • 例子:网络请求 + 本地数据库缓存(线程切换+结构化并发)
    场景:先查本地数据库,若无缓存则请求网络,成功后存数据库并更新 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 擅长处理连续的异步数据流(如实时搜索、传感器数据、数据库监听)。二者结合可覆盖几乎所有异步场景:

image.png
  • 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) // 设置默认上下文
}

七、类

  • 密封类和枚举类:密封类是特殊的枚举类,密封类有着枚举的类型功能还能像对象一样携带数据。

密封类 = 枚举的类型安全优势 + 对象的灵活数据承载能力
当需要固定常量集合 → 用枚举类
当需要异构状态表示 → 用密封类

关键差异详解
  1. 数据存储能力(您提到的核心区别)
// 枚举:所有实例强制相同数据结构
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()
}
  1. 实例创建方式
// 枚举:只能使用预定义实例
val status = PaymentStatus.SUCCESS

// 密封类:可动态创建实例
val result1 = PaymentResult.Success("txn_123", 99.9)
val result2 = PaymentResult.Failed(404, "Not found")
  1. 类型系统优势
密封类适用场景: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)
    }
}
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容