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 } ?: 0Lreturn 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.Loadingwhen (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()}class UserListViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory {override fun <T : ViewModel> create(modelClass: Class<T>): T {if (modelClass.isAssignableFrom(UserListViewModel::class.java)) {@Suppress("UNCHECKED_CAST")return UserListViewModel(repository) as T}throw IllegalArgumentException("Unknown ViewModel class")}}
}
2.6 UI 层(UserListActivity.kt)
观察 StateFlow
并更新 UI:
class UserListActivity : AppCompatActivity() {private lateinit var binding: ActivityUserListBindingprivate val viewModel: UserListViewModel by viewModels {UserListViewModelFactory(UserRepository()) // 这里需要提供UserRepository的实例}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.VISIBLEbinding.recyclerView.visibility = View.GONEbinding.errorText.visibility = View.GONE}private fun showUsers(users: List<User>) {binding.progressBar.visibility = View.GONEbinding.recyclerView.visibility = View.VISIBLEbinding.errorText.visibility = View.GONE// 初始化 RecyclerView 并设置适配器binding.recyclerView.adapter = UserAdapter(users)}private fun showError(message: String?) {binding.progressBar.visibility = View.GONEbinding.recyclerView.visibility = View.GONEbinding.errorText.visibility = View.VISIBLEbinding.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) breakprocessLine(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 = 0while (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
库中的 TestCoroutineDispatcher
或 runTest
方法:
5.1 单元测试示例
class UserRepositoryTest {private lateinit var repository: UserRepositoryprivate lateinit var testDispatcher: TestDispatcher@Beforefun setup() {testDispatcher = UnconfinedTestDispatcher() // 无限制调度器(立即执行)val apiService = mockk<UserApiService>()val userDao = mockk<UserDao>()repository = UserRepository(apiService, userDao)}@Testfun `getUsers 缓存有效时返回缓存数据`() = runTest(testDispatcher) {// 模拟缓存数据(有效期内)val cachedUsers = listOf(User("1", "Alice", 20, System.currentTimeMillis()))every { userDao.getCachedUsers() } returns cachedUsersval result = repository.getUsers()assertTrue(result is Result.Success)assertEquals(cachedUsers, (result as Result.Success).data)}@Testfun `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 协程开发的最佳实践:
- 结构化并发:始终使用
lifecycleScope
或viewModelScope
管理协程生命周期,避免内存泄漏。 - 明确线程分工:IO 操作使用
Dispatchers.IO
,计算任务使用Dispatchers.Default
,UI 更新使用Dispatchers.Main
(默认)。 - 异常处理分层:
- 网络 / 数据库层:返回
Result
类型或抛出可恢复异常。 - ViewModel 层:统一捕获异常并转换为 UI 状态(如
Loading
、Error
)。 - UI 层:根据状态更新界面,避免在协程内直接操作 UI(通过
StateFlow
/LiveData
间接更新)。
- 网络 / 数据库层:返回
- 资源清理:使用
try-finally
或use
方法确保文件流、网络连接等资源在协程取消时释放。 - 测试覆盖:使用
runTest
和TestDispatcher
测试协程逻辑,验证数据加载、缓存策略和异常处理的正确性。
通过遵循这些规范,协程能显著提升 Android 异步代码的可读性、可维护性和健壮性,是现代 Android 开发的核心工具之一。
相关文章:
Android 异步编程中协程的完整实战示例
一、全链路数据加载:网络请求 数据库缓存 在实际开发中,数据加载通常需要先检查本地缓存,若缓存失效则从网络获取,并将结果更新到本地。以下是完整的 MVVM 架构示例: 1. 项目结构 app/ ├── data/ …...

多部手机连接同一wifi的ip一样吗?
在家庭和办公环境中,多台手机同时连接同一个WiFi路由器已成为常态。不少用户会产生疑问:这些设备的IP地址会相同吗?下面就一起来了解一下吧。 一、多部手机连接同一WiFi的IP一样吗 多部手机连接同一WiFi时的IP地址是否相同,需要…...

大语言模型值ollama使用(1)
ollama为本地调用大语言模型提供了便捷的方式。下面列举如何在windows系统中快捷调用ollama。 winR打开运行框,输入cmd 1、输入ollama list 显示已下载模型 2、输入ollama pull llama3 下载llama3模型 3、 输入 ollama run llama3 运行模型 4、其他 ollama li…...
大模型应用开发之Langchain
一、框架简述 Langchain 是一个用于构建和管理 LLM 应用的开发框架。它为开发者提供了工具和接口,以便于更轻松地将大语言模型集成到应用程序中,并处理语言模型生成的响应、管理对话状态、执行链式调用、处理多步任务等。 二、Langchain主要模块 1、M…...

thc-ssl-dos:SSL 压力测试的轻量级工具!全参数详细教程!Kali Linux教程!
简介 THC-SSL-DOS 是一款用于验证 SSL 性能的工具。 建立安全的 SSL 连接需要服务器比客户端高 15 倍的处理能力。 THC-SSL-DOS 利用这种不对称特性,通过使服务器过载并使其断网。 此问题影响当今所有 SSL 实现。供应商自 2003 年以来就已意识到这个问题&#x…...
什么是内网ip证书
内网IP证书是一种基于公钥基础设施(PKI)技术的数字证书,专门用于保护企业内部网络中通过IP地址访问服务的通信安全。以下是对内网IP证书的详细解析: 一、核心定义与用途 定义:内网IP证书是SSL/TLS证书的一种特殊类型…...

【速通RAG实战:进阶】17、AI视频打点全攻略:从技术实现到媒体工作流提效的实战指南
一、AI视频打点的技术底层与数据处理流程 (一)视频内容结构化的核心技术栈 AI视频打点的本质是将非结构化视频数据转化为带时间戳的结构化信息,其技术流程涵盖音视频处理、语音识别、自然语言处理三大核心模块,形成“数据采集-内容解析-智能标记-协同应用”的完整闭环。 …...

立控信息智能装备柜:科技赋能军队装备管理现代化
在军事装备管理领域,高效、安全、智能化的存储解决方案至关重要。传统的人工管理模式不仅效率低下,还容易因人为疏忽导致装备丢失或管理混乱。LKONE智能装备柜凭借先进的物联网技术、生物识别安全系统和智能管理功能,为军队提供了一套高效、…...

【freertos-kernel】queue(发送)
文章目录 补充各种yeildTCB的xStateListItem和xEventListItem xQueueGenericSendprvCopyDataToQueueprvNotifyQueueSetContainervTaskInternalSetTimeOutStatevTaskSuspendAllxTaskResumeAllprvLockQueueprvUnlockQueueprvIncrementQueueTxLockvTaskPlaceOnEventListprvAddCurr…...
【华为云物联网】如何实现在 MQTT.fx 上模拟数据间隔上传一次,并按设定系数变动数据
虽然 MQTT.fx 本身不支持定时循环脚本发送消息,但可以通过以下方式 实现在 MQTT.fx 上模拟设备参数每隔 1 分钟上传一次,并按设定系数变动数据: ✅ 推荐方式:使用 Python 脚本+MQTT.fx 联动观察 你将用 Python 自动发送数据,MQTT.fx 订阅对应主题观察是否发送成功。 🧩…...

破解高原运维难题:分布式光伏智能监控系统的应用研究
安科瑞刘鸿鹏 摘要 高原地区光照资源丰富,具有发展分布式光伏发电的巨大潜力。然而,该地区复杂的气候环境、地形地貌和运维条件对光伏电站的运行与维护带来严峻挑战。本文结合Acrel1000DP分布式光伏监控系统的技术特点和典型应用案例,探讨其…...

图标变白,开始菜单栏无法打开程序(以jupyter为例)
不知道是本人删了一些东西导致的还是什么原因,总之现在本人的jupyter只能通过命令行打开,以往我是从开始菜单栏打开。琢磨了一段时间,发现是.ico文件没有了。重新在网上下载图片,用网站图片转 ico 图标 - 锤子在线工具 转换一下格…...

大语言模型(LLM)入门 - (1) 相关概念
文章来自:大语言模型(LLM)小白入门自学项目-TiaoYu-1 GitHub - tiaoyu1122/TiaoYu-1: For People! For Freedom!For People! For Freedom! Contribute to tiaoyu1122/TiaoYu-1 development by creating an account on GitHub.https://github.com/tiaoyu1122/TiaoYu…...

行为型:访问者模式
目录 1、核心思想 2、实现方式 2.1 模式结构 2.2 实现案例 3、优缺点分析 4、适用场景 1、核心思想 目的:数据结构稳定的情况下,解决数据与算法的耦合问题。适用于对象结构稳定但需频繁扩展操作的场景。 实现:在访问数据时根据数据类…...

C++数据结构 : 哈希表的实现
C数据结构 : 哈希表的实现 目录 C数据结构 : 哈希表的实现引言1. 哈希概念1.1 直接定址法1.2 哈希冲突1.3 负载因子 2. 哈希函数2.1 除法散列法/除留余数法2.2 乘法散列法(了解)2.3 全域散列法(了解) 3. 处…...
抖音电商客户端一面面经
抖音电商客户端一面面经 时间: 25.05.30 岗位: 抖音电商客户端开发工程师 形式: 技术一面 刚刚结束了字节跳动抖音电商客户端开发工程师岗位的技术一面,整体感觉考察范围非常全面,涵盖了基础、项目、算法、系统设计等…...
JavaScript 在 AcroForm 中的广泛应用
在Adobe表单(特别是SAP Interactive Forms by Adobe)中使用JavaScript的各种技巧和方法,下面这些代码片段可以帮助开发者更高效地处理表单逻辑和交互。 1. 获取数据内容 从上下文结构中获取数据 var LV_DATA = xfa.resolveNode("$record.IM_TEST.FIELDNAME").val…...
Socket编程之TCP套件字
基于的TCP套件字编程流程 1. Socket套接字 Socket是一个编程接口(网络编程接口),是一种特殊的文件描述符(write/read)。Socket并不 仅限于TCP/IP Socket独立于具体协议的编程接口,这个接口位于TCP/IP四层…...

AD9268、AD9643调试过程中遇到的问题
Ad9268芯片 AD9268是一款双通道、16位、80 MSPS/105 MSPS/125 MSPS模数转换器(ADC)。AD9268旨在支持要求高性能、低成本、小尺寸和多功能的通信应用。双通道ADC内核采用多级差分流水线架构,集成输出纠错逻辑。每个ADC都具有宽带宽、差分采样保持模拟输入放大器&…...
Java-File类基本方法使用指南
Java-File类基本方法使用指南 一、File类基础概念1.1 什么是File类1.2 File类的构造函数 二、文件和目录的创建与删除2.1 创建文件 - createNewFile()2.2 创建目录 - mkdir() 和 mkdirs()2.3 删除文件或目录 - delete() 三、文件和目录的查询与判断3.1 存在性判断 - exists()3.…...
Python爬虫实战:研究PyQuery库相关技术
1. 引言 1.1 研究背景与意义 随着互联网的快速发展,网络上的数据量呈爆炸式增长。如何高效地从海量的网页数据中提取有价值的信息,成为当前信息技术领域的一个重要研究方向。网络爬虫作为一种自动获取网页内容的程序,能够按照一定的规则,自动地抓取万维网信息,在搜索引擎…...
第九篇:MySQL 安全加固与访问控制策略实战
数据库的安全不仅仅是防止外部入侵,更包括合理配置账户权限、日志审计、网络加密、配置加固等。本文将系统性梳理 MySQL 的安全机制与实战加固方法,助你构建安全可靠的数据库运行环境。 一、数据库安全风险面 数据库常面临的威胁: 弱口令或默…...
神经网络-Day40
目录 单通道图片的规范写法图像任务中的张量形状NLP任务中的张量形状1. **Flatten操作**2. **view/reshape操作** 总结彩色图片的规范写法 图像数据的格式以及模型定义的过程,和之前结构化数据的略有不同,主要差异体现在2处 模型定义的时候需要展平图像由…...
WindowServer2022下docker方式安装dify步骤
WindowServer2022下docker方式安装dify步骤(稳定后考虑部署至linux中) 教程:https://blog.csdn.net/qq_49035156/article/details/143264534 0、资源要求 ---windows:8核CPU、16G内存、200G500G存储 ---10.21.31.122/administra…...
Java五种方法批量处理List元素全解
Java:如何优雅批量处理List中的每个元素 一、场景分析:为什么需要批量处理List?二、核心方法:五种实现方式对比2.1 普通for循环(最直接的方式)代码示例:优缺点: 2.2 Java 8 replaceAllÿ…...
springboot文件上传下载
基于ResponseEntity的下载响应 SpringBoot中,ResponseEntity类型可以精确控制HTTP响应,为文件下载提供完善的HTTP头信息。 RestController RequestMapping("/api/download") public class FileDownloadController {GetMapping("/file/{…...

webpack CDN打包优化
CDN网络分发服务 请求资源时最近的服务器将缓存内容交给用户 体积较大且变动不多的文件存在CDN文件中 react react-dom资源 // 添加自定义对于webpack的配置const path require(path) const { whenProd, getPlugin, pluginByName } require(craco/craco)module.exports {//…...

ARM内核一览
经常看介绍某某牛批芯片用的又是ARM什么核,看的云里雾里,所以简单整理整理。(内容来自官网和GPT) 1 ARM 内核总体分类 系列特点应用场景Cortex-M超低功耗、低成本、实时性嵌入式系统、微控制器、IoTCortex-R高可靠性、硬实时汽车…...

Rust 和 Python 如何混合使用
Rust 与 Python 可以通过多种方式混合使用,如 FFI 接口、PyO3 库、CFFI、CPython API、wasm 模块嵌入等。这种混合开发模式可结合 Rust 的性能优势与 Python 的开发效率。其中,PyO3 是目前最受欢迎的桥接工具,它允许使用 Rust 编写 Python 扩…...

台式电脑CPU天梯图_2025年台式电脑CPU天梯图
CPU的选择绝对是重中之重,它关乎了一台电脑性能好坏。相信不少用户,在挑选CPU的时候不知道谁强谁弱,尤其是intel和AMD两款CPU之间。下面通过2025年台式电脑CPU天梯图来了解下这两款cpu. 2025年台式电脑CPU天梯图 2025年台式电脑CPU天梯图包含了老旧型号以及12代、13代、14代…...