Android 异步编程中协程的完整实战示例

一、全链路数据加载:网络请求 + 数据库缓存

在实际开发中,数据加载通常需要先检查本地缓存,若缓存失效则从网络获取,并将结果更新到本地。以下是完整的 MVVM 架构示例:

1. 项目结构
复制代码
app/
├── data/               # 数据层
│   ├── model/          # 数据模型
│   │   └── User.kt
│   ├── remote/         # 网络层
│   │   └── UserApiService.kt
│   ├── local/          # 本地数据库(Room)
│   │   ├── UserDao.kt
│   │   └── AppDatabase.kt
│   └── repository/     # 仓库层
│       └── UserRepository.kt
├── ui/                 # UI 层
│   └── UserListActivity.kt
└── viewmodel/          # ViewModel 层
    └── UserListViewModel.kt
2. 关键代码实现
2.1 数据模型(User.kt)
复制代码
@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: String,
    val name: String,
    val age: Int,
    val lastUpdateTime: Long = System.currentTimeMillis() // 缓存时间戳
)
2.2 网络层(UserApiService.kt)

使用 Retrofit 定义挂起函数(协程友好):

复制代码
interface UserApiService {
    @GET("users")
    suspend fun getUsersFromNetwork(): Response<List<User>>
}
2.3 本地数据库(UserDao.kt)

Room DAO 支持协程(suspend 函数自动在 IO 线程执行):

复制代码
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    suspend fun getCachedUsers(): List<User>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(users: List<User>)

    @Query("DELETE FROM users")
    suspend fun clearCache()
}
2.4 仓库层(UserRepository.kt)

协程的核心逻辑层,处理网络请求、缓存策略和数据合并:

复制代码
class UserRepository(
    private val apiService: UserApiService,
    private val userDao: UserDao
) {
    // 缓存有效期(假设 5 分钟)
    private val CACHE_DURATION = 5 * 60 * 1000L

    // 获取用户数据(优先缓存,缓存过期则从网络加载)
    suspend fun getUsers(): Result<List<User>> = withContext(Dispatchers.IO) {
        try {
            // 步骤1:检查本地缓存是否有效
            val cachedUsers = userDao.getCachedUsers()
            if (cachedUsers.isNotEmpty() && isCacheValid(cachedUsers)) {
                return@withContext Result.success(cachedUsers)
            }

            // 步骤2:缓存无效,从网络获取
            val response = apiService.getUsersFromNetwork()
            if (response.isSuccessful) {
                val remoteUsers = response.body() ?: emptyList()
                // 步骤3:更新本地缓存
                userDao.clearCache()
                userDao.insertUsers(remoteUsers)
                return@withContext Result.success(remoteUsers)
            }

            // 网络请求失败时,返回缓存(即使过期)
            if (cachedUsers.isNotEmpty()) {
                return@withContext Result.success(cachedUsers)
            }

            Result.failure(Exception("网络请求失败且无缓存"))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    // 检查缓存是否有效(取最新一条数据的时间戳)
    private fun isCacheValid(users: List<User>): Boolean {
        val latestTime = users.maxOfOrNull { it.lastUpdateTime } ?: 0L
        return System.currentTimeMillis() - latestTime < CACHE_DURATION
    }
}
2.5 ViewModel 层(UserListViewModel.kt)

使用 viewModelScope 启动协程,管理数据加载状态:

复制代码
class UserListViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            when (val result = repository.getUsers()) {
                is Result.Success -> {
                    _uiState.value = UiState.Success(result.data)
                }
                is Result.Failure -> {
                    _uiState.value = UiState.Error(result.exception.message)
                }
            }
        }
    }

    sealed class UiState {
        object Loading : UiState()
        data class Success(val users: List<User>) : UiState()
        data class Error(val message: String?) : UiState()
    }
}
2.6 UI 层(UserListActivity.kt)

观察 StateFlow 并更新 UI:

复制代码
class UserListActivity : AppCompatActivity() {
    private lateinit var binding: ActivityUserListBinding
    private val viewModel: UserListViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityUserListBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 观察 UI 状态
        lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UserListViewModel.UiState.Loading -> showLoading()
                    is UserListViewModel.UiState.Success -> showUsers(state.users)
                    is UserListViewModel.UiState.Error -> showError(state.message)
                }
            }
        }

        // 触发数据加载
        viewModel.loadUsers()
    }

    private fun showLoading() {
        binding.progressBar.visibility = View.VISIBLE
        binding.recyclerView.visibility = View.GONE
        binding.errorText.visibility = View.GONE
    }

    private fun showUsers(users: List<User>) {
        binding.progressBar.visibility = View.GONE
        binding.recyclerView.visibility = View.VISIBLE
        binding.errorText.visibility = View.GONE
        // 初始化 RecyclerView 并设置适配器
        binding.recyclerView.adapter = UserAdapter(users)
    }

    private fun showError(message: String?) {
        binding.progressBar.visibility = View.GONE
        binding.recyclerView.visibility = View.GONE
        binding.errorText.visibility = View.VISIBLE
        binding.errorText.text = message ?: "加载失败"
    }
}

二、协程的取消与资源清理

在协程中执行文件操作、网络请求或打开数据库连接时,需要确保协程取消时释放资源。以下是资源清理的完整示例

2.1 取消协程时关闭文件
复制代码
// 在 ViewModel 中启动一个协程,读取大文件并处理
fun processLargeFile(filePath: String) {
    viewModelScope.launch {
        val file = File(filePath)
        val inputStream = file.inputStream()
        try {
            // 模拟逐行读取文件(可取消)
            var line: String?
            while (isActive) { // 检查协程是否活跃
                line = inputStream.bufferedReader().readLine()
                if (line == null) break
                processLine(line) // 处理每一行数据
            }
        } finally {
            // 协程取消时,确保关闭文件流
            inputStream.close()
            Log.d("FileProcess", "文件流已关闭")
        }
    }
}
2.2 取消网络请求(Retrofit + 协程)

Retrofit 的 Call 对象支持协程取消,协程取消时会自动取消底层的网络请求:

复制代码
// 定义可取消的网络请求
suspend fun fetchData(): Result<Data> = withContext(Dispatchers.IO) {
    try {
        val response = apiService.getData() // Retrofit 的 suspend 函数
        if (response.isSuccessful) {
            Result.success(response.body()!!)
        } else {
            Result.failure(Exception("HTTP 错误: ${response.code()}"))
        }
    } catch (e: CancellationException) {
        // 协程被取消时触发,可在此记录日志或清理资源
        Log.d("Network", "请求被取消")
        throw e // 重新抛出,确保上层知道协程已取消
    } catch (e: Exception) {
        Result.failure(e)
    }
}

三、Flow 的高级用法:处理背压与热流

3.1 背压(Backpressure)处理

当生产者发射数据过快,消费者处理不过来时,使用 conflate(取最新值)或 buffer(缓存数据)解决背压问题:

复制代码
// 模拟传感器数据(每秒发射 100 次)
fun sensorDataFlow(): Flow<Int> = flow {
    var value = 0
    while (true) {
        emit(value++)
        delay(10) // 10ms 发射一次(100Hz)
    }
}.flowOn(Dispatchers.IO)

// 在 ViewModel 中收集数据(每秒处理 1 次)
fun startSensorMonitoring() {
    viewModelScope.launch {
        sensorDataFlow()
            .conflate() // 只处理最新值,丢弃中间未处理的数据
            // .buffer(10) // 缓存 10 个数据,超出则挂起生产者
            .collect { value ->
                delay(1000) // 模拟耗时处理(1Hz)
                _sensorValue.value = value
            }
    }
}
3.2 SharedFlow:多订阅者热流

SharedFlow 适用于多个订阅者需要接收同一数据流的场景(如事件广播):

复制代码
// 在 Repository 中定义 SharedFlow
class EventRepository {
    private val _eventFlow = MutableSharedFlow<Event>()
    val eventFlow: SharedFlow<Event> = _eventFlow.asSharedFlow()

    // 发送事件(如网络状态变化)
    suspend fun sendEvent(event: Event) {
        _eventFlow.emit(event)
    }
}

// 在多个 Activity/Fragment 中订阅
lifecycleScope.launch {
    eventRepository.eventFlow.collect { event ->
        when (event) {
            is Event.NetworkConnected -> updateNetworkStatus(true)
            is Event.NetworkDisconnected -> updateNetworkStatus(false)
        }
    }
}

四、协程与 WorkManager 集成:后台任务

WorkManager 是 Android 官方的后台任务调度库,支持协程。以下是使用协程实现后台数据同步的示例:

4.1 定义协程 Worker
复制代码
class DataSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            try {
                // 执行后台数据同步(调用 Repository)
                val result = repository.syncData()
                if (result.isSuccess) {
                    Result.success()
                } else {
                    Result.retry() // 失败后重试
                }
            } catch (e: Exception) {
                Result.failure()
            }
        }
    }
}
4.2 调度后台任务
复制代码
// 在需要的地方(如 Application)调度每日同步
val workRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(1, TimeUnit.DAYS)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueue(workRequest)

五、协程测试:使用 TestCoroutineDispatcher

测试协程代码时,需控制协程的执行时间和顺序。使用 kotlinx-coroutines-test 库中的 TestCoroutineDispatcherrunTest 方法:

5.1 单元测试示例
复制代码
class UserRepositoryTest {
    private lateinit var repository: UserRepository
    private lateinit var testDispatcher: TestDispatcher

    @Before
    fun setup() {
        testDispatcher = UnconfinedTestDispatcher() // 无限制调度器(立即执行)
        val apiService = mockk<UserApiService>()
        val userDao = mockk<UserDao>()
        repository = UserRepository(apiService, userDao)
    }

    @Test
    fun `getUsers 缓存有效时返回缓存数据`() = runTest(testDispatcher) {
        // 模拟缓存数据(有效期内)
        val cachedUsers = listOf(User("1", "Alice", 20, System.currentTimeMillis()))
        every { userDao.getCachedUsers() } returns cachedUsers

        val result = repository.getUsers()

        assertTrue(result is Result.Success)
        assertEquals(cachedUsers, (result as Result.Success).data)
    }

    @Test
    fun `getUsers 缓存过期时从网络加载`() = runTest(testDispatcher) {
        // 模拟过期缓存
        val expiredUsers = listOf(User("1", "Alice", 20, System.currentTimeMillis() - 10 * 60 * 1000))
        every { userDao.getCachedUsers() } returns expiredUsers

        // 模拟网络成功响应
        val remoteUsers = listOf(User("2", "Bob", 25))
        coEvery { apiService.getUsersFromNetwork() } returns Response.success(remoteUsers)

        val result = repository.getUsers()

        // 验证网络请求被调用,且缓存被更新
        coVerify { apiService.getUsersFromNetwork() }
        coVerify { userDao.insertUsers(remoteUsers) }
        assertTrue(result is Result.Success)
        assertEquals(remoteUsers, (result as Result.Success).data)
    }
}

六、总结:协程的完整使用规范

通过以上示例,可以总结出 Android 协程开发的最佳实践

  1. 结构化并发 :始终使用 lifecycleScopeviewModelScope 管理协程生命周期,避免内存泄漏。
  2. 明确线程分工 :IO 操作使用 Dispatchers.IO,计算任务使用 Dispatchers.Default,UI 更新使用 Dispatchers.Main(默认)。
  3. 异常处理分层
    • 网络 / 数据库层:返回 Result 类型或抛出可恢复异常。
    • ViewModel 层:统一捕获异常并转换为 UI 状态(如 LoadingError)。
    • UI 层:根据状态更新界面,避免在协程内直接操作 UI(通过 StateFlow/LiveData 间接更新)。
  4. 资源清理 :使用 try-finallyuse 方法确保文件流、网络连接等资源在协程取消时释放。
  5. 测试覆盖 :使用 runTestTestDispatcher 测试协程逻辑,验证数据加载、缓存策略和异常处理的正确性。

通过遵循这些规范,协程能显著提升 Android 异步代码的可读性可维护性健壮性,是现代 Android 开发的核心工具之一。

相关推荐
墨狂之逸才1 小时前
kotlin泛型实化
android·kotlin
_一条咸鱼_1 小时前
Android Runtime虚拟机实例创建与全局状态初始化(11)
android·面试·android jetpack
墨狂之逸才2 小时前
kotlin中:: 是一个非常重要的操作符,称为引用操作符
android·kotlin
工业互联网专业2 小时前
基于Android的记录生活APP_springboot+vue
android·vue.js·spring boot·毕业设计·源码·课程设计·记录生活app
小陶来咯2 小时前
【仿muduo库实现并发服务器】实现时间轮定时器
android·运维·服务器
Yeah_0day3 小时前
移动安全Android——客户端数据安全
android·客户端数据安全·本地文件权限配置·本地文件内容安全·本地日志内容安全
Leoysq4 小时前
Unity链接Mysql 数据库实现注册登录
android·adb
Yeah_0day5 小时前
移动安全Android——客户端静态安全
android·app测试·安卓客户端测试·组件导出安全测试·安装包签名·反编译保护·应用完整性校验
奔跑吧 android11 小时前
【android bluetooth 协议分析 02】【bluetooth hal 层详解 6】【bt_vendor_opcode_t 介绍】
android·hal·bt·aosp13·hidl_1.0
zhifanxu15 小时前
Android开发常用Kotlin高级语法
android·开发语言·kotlin