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

news2025/6/3 7:49:10

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

在实际开发中,数据加载通常需要先检查本地缓存,若缓存失效则从网络获取,并将结果更新到本地。以下是完整的 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()
    }

    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: ActivityUserListBinding
    private 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.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 库中的 TestCoroutineDispatcher 或 runTest 方法:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2395090.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多部手机连接同一wifi的ip一样吗?

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

大语言模型值ollama使用(1)

ollama为本地调用大语言模型提供了便捷的方式。下面列举如何在windows系统中快捷调用ollama。 winR打开运行框&#xff0c;输入cmd 1、输入ollama list 显示已下载模型 2、输入ollama pull llama3 下载llama3模型 3、 输入 ollama run llama3 运行模型 4、其他 ollama li…

thc-ssl-dos:SSL 压力测试的轻量级工具!全参数详细教程!Kali Linux教程!

简介 THC-SSL-DOS 是一款用于验证 SSL 性能的工具。 建立安全的 SSL 连接需要服务器比客户端高 15 倍的处理能力。 THC-SSL-DOS 利用这种不对称特性&#xff0c;通过使服务器过载并使其断网。 此问题影响当今所有 SSL 实现。供应商自 2003 年以来就已意识到这个问题&#x…

【速通RAG实战:进阶】17、AI视频打点全攻略:从技术实现到媒体工作流提效的实战指南

一、AI视频打点的技术底层与数据处理流程 (一)视频内容结构化的核心技术栈 AI视频打点的本质是将非结构化视频数据转化为带时间戳的结构化信息,其技术流程涵盖音视频处理、语音识别、自然语言处理三大核心模块,形成“数据采集-内容解析-智能标记-协同应用”的完整闭环。 …

立控信息智能装备柜:科技赋能军队装备管理现代化

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

【freertos-kernel】queue(发送)

文章目录 补充各种yeildTCB的xStateListItem和xEventListItem xQueueGenericSendprvCopyDataToQueueprvNotifyQueueSetContainervTaskInternalSetTimeOutStatevTaskSuspendAllxTaskResumeAllprvLockQueueprvUnlockQueueprvIncrementQueueTxLockvTaskPlaceOnEventListprvAddCurr…

破解高原运维难题:分布式光伏智能监控系统的应用研究

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

图标变白,开始菜单栏无法打开程序(以jupyter为例)

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

大语言模型(LLM)入门 - (1) 相关概念

文章来自&#xff1a;大语言模型(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、核心思想 目的&#xff1a;数据结构稳定的情况下&#xff0c;解决数据与算法的耦合问题。适用于对象结构稳定但需频繁扩展操作的场景。 实现&#xff1a;在访问数据时根据数据类…

C++数据结构 : 哈希表的实现

C数据结构 &#xff1a; 哈希表的实现 目录 C数据结构 &#xff1a; 哈希表的实现引言1. 哈希概念1.1 直接定址法1.2 哈希冲突1.3 负载因子 2. 哈希函数2.1 除法散列法/除留余数法2.2 乘法散列法&#xff08;了解&#xff09;2.3 全域散列法&#xff08;了解&#xff09; 3. 处…

AD9268、AD9643调试过程中遇到的问题

Ad9268芯片 AD9268是一款双通道、16位、80 MSPS/105 MSPS/125 MSPS模数转换器(ADC)。AD9268旨在支持要求高性能、低成本、小尺寸和多功能的通信应用。双通道ADC内核采用多级差分流水线架构&#xff0c;集成输出纠错逻辑。每个ADC都具有宽带宽、差分采样保持模拟输入放大器&…

webpack CDN打包优化

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

ARM内核一览

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

Rust 和 Python 如何混合使用

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

台式电脑CPU天梯图_2025年台式电脑CPU天梯图

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

2025年渗透测试面试题总结-匿名[校招]安全服务工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 匿名[校招]安全服务工程师 一面问题与完整回答 1. 学校、专业、成绩与排名 2. 学习安全时长 3. 当前学习…

Deseq2:MAG相对丰度差异检验

首先使用代码将contigs和MAG联系起来 https://github.com/MrOlm/drep/blob/master/helper_scripts/parse_stb.py ~/parse_stb.py --reverse -f ~/bin_dir/* -o ~/bin_dir/genomes.stb # 查看第一列的contigs有没有重复&#xff08;重复的话会影响后续比对&#xff09; awk {p…

CTFHub-RCE 命令注入-过滤目录分隔符

观察源代码 代码里面可以发现过滤了目录分隔符\和/ 判断是Windows还是Linux 源代码中有 ping -c 4 说明是Linux 查看有哪些文件 127.0.0.1|ls 打开flag文件 发现存在一个flag_is_here的文件夹&#xff0c;我们需要打开这个文件夹找到目标文件我们尝试分步&#xff0c;先利…

CentOS-stream-9 Zabbix的安装与配置

一、Web环境搭建部署Zabbix时&#xff0c;选择合适的MariaDB、PHP和Nginx版本非常重要&#xff0c;以确保兼容性和最佳性能。以下是建议版本&#xff1a;Zabbix 6.4 MariaDB&#xff1a;官方文档推荐使用MariaDB 10.3或更高版本。对于CentOS Stream 9&#xff0c;建议使用Maria…