Android 学习笔记 D09|接入 Retrofit:从一段 JSON 到一张文章卡片
摘要:接入玩 Android 首页接口,拆开 HTTP 调用、响应 DTO、业务校验和界面模型,分别定位断网、协议错误、业务失败与解析失败。
标签:Android、Retrofit、OkHttp、网络请求、数据模型
本文对应 28 天课程 D09,以及《第一行代码》第 3 版第 11 章网络技术。今天要完成的变化是把 FakeRepository 换成真实网络来源,同时尽量保持列表界面的调用方式稳定。文中是教学片段,依赖、导包和 UI 连接需要结合工程补齐,未宣称已编译或已完成线上验证。
1. 先查看契约,再定义接口
"接口能在浏览器打开"只能证明某次 HTTP 访问有响应。接入 App 前还要确认请求方法、Base URL、参数位置、页码起点和返回包裹结构。玩 Android 首页采用 GET,路径为 article/list/{page}/json,请求页码从零开始;需要 Manifest 的网络权限。下面只读取文章列表需要的字段,不把整份返回内容搬进界面。玩 Android 开放 API
Retrofit 根据注解构造接口调用并转换响应,OkHttp 承担底层 HTTP 工作。二者之间还存在转换器:服务器返回的 JSON 字节必须按选定的序列化规则变成对象。本文选择 Kotlin Serialization,项目就应配置对应插件、运行库和匹配的 Retrofit 转换器,不再混入另一套字段注解。Retrofit 接口声明
从数据流看,卡片不应知道外层有 errorCode,也不应负责决定缺失作者显示什么。DTO 反映接口,领域或界面模型反映本地使用需求,映射是边界。这个分离能让下一次字段变化局限在数据层,也方便用受控输入测试转换规则。
2. 写出最小的 DTO 和映射
以下示例假设已配置 Json { ignoreUnknownKeys = true },Base URL 为 https://www.wanandroid.com/。Serializable 来自 Kotlin Serialization;Retrofit HTTP 注解来自 retrofit2.http。省略构建器,避免复制未知版本组合。
kotlin
@Serializable
data class FeedEnvelope(
val errorCode: Int,
val errorMsg: String = "",
val data: PageDto? = null
)
@Serializable
data class PageDto(val datas: List<ArticleDto>, val over: Boolean)
@Serializable
data class ArticleDto(
val id: Long,
val title: String,
val author: String? = null,
val shareUser: String? = null,
val link: String
)
data class Article(val id: Long, val title: String, val author: String, val url: String)
data class ArticlePage(val items: List<Article>, val hasMore: Boolean)
class BusinessFailure(message: String) : Exception(message)
interface FeedApi {
@GET("article/list/{page}/json")
suspend fun page(@Path("page") page: Int): FeedEnvelope
}
fun ArticleDto.toArticle(decodeTitle: (String) -> String) = Article(
id = id,
title = decodeTitle(title),
author = author?.takeIf { it.isNotBlank() }
?: shareUser?.takeIf { it.isNotBlank() } ?: "匿名作者",
url = link
)
suspend fun loadPage(api: FeedApi, page: Int): ArticlePage {
val body = api.page(page)
if (body.errorCode != 0) throw BusinessFailure(body.errorMsg)
val data = body.data ?: throw BusinessFailure("响应缺少文章数据")
val items = data.datas.map { dto ->
dto.toArticle { title ->
HtmlCompat.fromHtml(title, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
}
}
return ArticlePage(items, hasMore = !data.over)
}
标题转换使用 AndroidX HtmlCompat,这里把接口标题当作带实体或标记的展示文本,转换为纯文本;它不意味着任何外部 HTML 都可以安全放进 WebView。作者依次使用作者字段、分享者字段和本地兜底,这是本文的显示规则。若产品只承认作者字段,应修改映射和对应检查,不能默默把两种身份混成同一概念。
id、标题、链接仍然是必需字段,没有用一个随意的默认值掩盖协议变化。缺少关键身份的文章无法可靠用于列表 key、收藏主键和详情路由;是否跳过错误记录必须另定规则并记录诊断。ignoreUnknownKeys 只忽略额外字段,不代表必需字段缺失或类型错误也会自动被修复。
3. 四类失败为什么要分开
断网可能让调用在取得响应前失败;HTTP 404 或 500 是收到响应但状态不满足成功约定;HTTP 200 且业务码非零,说明接口业务拒绝或处理失败;JSON 类型不匹配则可能在转换时抛出解析异常。这个顺序帮助定位责任层,不能只用"返回了东西"判断成功。
示例让 Retrofit 直接返回响应体,非成功 HTTP 状态会通过异常体现;如果接口改成 Response<FeedEnvelope>,调用方就要检查状态与 body,不能继续套用完全相同的逻辑。Retrofit Kotlin 支持
Repository 可以把上述可预期失败转换为项目统一的错误类型,再交给 ViewModel 决定界面显示。诊断信息与面向用户的信息也不同:用户需要"网络不可用,请重试",排错需要异常类别、请求页码和关联标识。不要在博客截图里贴令牌或完整个人响应,公开教学数据同样只保留必要字段。
4. 一次可重复的数据转换实验
准备三个本地 JSON 样本:正常文章、作者空白但有分享者、标题含 &。逐个送入同一转换链,预期第三个标题展示普通与号,第二个按约定展示分享者。随后故意删除文章 ID,预期转换失败或进入明确的无效记录分支,不能悄悄造出相同的零 ID。
再通过 Fake 或自己控制的测试服务分别提供业务错误、HTTP 错误和损坏 JSON。每次仅改变一个条件,记录错误在哪一层出现、界面显示什么、是否仍保留旧列表。不要为了制造异常向公开服务集中发送请求,也不要把 Fake 成功当作真实网络联调已经完成。
最后在工程里切换 Fake 与 Remote。若切换时必须修改每个卡片,说明界面依赖泄漏了;理想变化是 Repository 的装配位置以及必要的加载状态,而不是整套 UI 改写。这种可替换性也是接下来分页和离线缓存的基础。
5. 原创面试问答与追问
问一:为什么不直接把 DTO 传给 Compose? 可以在极小样例里这样做,但接口结构会渗透到 UI。追问:分模型是否一定更好?应按变化边界取舍,本项目有作者兜底、标题解码和本地收藏,映射有具体价值。
问二:HTTP 200 等于列表加载成功吗? 不等于,还要检查业务码、数据结构与转换结果。追问:业务失败该显示空列表吗?不该,成功但无数据和请求未成功是不同状态。
问三:Retrofit 与 OkHttp 可以互换吗? 它们主要职责不同,前者组织声明与转换,后者处理 HTTP 调用。追问:请求头在哪里添加?可按调用范围用声明参数或统一拦截器,不能把所有变化都塞进 UI。
这些问题为本课程原创自测,由面试鸭题库中的线程、异步基础延伸到网络实践,不表示该题库包含独立的 Retrofit 或 HTTP 专题,未复刻题库答案。
6. D09 验收与阅读衔接
验收时拿一条文章,说明它的 ID、标题、作者和 URL 从哪里来;再任选一种失败,解释它发生在 HTTP、业务还是解析层。至少保存一份经过脱敏的输入输出对照,证明规则可以重复执行。对应书本第 11 章网络请求与解析;下一课只增加分页状态,不重新发明另一套 DTO 与网络入口。