Android 基础补强 B19|从 SunnyWeather 读懂 MVVM,再迁移到自己的阅读客户端
《第一行代码》第 15 章把前面学过的网络、存储、界面和 Jetpack 组织成天气应用。读这一章最有价值的问题是:为什么数据要经过这些位置,刷新时谁负责什么,以及失败后界面如何表达。
本篇结合工作区 SunnyWeather 示例的 Repository 与依赖配置,讨论如何把思想迁移到 DevCommunity。它是代码阅读与改造思路,不是已完成的天气接口联调记录,也不要求把整个旧工程复制成新项目。
先追一条请求,不先背目录
从城市搜索出发:用户输入关键词,界面或 ViewModel 发起搜索,Repository 调用网络层,返回城市数据或错误,再由界面展示。天气刷新则需要组织实时天气与未来天气,形成页面所需的组合结果。
text
用户意图 → 屏幕状态持有者 → Repository → 网络或本地数据
↓
界面展示 ← 可观察的状态 ← 成功结果、缓存或错误规则
这个图不是规定每次都必须经过相同数量的类。每一层应该承担可说明的职责:网络层描述协议,Repository 决定数据来源与组合,状态持有者管理屏幕呈现,界面负责展示与事件。当前官方架构建议同样强调清楚的 UI 与数据层边界。架构建议
从现有代码观察两个设计选择
工作区示例的 Repository 使用 LiveData 构建结果,并通过统一的辅助函数包装异常;刷新天气时在协程作用域内并发获取两类天气,再组合返回。这里可以学习"把来源组合限制在一个位置"和"给上层一致的结果入口"。
阅读时也要保留时代背景。当前文件仍有较旧的目标 SDK 和部分依赖配置,不能直接当作新工程的推荐版本;LiveData 也不等于不能继续使用,是否调整取决于界面和协程组织方式。
更值得检查的是异常语义:宽泛捕获 Exception 时,取消异常是否被当成普通失败处理。迁移代码时应保留协程取消传播,让已经结束的业务任务不会继续像普通结果一样提交状态。Android 协程最佳实践
并发请求先规定失败关系
下面是独立教学模型,演示两类结果缺一不可时的组合。需要 kotlinx.coroutines 的 async 与 coroutineScope;Service 的实现必须保证调用方式适合所在上下文。代码没有连接真实天气服务,也未随稿编译。
kotlin
data class CurrentWeather(val temperature: Double)
data class Forecast(val description: String)
data class WeatherPage(val current: CurrentWeather, val forecast: Forecast)
interface WeatherService {
suspend fun current(placeId: String): CurrentWeather
suspend fun forecast(placeId: String): Forecast
}
class WeatherRepository(private val service: WeatherService) {
suspend fun refresh(placeId: String): WeatherPage = coroutineScope {
val current = async { service.current(placeId) }
val forecast = async { service.forecast(placeId) }
WeatherPage(current.await(), forecast.await())
}
}
这个策略强调两个任务属于同一次操作。如果业务允许部分成功,应该另外设计部分内容与错误提示,而不是只把 coroutineScope 换个名字就认为完成容错。哪些结果可以独立展示,是业务问题;作用域与异常机制只是实现选择。
对阅读客户端,首页文章和本地收藏可能来自不同来源,但未必需要采用天气页的"缺一不可"规则。网络失败时仍可显示缓存,收藏读取失败也需要单独判断影响范围。迁移的是分析方法,不是照搬成功条件。
把天气项目映射到阅读项目
| 天气应用场景 | 阅读客户端对应问题 | 需要重新设计的部分 |
|---|---|---|
| 搜索城市 | 搜索文章 | 防抖、分页、旧结果身份 |
| 记住选中城市 | 保存本地收藏或阅读状态 | 多条结构化记录与查询 |
| 刷新天气 | 刷新首页文章 | 保留旧内容、缓存顺序 |
| 显示组合结果 | 文章与收藏状态合并 | 多页面事实来源一致 |
| 发布安装包 | 交付学习项目 | 实际版本、测试和说明 |
如果每个页面都直接知道真实接口和数据库,替换数据源会牵动很多地方。先定义 UI 真正需要的 Repository 能力,再使用 Fake 验证状态,能更早发现职责问题。接口不必一开始抽象到支持所有产品,满足当前稳定需求即可。
从旧示例迁移的可控顺序
第一步记录旧工程当前能否构建、能否访问服务;第二步只追踪一个查询;第三步在新工程用 Fake 表达相同能力;第四步接入真实数据;第五步再考虑状态工具和持久化变化。每次只改清楚的一层,保留前后的行为对照。
如果接口不可用,不应把整个项目判断成毫无价值。仍能阅读分层、状态和调用关系,但要准确区分"理解了代码"与"完成了真实联调"。同理,Gradle 同步成功不证明 API 密钥、响应字段和业务状态仍然有效。
故障实验与预期
让实时天气请求成功、预测请求失败,观察当前全有或全无策略怎样表现。再明确一个部分成功方案,写出界面应该保留什么、重试哪个来源,然后实现与验证。阅读项目可用首页刷新失败但收藏仍可查看作为类似思考。
第二个实验让旧搜索晚于新搜索返回。检查 Repository 只负责获取数据,还是还偷偷修改了全局 UI;若后者存在,会更难约束请求身份。预期把结果提交规则放在可控制的业务边界,并给出取消与失效条件。
三道原创面试自测
MVVM 是否就是三个目录? 不是,关键是职责、依赖和数据流。追问"怎么证明",从一次操作追踪到数据和界面,说明每一步能否独立替换和验证。
两个 async 是否总能加快请求? 只对确实可并发且受资源条件支持的工作有意义,还要考虑失败关联。追问"一个失败呢",按业务决定全部失败或部分展示,不能忽略取消语义。
旧项目最值得保留什么? 清楚的问题拆分和可解释的调用关系。追问"哪些要重新核验",依赖、平台行为、接口协议与异常处理都需要按当前环境验证。
完成本篇后,应能从天气项目提炼一种设计方法,再解释为什么阅读客户端作出不同选择,而不是只换应用名称和图标。