使用Retrofit与Kotlin序列化从互联网获取数据
1. 开始之前
大多数Android应用都会连接互联网,从后端服务器获取邮件、消息或其他信息。Gmail、YouTube和Google Photos都是通过联网展示用户数据的例子。
本实验使用开源、社区维护的库构建数据层,从后端服务器获取数据。这样可简化数据获取,也有助于遵循在后台线程执行操作等Android最佳实践。网络缓慢或不可用时,还会显示错误消息,让用户了解连接问题。
前提知识
- 创建Composable函数的基础知识。
- 使用Android架构组件ViewModel的基础知识。
- 使用协程处理长时间任务的基础知识。
- 在
build.gradle.kts中添加依赖的基础知识。
学习内容
- 什么是REST Web服务。
- 如何用Retrofit连接互联网中的REST服务并获取响应。
- 如何用kotlinx.serialization将JSON响应解析为数据对象。
要完成的任务
- 修改起始应用,使其发起Web服务API请求并处理响应。
- 用Retrofit实现数据层。
- 用kotlinx.serialization把响应解析成数据对象列表,并放入UI状态。
- 利用Retrofit的协程支持简化代码。
所需条件
安装Android Studio的计算机,以及Mars Photos应用的起始代码。
2. 应用概览
Mars Photos应用连接Web服务,获取并显示火星地表照片。这些真实照片来自NASA的火星探测车。最终应用以网格显示照片:


3. 探索Mars Photos起始应用
下载起始代码
下载起始代码ZIP,或克隆仓库:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout starter
起始代码位于starter分支,也可在GitHub仓库浏览。
运行起始代码
- 在Android Studio打开下载的
basic-android-kotlin-compose-training-mars-photos项目。 - 在Android窗格展开app > kotlin + java,可见
ui包,它是应用的UI层。

编译并运行应用,屏幕中央会显示占位文本;本实验结束时,将其替换成获取到的照片数量。

起始代码结构
ui/MarsPhotosApp.kt包含MarsPhotosApp可组合函数,显示顶部应用栏与HomeScreen等内容。前面的占位文本位于其中;后续实验将展示后端数据。screens/MarsViewModel.kt是对应的ViewModel,包含名为marsUiState的MutableState属性。更新该属性会更新屏幕上的占位文本。getMarsPhotos()先更新占位响应,稍后会用于显示服务器返回的数据。本实验的目标是用网络数据更新ViewModel中的MutableState。screens/HomeScreen.kt包含HomeScreen与ResultScreen。后者用简单Box布局中的Text显示marsUiState。MainActivity.kt只负责加载ViewModel并显示MarsPhotosApp。
4. Web服务简介
本实验创建网络服务层,与后端通信并获取所需数据。第三方库Retrofit负责这项工作;ViewModel与数据层交互,应用其余部分无需了解具体实现。

MarsViewModel负责发起网络调用,获取火星照片数据;数据变化时,通过MutableState更新UI。后续实验会在数据层加入Repository,由它与Retrofit服务通信,并向应用其余部分公开数据。
5. Web服务与Retrofit
火星照片数据存储在Web服务器上。应用需要与互联网中的服务器建立连接并通信,才能取得这些数据。


许多Web服务采用通用、无状态的REST(Representational State Transfer,表述性状态转移)架构,称为RESTful服务。请求通过标准化的URI(统一资源标识符)发出。URI按名称标识资源,本身不一定指明资源位置或访问方式。
本课服务器为android-kotlin-fun-mars-server.appspot.com,同时服务火星房产和火星照片两个示例。URL(统一资源定位符)是URI的一个子集,指出资源所在位置及获取机制。例如:
- /realestate获取火星房产列表。
- /photos获取火星照片列表。
这些URL通过HTTP协议标识可获取的网络资源。本实验使用/photos端点,即访问服务器上Web服务的URL。常见Web URL也是URI的一种,本课程会交替使用这两个称呼。
Web服务请求
请求包含URI,并使用与Chrome等浏览器相同的HTTP协议传输到服务器。HTTP操作告诉服务器要做什么:
- GET:获取服务器数据。
- POST:创建数据。
- PUT:更新现有数据。
- DELETE:删除数据。
应用用GET获取照片信息,服务器返回包含图片URL的响应。


响应通常采用XML或JSON等格式。JSON以键值对表示结构化数据,应用借此与REST API通信。接下来将使用已经准备好的后端,通过Retrofit建立连接、通信并接收JSON响应。
外部库
第三方库扩展了Android核心API。本课程使用的库开源、由社区开发,并由全球Android社区共同维护,帮助开发者构建更好的应用。
Retrofit库
本实验使用Retrofit与RESTful火星服务通信。原文将其作为维护良好的库的例子:可查看GitHub页面中开放和已关闭的问题、功能请求,以及开发者是否持续解决问题并回应请求。更多用法见Retrofit文档。
Retrofit生成与REST后端交互的代码;开发者仍需根据参数提供服务URI,后文会说明。

添加Retrofit依赖
Android Gradle支持添加外部库;除依赖声明外,还应配置库所在仓库。打开模块级build.gradle.kts (Module :app),在dependencies中添加:
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
第一项是Retrofit2本身,第二项是标量转换器。Retrofit2是更新后的Retrofit;标量转换器可让它将JSON结果作为String返回。JSON用于客户端与服务器之间保存和传输数据,后文会进一步介绍。点击Sync Now,以新依赖重新构建项目。
6. 连接互联网
先让Retrofit获取火星Web服务的原始JSON响应,并作为String显示;占位Text会显示响应或连接错误消息。Retrofit根据服务内容创建网络API,通过转换器将响应解码成String等对象;它支持XML、JSON等常用格式,并处理在后台执行请求等细节。

为项目添加ViewModel使用的数据层:创建MarsApiService数据源、指定基础URL与转换工厂的Retrofit对象、描述HTTP通信的接口,并公开服务实例。
创建服务API
- 右键项目窗格中的
com.example.marsphotos,选择New > Package。 - 在建议包名末尾添加
network。 - 在新包中新建Kotlin文件
MarsApiService并打开。 - 添加服务基础URL常量:
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
在常量下方添加Retrofit构建器:
import retrofit2.Retrofit
private val retrofit = Retrofit.Builder()
构建Web服务API需要基础URI和转换工厂。转换器决定如何处理返回数据;此处用ScalarsConverter将JSON响应作为String返回,它也支持其他基本类型。
调用addConverterFactory():
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
再通过baseUrl()设置基础URL,调用build()创建对象:
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
在构建器之后定义MarsApiService接口,描述Retrofit如何用HTTP请求与服务器通信:
interface MarsApiService {
}
添加获取响应的getPhotos()函数:
interface MarsApiService {
fun getPhotos()
}
用@GET标明GET请求,并指定photos端点:
import retrofit2.http.GET
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
调用方法时,Retrofit会把photos追加到构建器中的基础URL以发起请求。将返回类型设置为String:
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
对象声明
Kotlin的object声明可定义单例:只创建一个实例,并提供全局访问点。对象在首次访问时进行线程安全的初始化。object关键字后必须有名称。示例如下,无需复制:
// Example for Object declaration, do not copy over
object SampleDataProvider {
fun register(provider: SampleProvider) {
// ...
}
// ...
}
// To refer to the object, use its name directly.
SampleDataProvider.register(...)
Retrofit对象的create()调用在内存、速度和性能方面成本较高。此应用只需要一个API服务实例,因此暂用object声明向其他部分公开服务。
在接口外定义公共对象MarsApi:
object MarsApi {}
添加类型为MarsApiService、名为retrofitService的惰性初始化属性;暂时忽略错误,下一步解决:
object MarsApi {
val retrofitService : MarsApiService by lazy {}
}
惰性初始化把对象创建延迟到实际使用时,避免不必要的计算与资源占用。Kotlin对此提供一等支持。
调用retrofit.create()并传入接口,初始化该属性:
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
设置完成。每次调用MarsApi.retrofitService都会访问同一个实现接口的单例服务对象,它在首次访问时创建。
在MarsViewModel中调用Web服务
接下来实现getMarsPhotos(),调用REST服务并处理JSON字符串。推荐从Repository调用服务,将数据层与其他部分隔离;后续实验会加入Repository。
viewModelScope
viewModelScope是每个ViewModel内置的协程作用域。ViewModel被清理时,作用域内启动的协程自动取消。可以在此启动协程并发起网络请求;作用域属于ViewModel,因此配置变更时请求仍可继续。
在MarsApiService.kt中将getPhotos()改为挂起函数,以异步调用而不阻塞调用线程:
@GET("photos")
suspend fun getPhotos(): String
打开ui/screens/MarsViewModel.kt,删除getMarsPhotos()中设置"Set the Mars API Response here!"的语句:
private fun getMarsPhotos() {}
通过viewModelScope.launch启动协程:
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
private fun getMarsPhotos() {
viewModelScope.launch {}
}
在其中调用单例服务的getPhotos(),将结果保存为listResult:
import com.example.marsphotos.network.MarsApi
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
把结果赋给表示最近一次请求状态的可变状态对象marsUiState:
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
此时运行应用,它可能立即关闭,并可能弹出错误提示,这就是崩溃。查看Android Studio的Logcat,日志以类似下列内容开头:
--------- beginning of crash
22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
Process: com.example.android.marsphotos, PID: 22803
java.lang.SecurityException: Permission denied (missing INTERNET permission?)
...
错误提示缺少INTERNET权限。下一步添加权限解决此问题。
7. 添加互联网权限与异常处理
Android权限
权限用于保护用户隐私。应用访问联系人、通话记录等敏感数据,或相机、互联网等系统功能时,需要声明或请求权限。
互联网连接涉及安全问题,应用默认不能联网;必须显式声明INTERNET,它属于普通权限。更多类型见Android权限文档。
在manifests/AndroidManifest.xml的<application>之前添加:
<uses-permission android:name="android.permission.INTERNET" />
重新编译运行。网络正常时会看到照片数据JSON,每条记录都有id和img_src,后文介绍其格式。

点击设备或模拟器的返回按钮关闭应用。
异常处理
代码仍有一个问题。将设备或模拟器设为飞行模式,从最近任务重新打开应用,或从Android Studio运行,再检查Logcat中的致命异常:
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.android.marsphotos, PID: 3302
该错误表明应用尝试连接并超时。这类异常很常见;与权限问题不同,无法直接修复外部连接,但可以处理异常。
异常是什么
异常在运行时而非编译时发生。未处理的异常可能让应用突然终止,造成糟糕体验。异常处理可阻止这种终止,并友好地应对问题。原因可能简单到除以零,也可能是网络错误,与之前实验中的IllegalArgumentException类似。
连接服务器的潜在问题包括:API中的URL或URI错误、服务器不可用、网络延迟、设备连接较差或没有网络。可用try-catch在运行时处理,更多信息见Kotlin异常文档。
基本语法:
try {
// some code that can cause an exception.
}
catch (e: SomeException) {
// handle the exception to avoid abrupt termination.
}
将可能出错的网络调用放在try块中,在catch中防止应用突然终止,并执行恢复逻辑。在getMarsPhotos()的launch块中,用try包围MarsApi调用,再添加catch:
import java.io.IOException
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
再次运行应用,此时不会因该异常崩溃。
添加UI状态
目前marsUiState只保存最近请求的状态值,不能区分加载、成功和失败。加载表示等待数据,成功表示已获取数据,错误表示网络或连接失败。使用密封接口限制可能的值,更容易管理状态。以下仅为概念示例,无需复制:
// No need to copy over
sealed interface MarsUiState {
data class Success : MarsUiState
data class Loading : MarsUiState
data class Error : MarsUiState
}
成功响应需要保存照片信息,因此给Success数据类添加构造参数。Loading和Error无需新数据或每次创建新对象,将其改为object。
在MarsViewModel文件的import之后添加密封接口:
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
object Error : MarsUiState
object Loading : MarsUiState
}
将ViewModel中的状态类型改为MarsUiState,默认值设为Loading,并将setter设为私有以保护写入:
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
获取成功后,传入listResult:
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
在catch中设置错误状态:
catch (e: IOException) {
marsUiState = MarsUiState.Error
}
可以把赋值移到try-catch外,完整函数为:
private fun getMarsPhotos() {
viewModelScope.launch {
marsUiState = try {
val listResult = MarsApi.retrofitService.getPhotos()
MarsUiState.Success(listResult)
} catch (e: IOException) {
MarsUiState.Error
}
}
}
在screens/HomeScreen.kt中对状态使用when;成功时调用ResultScreen并传入photos,暂时忽略缺少其他分支的错误:
import androidx.compose.foundation.layout.fillMaxWidth
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Success -> ResultScreen(
marsUiState.photos, modifier = modifier.fillMaxWidth()
)
}
}
状态已不再是String,而是具有Loading、Success、Error三种取值的密封接口。添加另外两个分支,分别显示后续实现的LoadingScreen和ErrorScreen:
import androidx.compose.foundation.layout.fillMaxSize
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> ResultScreen(
marsUiState.photos, modifier = modifier.fillMaxWidth()
)
is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
}
}
打开res/drawable/loading_animation.xml。该资源描述让loading_img.xml绕中心旋转的动画,预览中看不到动画。

在HomeScreen下方加入LoadingScreen;起始代码已包含loading_img:
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
Image(
modifier = modifier.size(200.dp),
painter = painterResource(R.drawable.loading_img),
contentDescription = stringResource(R.string.loading)
)
}
再加入显示错误信息的ErrorScreen:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
)
Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
}
}
保持飞行模式运行,应用不会再突然关闭,而会显示错误消息:

关闭飞行模式,再运行并测试,确认能看到JSON字符串:


8. 用kotlinx.serialization解析JSON响应
JSON
服务通常以XML或JSON返回结构化数据,应用必须知道其结构才能读取。本例访问照片端点,浏览器中会显示JSON格式的照片ID与图片URL列表。
示例JSON结构

- 响应是由方括号包围的数组,包含JSON对象。
- 对象由花括号包围。
- 各对象包含一组以逗号分隔的键值对。
- 键和值之间用冒号分隔。
- 名称用引号包围。
- 值可以是数字、字符串、布尔值、数组、对象或null。
例如img_src是URL字符串,粘贴到浏览器可看到火星地表照片:

现在应用已能收到JSON,但显示图片需要Kotlin对象,而非一大段JSON字符串。将外部数据读取为运行时对象叫反序列化;序列化则把应用数据转成可经网络传输的格式。两者都是网络数据交换的重要环节。
kotlinx.serialization提供把JSON字符串转换为Kotlin对象的库,并有社区开发的Retrofit Kotlin Serialization Converter与其配合。接下来把响应解析为表示火星照片的对象,显示照片数量。
添加依赖
打开模块级build.gradle.kts,在plugins中加入:
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
在dependencies中加入Kotlin JSON序列化依赖:
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
找到原标量转换器依赖:
// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
替换为:
// Retrofit with Kotlin serialization Converter
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
点击Sync Now。删除标量依赖后可能出现编译错误,下一步处理。
实现MarsPhoto数据类
服务响应中的一个条目类似:
[
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]
id用引号包围,因此是String而非整数;img_src也是String,保存图片URL。kotlinx.serialization需要一个Kotlin数据类保存解析结果。
右键network包,选择New > Kotlin File/Class,选择Class并命名MarsPhoto。在类定义前添加data,将花括号改为圆括号:
data class MarsPhoto()
数据类必须至少有一个属性,因此先会报错。添加以下属性:
data class MarsPhoto(
val id: String, val img_src: String
)
再加上Serializable注解:
import kotlinx.serialization.Serializable
@Serializable
data class MarsPhoto(
val id: String, val img_src: String
)
每个变量对应JSON中的一个键,按本例响应均使用String。解析时,库按键名匹配并填充对象。
@SerialName注解
JSON键名不一定符合Kotlin编码风格。例如img_src使用下划线,而Kotlin属性通常采用驼峰式。通过SerialName可使用不同的属性名,同时映射到原JSON键。替换对应属性:
import kotlinx.serialization.SerialName
@SerialName(value = "img_src")
val imgSrc: String
更新MarsApiService和MarsViewModel
现在用kotlinx.serialization转换器把JSON转换为Kotlin对象。打开network/MarsApiService.kt,因前面修改依赖,ScalarsConverterFactory引用无法解析。删除:
import retrofit2.converter.scalars.ScalarsConverterFactory
修改Retrofit构建器,使用序列化转换器:
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType
private val retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(BASE_URL)
.build()

现在可让Retrofit把JSON数组转换为MarsPhoto列表,而非JSON字符串。修改接口:
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
打开MarsViewModel中的getMarsPhotos。此时listResult是List<MarsPhoto>,其size就是收到并解析的照片数量。修改状态:
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
"Success: ${listResult.size} Mars photos retrieved"
)
关闭设备或模拟器的飞行模式,编译并运行。此时显示照片数量,不再显示整段JSON:

若无法联网,先确认飞行模式已关闭。
9. 完成代码
可使用以下命令获取完成代码:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter
也可以下载ZIP,解压后在Android Studio打开。完成代码位于repo-starter分支,可在GitHub浏览。
10. 小结
REST Web服务
- Web服务通过互联网提供功能,让应用发出请求并获取数据。
- RESTful服务采用REST架构,建立在标准Web组件与协议之上。
- 请求通过URI标准化地发出。
- 应用需要建立网络连接、与服务通信,再接收响应并解析成可用格式。
- Retrofit是让应用请求REST服务的客户端库。
- 转换器决定如何处理发送和接收的数据,例如ScalarsConverter将数据视为String或其他基本类型。
- 联网需在清单声明
android.permission.INTERNET。 - 惰性初始化将对象创建推迟到首次实际使用,之后复用该对象。
JSON解析
- 服务响应常用JSON表示结构化数据。
- JSON对象是键值对集合;对象集合可组成JSON数组,本实验响应即为数组。
- 键用引号包围,值可为数值或字符串等。
- Kotlin的序列化工具位于独立的kotlinx.serialization组件,可把JSON转为Kotlin对象。
- 社区提供与Retrofit配合的Kotlin序列化转换器。
- 解析时,JSON键与同名对象属性匹配。
- 需要使用不同属性名时,用
@SerialName指定对应JSON键。











暂无评论内容