1. 准备工作
市场中的大多数 Android 应用都需要连接到互联网才能执行网络操作,例如从后端服务器检索电子邮件、消息或其他信息。Gmail、YouTube 和 Google 相册等应用就需要连接到互联网才能显示用户数据。
在此 Codelab 中,您将使用依托于社区的开源库来构建数据层,并从后端服务器获取数据。这样可以简化数据提取,还有利于应用遵循 Android 最佳实践,例如在后台线程上执行操作。此外,如果互联网速度较慢或不可用,应用界面将显示一条错误消息,让用户及时获知网络连接问题。
前提条件
- 了解有关如何创建可组合函数的基础知识。
- 了解有关如何使用 Android 架构组件
ViewModel
的基础知识。 - 了解有关如何使用协程处理长时间运行的任务的基础知识。
- 了解有关如何在
build.gradle.kts
中添加依赖项的基础知识。
学习内容
- 什么是 REST Web 服务。
- 如何使用 Retrofit 库连接到互联网上的 REST Web 服务,并获取响应。
- 如何使用序列化 (kotlinx.serialization) 库将 JSON 响应解析为数据对象。
实践内容
- 修改起始应用以发出 Web 服务 API 请求,并处理响应。
- 使用 Retrofit 库为您的应用实现数据层。
- 使用 kotlinx.serialization 库将 Web 服务的 JSON 响应解析为应用的数据对象列表,并将其附加到界面状态。
- 使用 Retrofit 对协程的支持来简化代码。
所需条件
- 一台安装了 Android Studio 的计算机
- Mars Photos 应用的起始代码
2. 应用概览
您将使用名为 Mars Photos 的应用,其中显示了火星表面的图片。该应用需要连接到 Web 服务,才能检索并显示火星照片。这些图片是由 NASA 的火星探测器拍摄的真实照片。下图是最终应用的屏幕截图,其中以网格形式列出了多张图片。
您在此 Codelab 中构建的应用版本不会有很多视觉上的亮点;此 Codelab 侧重于应用的数据层部分,旨在连接到互联网并使用 Web 服务下载原始资源数据。为了确保该应用正确检索和解析这些数据,您可以在 Text
可组合项中输出从后端服务器接收的照片数。
3. 探索 Mars Photos 起始应用
下载起始代码
首先,请下载起始代码:
或者,您也可以克隆该代码的 GitHub 代码库:
$ 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
您可以在 Mars Photos
GitHub 代码库中浏览该代码。
运行起始代码
- 在 Android Studio 中打开下载的项目。该项目的文件夹名称为
basic-android-kotlin-compose-training-mars-photos
。 - 在 Android 窗格中,展开 app > kotlin + java。请注意,该应用有一个名为
ui
的软件包文件夹。这是应用的界面层。
- 运行应用。编译和运行应用时,您会看到以下界面,其中占位符文本居中显示。在此 Codelab 结束时,您将使用检索的照片数更新此占位文本。
起始代码演示
在此任务中,您将熟悉项目的结构。下面列出了项目中的重要文件和文件夹。
ui\MarsPhotosApp.kt
:
- 此文件包含可组合项
MarsPhotosApp
,它会在屏幕上显示内容,例如顶部应用栏和HomeScreen
可组合项。上一步中的占位符文本会显示在此可组合项中。 - 在下一个 Codelab 中,此可组合项将显示从火星照片后端服务器接收的数据。
screens\MarsViewModel.kt
:
- 此文件是
MarsPhotosApp
的对应视图模型。 - 此类包含一个名为
marsUiState
的MutableState
属性。更新此属性的值会更新屏幕上显示的占位符文本。 getMarsPhotos()
方法会更新占位符响应。您稍后将在此 Codelab 中使用此方法显示从服务器提取的数据。此 Codelab 的目标是使用您从互联网获得的真实数据更新ViewModel
中的MutableState
。
screens\HomeScreen.kt
:
- 此文件包含
HomeScreen
和ResultScreen
可组合项。ResultScreen
具有一个简单的Box
布局,它会在Text
可组合项中显示marsUiState
的值。
MainActivity.kt
:
- 此 activity 的唯一任务就是加载
ViewModel
并显示MarsPhotosApp
可组合项。
4. Web 服务简介
在此 Codelab 中,您将为网络服务创建一个层,与后端服务器进行通信并提取所需数据。您将使用名为 Retrofit 的第三方库来完成此任务。我们稍后会对此进行详细介绍。ViewModel
与该数据层进行通信,其余应用对此实现是透明的。
MarsViewModel
负责发出网络调用以获取火星照片数据。在 ViewModel
中,您使用 MutableState
,以在数据发生更改时更新应用界面。
5. Web 服务和 Retrofit
火星照片数据存储在网络服务器中。为了将这些数据传输到您的应用中,需要与互联网上的服务器建立连接并进行通信。
如今,大多数 Web 服务器都使用一种名为表征状态转移 (REST, REpresentational State Transfer) 的常用无状态网络架构来运行 Web 服务。提供此架构的 Web 服务称为 RESTful 服务。
通过统一资源标识符 (URI) 以标准化方式向 RESTful Web 服务发出请求。URI 可按名称识别服务器中的资源,而不会暗示其位置或访问方式。例如,在本节课的应用中,您将使用以下服务器 URI 检索图片网址(此服务器同时托管火星地产和火星照片):
android-kotlin-fun-mars-server.appspot.com
网址(统一资源定位符)是 URI 的子集,用于指定资源的位置以及检索资源的机制。
例如:
以下网址将获取火星上可用的地产资源列表:
https://android-kotlin-fun-mars-server.appspot.com/realestate
以下网址将获取火星照片列表:
https://android-kotlin-fun-mars-server.appspot.com/photos
这些网址指的是可通过超文本传输协议 (http:) 从网络获取的标识资源,如 /realestate 或 /photos。您将在此 Codelab 中使用 /photos 端点。端点是一个网址,您可以通过它访问在服务器上运行的 Web 服务。
Web 服务请求
每个 Web 服务请求均包含 URI,并使用网络浏览器(例如 Chrome)所使用的 HTTP 协议传输至服务器。HTTP 请求包含一项用于告知服务器该执行什么操作的操作。
常见的 HTTP 操作包括:
- GET,用于检索服务器数据。
- POST,用于在服务器上创建新数据。
- PUT,用于更新服务器上现有的数据。
- DELETE,用于从服务器中删除数据。
您的应用将向服务器发出获取火星照片信息的 HTTP GET 请求,然后服务器会返回对应用的响应(包括图片网址)。
Web 服务的响应会采用一种常用的数据格式,例如 XML(可扩展标记语言)或 JSON(JavaScript 对象表示法)。JSON 格式以键值对形式表示结构化数据。应用使用 JSON 与 REST API 进行通信,我们将在后续任务中对此进行详细介绍。
在此任务中,您将与服务器建立网络连接、与服务器通信并接收 JSON 响应。您将使用已经为您编写的后端服务器。在此 Codelab 中,您将使用 Retrofit 库,这是一个用于与后端服务器进行通信的第三方库。
外部库
外部库或第三方库就像对核心 Android API 的扩展。您在本课程中使用的库是社区开发的开源库,并通过世界各地的大型 Android 社区共同贡献进行维护。这些资源可帮助像您这样的 Android 开发者构建更出色的应用。
Retrofit 库
您将在此 Codelab 中用来与 RESTful Mars Web 服务通信的 Retrofit 库,就是得到良好支持和维护的库的一个很好的例子。您可以查看其 GitHub 页面,查看尚未解决及已解决的问题(其中一些是功能请求)。如果开发者定期解决问题并响应功能请求,则意味着该库可能得到了良好维护,并且非常适合在应用中使用。您还可以参阅 Retrofit 文档,详细了解该库。
Retrofit 库与 REST 后端进行通信。它会生成代码,但您需要根据我们传递给它的参数为 Web 服务提供 URI。我们将在后面的部分中详细了解此主题。
添加 Retrofit 依赖项
Android Gradle 允许您将外部库添加到项目中。除了库依赖项之外,您还需要添加托管库的代码库。
- 打开模块级 Gradle 文件
build.gradle.kts (Module :app)
。 - 在
dependencies
部分,为 Retrofit 库添加以下几行代码:
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
这两个库协同工作。第一个依赖项用于 Retrofit 2 库本身,而第二个依赖项则用于 Retrofit 标量转换器。Retrofit2 是 Retrofit 库的更新版本。此标量转换器允许 Retrofit 将 JSON 结果作为 String
返回。JSON 是一种在客户端和服务器之间存储和传输数据的格式。我们将在后面的部分中介绍 JSON。
- 点击 Sync Now,以使用新的依赖项重建项目。
6. 连接互联网
您将使用 Retrofit 库与 Mars Web 服务通信,并将原始 JSON 响应显示为 String
。占位符 Text
将显示返回的 JSON 响应字符串或指明连接错误的消息。
Retrofit 会根据 Web 服务的内容为应用创建网络 API。它从 Web 服务提取数据,并通过独立的转换器库来路由数据。该库知道如何解码数据,并以 String
等对象形式返回这些数据。Retrofit 内置对 XML 和 JSON 等常用数据格式的支持。Retrofit 最终会创建一个代码来为您调用和使用此服务,包括关键详细信息(例如在后台线程上运行请求)。
在此任务中,您将向 Mars Photos 项目添加一个数据层,供 ViewModel
用来与 Web 服务通信。您将按照以下步骤实现 Retrofit 服务 API。
- 创建一个数据源:
MarsApiService
类。 - 使用基准网址和转换器工厂创建 Retrofit 对象,以转换字符串。
- 创建一个可说明 Retrofit 如何与网络服务器通信的接口。
- 创建一个 Retrofit 服务,并向应用的其余 API 服务公开实例。
实现上述步骤:
- 右键点击 Android 项目窗格中的 com.example.marsphotos 软件包,然后依次选择 New > Package。
- 在弹出式窗口中,将 network 附加到建议软件包名称的末尾。
- 在新软件包“network”下创建新的 Kotlin 文件。将该文件命名为
MarsApiService
。 - 打开
network/MarsApiService.kt
。 - 为 Web 服务的基础网址添加以下常量。
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- 在该常量正下方添加 Retrofit 构建器,用于构建和创建 Retrofit 对象。
import retrofit2.Retrofit
private val retrofit = Retrofit.Builder()
Retrofit 需要 Web 服务的基础 URI 和转换器工厂来构建 Web 服务 API。转换器会告知 Retrofit 如何处理它从 Web 服务获取的数据。在这种情况下,您需要 Retrofit 从 Web 服务提取 JSON 响应,并将该响应作为 String
返回。Retrofit 包含一个 ScalarsConverter
,它支持字符串和其他基元类型。
- 使用
ScalarsConverterFactory
实例对构建器调用addConverterFactory()
。
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
- 使用
baseUrl()
方法为 Web 服务添加基础网址。 - 调用
build()
以创建 Retrofit 对象。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- 在对 Retrofit 构建器的调用的下方,定义一个名为
MarsApiService
的接口,该接口定义 Retrofit 如何使用 HTTP 请求与网络服务器通信。
interface MarsApiService {
}
- 向
MarsApiService
接口添加一个名为getPhotos()
的函数,以从 Web 服务中获取响应字符串。
interface MarsApiService {
fun getPhotos()
}
- 使用
@GET
注解告知 Retrofit 这是 GET 请求,并为该 Web 服务方法指定端点。在这种情况下,端点为photos
。如上个任务中所述,您将在此 Codelab 中使用 /photos 端点。
import retrofit2.http.GET
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
调用 getPhotos()
方法时,Retrofit 会将端点 photos
附加到您用于启动请求的基准网址(由您在 Retrofit 构建器中定义)。
- 将函数的返回值类型添加到
String
。
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
对象声明
在 Kotlin 中,对象声明用于声明单例对象。单例模式可确保对于一个对象只创建一个实例,并提供一个对该对象的全局访问点。对象的初始化是线程安全操作,在首次访问时完成。
以下是对象声明及其访问的示例。对象声明的名称后面跟有 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()
函数的成本很高。该应用只需要一个 Retrofit API 服务实例,因此,您可以使用对象声明向应用的其余部分公开该服务。
- 在
MarsApiService
接口声明外,定义一个名为MarsApi
的公共对象,以初始化 Retrofit 服务。此对象是应用的其余部分可以访问的公开单例对象。
object MarsApi {}
- 在
MarsApi
对象声明内,添加一个名为retrofitService
、类型为MarsApiService
的延迟初始化的 Retrofit 对象属性。您可以进行这种延迟初始化,确保其在首次使用时进行初始化。忽略这个错误,您将在后续步骤中修复它。
object MarsApi {
val retrofitService : MarsApiService by lazy {}
}
- 使用带有
MarsApiService
界面的retrofit.create()
方法初始化retrofitService
变量。
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
Retrofit 设置已完成!每次您的应用调用 MarsApi.retrofitService
时,调用方都会访问同一个单例 Retrofit 对象,该对象会实现在首次访问时创建的 MarsApiService
。在下一个任务中,您将使用您实现的 Retrofit 对象。
在 MarsViewModel 中调用 Web 服务
在此步骤中,您将实现 getMarsPhotos()
方法,该方法会调用 REST 服务,然后处理返回的 JSON 字符串。
ViewModelScope
viewModelScope
是为应用中的每个 ViewModel
定义的内置协程作用域。在此作用域内启动的协程会在 ViewModel
被清除时自动取消。
您可以使用 viewModelScope
启动协程,并在后台发出 Web 服务请求。由于 viewModelScope
属于 ViewModel
,因此,即使应用发生配置更改,请求也会继续发出。
- 在
MarsApiService.kt
文件中,将getPhotos()
设置为挂起函数,使其异步,并且不会阻塞发起调用的线程。您可以从viewModelScope
内调用此函数。
@GET("photos")
suspend fun getPhotos(): String
- 打开
ui/screens/MarsViewModel.kt
文件。向下滚动到getMarsPhotos()
方法。删除用于将状态响应设置为"Set the Mars API Response here!"
的代码行,使getMarsPhotos()
方法为空。
private fun getMarsPhotos() {}
- 在
getMarsPhotos()
中,使用viewModelScope.launch
启动协程。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
private fun getMarsPhotos() {
viewModelScope.launch {}
}
- 在
viewModelScope
中,使用单例对象MarsApi
从retrofitService
接口调用getPhotos()
方法。将返回的响应保存在名为listResult
的val
中。
import com.example.marsphotos.network.MarsApi
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
- 将刚刚从后端服务器收到的结果分配给
marsUiState
。marsUiState
是一个可变状态对象,表示最近的网络请求的状态。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
- 运行应用。请注意,该应用会立即关闭,不一定会显示错误弹出窗口。应用发生了崩溃。
- 点击 Android Studio 中的 Logcat 标签页,并记下日志中以如下所示的代码行开头的错误消息:“
------- beginning of crash
”。
--------- 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 权限
Android 上的权限旨在保护 Android 用户的隐私。Android 应用必须声明或请求访问敏感用户数据(如联系人、通话记录和某些系统功能,如相机或互联网)的权限。
应用需要 INTERNET
权限才能访问互联网。连接到互联网会引起安全问题,因此我们默认应用没有连接互联网。您需要明确声明该应用需要访问互联网。系统会将此声明视为正常权限。如需详细了解 Android 权限及其类型,请参阅 Android 中的权限。
在此步骤中,您的应用通过在 AndroidManifest.xml
文件中添加 <uses-permission>
标签来声明它所需的权限。
- 打开
manifests/AndroidManifest.xml
。将下面这行代码添加到<application>
标签的前面:
<uses-permission android:name="android.permission.INTERNET" />
- 编译并再次运行应用。
如果互联网连接正常,您应该会看到包含火星照片相关数据的 JSON 文本。观察每条图片记录的 id
和 img_src
的重复规律。此 Codelab 稍后会详细介绍 JSON 格式。
- 点按设备或模拟器中的返回按钮,关闭应用。
异常处理
代码中存在一个 bug。若要查看,请执行以下步骤:
- 将设备或模拟器设为飞行模式,以模拟网络连接错误。
- 从“最近”菜单中重新打开应用,或从 Android Studio 中重启应用。
- 点击 Android Studio 中的 Logcat 标签页,并记下日志中如下所示的严重异常:
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.android.marsphotos, PID: 3302
此错误消息表示应用尝试连接并超时。在现实环境中,诸如此类的异常非常常见。与权限问题不同,此错误无法解决,但您可以自行处理。在下一步中,您将了解如何处理此类异常。
异常
异常是指在不通知用户的情况下,可能在运行时(并非编译时)出现并最终导致应用突然终止的错误。这可能会导致糟糕的用户体验。异常处理是一种机制,您可以利用该机制防止应用突然终止,并以人性化方式处理这种情况。
发生异常的原因可能非常简单,比如除以零就可能抛出异常,或者是网络连接错误。这些异常类似于上一个 Codelab 中讨论过的 IllegalArgumentException
。
连接到服务器时可能出现的问题包括:
- 在 API 中使用的网址或 URI 不正确。
- 服务器不可用,应用无法连接到服务器。
- 网络延迟问题。
- 设备的互联网连接状况不佳或无互联网连接。
这些异常无法在编译时进行处理,但您可以使用 try-catch
代码块在运行时处理异常。如需了解详情,请参阅异常。
try-catch 代码块的语法示例
try {
// some code that can cause an exception.
}
catch (e: SomeException) {
// handle the exception to avoid abrupt termination.
}
在 try
代码块中,您可以在预期会引发异常的位置添加代码。在您的应用中,这会是一次网络调用。在 catch
代码块中,您需要实现用于防止应用突然终止的代码。如果存在异常,系统会执行 catch
代码块,以从错误中恢复,而不是突然终止应用。
- 在
getMarsPhotos()
中的launch
代码块内,围绕MarsApi
调用添加一个try
代码块来处理异常。 - 在
try
代码块之后添加一个catch
代码块。
import java.io.IOException
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
- 再次运行该应用。请注意,应用这次不会崩溃。
添加状态界面
在 MarsViewModel
类中,最近的网络请求的状态 marsUiState
会保存为可变状态对象。但是,这个类缺乏保存如下不同状态的功能:正在加载、成功和失败。
- 正在加载状态表示应用正在等待数据。
- 成功状态表示已成功从 Web 服务检索到数据。
- 错误状态表示存在网络或连接错误。
如需表示应用中的这三种状态,您将使用封装接口。sealed interface
通过限制可能的值来轻松管理状态。在 Mars Photos 应用中,您将 marsUiState
网络响应限制为三种状态(数据类对象):正在加载、成功和错误,如以下代码所示:
// No need to copy over
sealed interface MarsUiState {
data class Success : MarsUiState
data class Loading : MarsUiState
data class Error : MarsUiState
}
在上述代码段中,如果返回成功响应,您会从服务器收到火星照片信息。为了存储数据,请向 Success
数据类添加一个构造函数参数。
对于 Loading
和 Error
状态,您无需设置新数据和创建新对象;只需传递网络响应即可。将 data
类更改为 Object
,以便为网络响应创建对象。
- 打开
ui/MarsViewModel.kt
文件。在 import 语句后,添加MarsUiState
封装接口。添加后,MarsUiState
对象的值就会变得详尽。
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
object Error : MarsUiState
object Loading : MarsUiState
}
- 在
MarsViewModel
类中,更新marsUiState
定义。将类型更改为MarsUiState
,将MarsUiState.Loading
作为其默认值。将 setter 设为不公开,以保护写入marsUiState
的内容。
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
- 向下滚动到
getMarsPhotos()
方法。将marsUiState
值更新为MarsUiState.Success
,并传递listResult
。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
- 在
catch
代码块内部,处理故障响应。将MarsUiState
设为Error
。
catch (e: IOException) {
marsUiState = MarsUiState.Error
}
- 您可以从
try-catch
代码块中取出marsUiState
分配。完成后的函数应如以下代码所示:
private fun getMarsPhotos() {
viewModelScope.launch {
marsUiState = try {
val listResult = MarsApi.retrofitService.getPhotos()
MarsUiState.Success(listResult)
} catch (e: IOException) {
MarsUiState.Error
}
}
}
- 在
screens/HomeScreen.kt
文件中,对marsUiState
添加一个when
表达式。如果marsUiState
为MarsUiState.Success
,则调用ResultScreen
并传入marsUiState.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()
)
}
}
- 在
when
代码块内,为MarsUiState.Loading
和MarsUiState.Error
添加检查项。让该应用显示LoadingScreen
、ResultScreen
和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
的动画。(您在预览中看不到这段动画。)
- 在
screens/HomeScreen.kt
文件中的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)
)
}
- 在
LoadingScreen
可组合项下方,添加以下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 等常见数据格式之一。每次调用都会返回结构化数据,并且您的应用需要知道该结构是什么,才能从响应中读取数据。
例如,在此应用中,您将从 https://android-kotlin-fun-mars-server.appspot.com/photos 服务器检索数据。如果在浏览器中输入此网址,您会看到以 JSON 格式显示的火星表面的 ID 和图片网址列表!
示例 JSON 响应的结构
JSON 响应的结构具有以下特征:
- JSON 响应是一个数组(以英文方括号表示)。该数组包含 JSON 对象。
- JSON 对象括在英文大括号中。
- 每个 JSON 对象都包含一组以英文逗号分隔的键值对。
- 键值对中的键和值以英文冒号分隔。
- 名称会用英文引号引起来。
- 值可以是数字、字符串、布尔值、数组、对象(JSON 对象)或 null。
例如,img_src
是一个网址,而该网址是一个字符串。如果将该网址粘贴到网络浏览器中,您会看到一张火星表面图片。
现在,在您的应用中,您将从 Mars Web 服务获得 JSON 响应,这是一个不错的开始。但显示图片实际上需要的是 Kotlin 对象,而不是大型 JSON 字符串。这个过程称为“反序列化”。
序列化是将应用所用的数据转换成可通过网络传输的格式的过程。与“序列化”相反,“反序列化”是从外部来源(如服务器)读取数据并将其转换为运行时对象的过程。这两者是大多数通过网络交换数据的应用的必备组件。
kotlinx.serialization
提供了一系列库,用于将 JSON 字符串转换为 Kotlin 对象。Kotlin 序列化转换器是一个社区开发的第三方库,可与 Retrofit 配合使用。
在此任务中,您将使用 kotlinx.serialization
库,将 Web 服务的 JSON 响应解析成表示火星照片的有用 Kotlin 对象。您需要更改应用,而不是显示原始 JSON,而该应用会显示返回的火星照片数。
添加 kotlinx.serialization
库依赖项
- 打开
build.gradle.kts (Module :app)
。 - 在
plugins
代码块中,添加kotlinx serialization
插件。
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
- 在
dependencies
部分,添加以下代码以包含kotlinx.serialization
依赖项。此依赖项可为 Kotlin 项目提供 JSON 序列化。
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
- 在
dependencies
代码块中,找到 Retrofit 标量转换器所在的代码行,并将其更改为使用kotlinx-serialization-converter
:
将以下代码
// 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,以使用新的依赖项重建项目。
实现火星照片数据类
您从 Web 服务中获取的 JSON 响应的示例条目类似于您之前看到的内容:
[
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]
请注意,在上面的示例中,每个火星照片条目都具有以下 JSON 键值对:
id
:资源的 ID,用字符串表示。由于它封装在英文引号 (" "
) 中,因此它是String
类型,而不是Integer
。img_src
:图片的网址,用字符串表示。
kotlinx.serialization 会解析此 JSON 数据并将其转换为 Kotlin 对象。为此,kotlinx.serialization 需要一个 Kotlin 数据类来存储解析后的结果。在此步骤中,您将创建数据类 MarsPhoto
。
- 右键点击 network 软件包,然后依次选择 New > Kotlin File/Class。
- 在对话框中,选择 Class,然后输入
MarsPhoto
作为类的名称。系统将在network
软件包中创建一个名为MarsPhoto.kt
的新文件。 - 在类定义前添加
data
关键字,使MarsPhoto
成为数据类。 - 将英文大括号
{}
更改为英文圆括号()
。此更改会引发错误,因为数据类必须至少定义一个属性。
data class MarsPhoto()
- 将以下属性添加到
MarsPhoto
类定义中。
data class MarsPhoto(
val id: String, val img_src: String
)
- 使
MarsPhoto
类可序列化,并为其添加@Serializable
注解。
import kotlinx.serialization.Serializable
@Serializable
data class MarsPhoto(
val id: String, val img_src: String
)
请注意,MarsPhoto
类中的每个变量都对应于 JSON 对象中的一个键名。为了匹配特定 JSON 响应中的类型,您可以为所有值使用 String
对象。
kotlinx serialization
解析 JSON 时,它会按名称匹配键,并用适当的值填充数据对象。
@SerialName 注解
有时,JSON 响应中的键名可能会使 Kotlin 属性引起混淆,或者可能与建议的编码样式不匹配。例如,在 JSON 文件中,img_src
键使用下划线,而按照惯例,Kotlin 属性则使用大写和小写字母(“驼峰命名法”)。
如需在数据类中使用与 JSON 响应中的键名不同的变量名称,请使用 @SerialName
注解。在下例中,数据类中变量的名称为 imgSrc
。您可以使用 @SerialName(value = "img_src")
将该变量映射到 JSON 属性 img_src
。
- 将
img_src
键所在的代码行替换为如下所示的代码行。
import kotlinx.serialization.SerialName
@SerialName(value = "img_src")
val imgSrc: String
更新 MarsApiService 和 MarsViewModel
在此任务中,您将使用 kotlinx.serialization
转换器将 JSON 对象转换为 Kotlin 对象。
- 打开
network/MarsApiService.kt
。 - 请注意
ScalarsConverterFactory
的未解析引用错误。这些错误是由上一部分中的 Retrofit 依赖项更改导致的。 - 删除
ScalarConverterFactory
的导入作业。您稍后将修复另一个错误。
移除:
import retrofit2.converter.scalars.ScalarsConverterFactory
- 在
retrofit
对象声明中,将 Retrofit 构建器更改为使用kotlinx.serialization
而不是ScalarConverterFactory
。
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()
现在,您已具备 kotlinx.serialization
,可以要求 Retrofit 从 JSON 数组中返回 MarsPhoto
对象列表,而不是返回 JSON 字符串。
- 更新
MarsApiService
接口,以便 Retrofit 返回MarsPhoto
对象列表,而不是返回String
。
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
- 对
viewModel
进行类似的更改。打开MarsViewModel.kt
,并向下滚动到getMarsPhotos()
方法。
在 getMarsPhotos()
方法中,listResult
是 List<MarsPhoto>
,而不再是 String
。该列表的大小就是已接收和解析的照片数。
- 如需输出检索的照片数,请按如下方式更新
marsUiState
:
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
"Success: ${listResult.size} Mars photos retrieved"
)
- 确保在设备或模拟器中关闭飞行模式。编译并运行应用。
这一次,消息应显示 Web 服务返回的资源数,而不是较大的 JSON 字符串:
9. 解决方案代码
如需下载已完成的 Codelab 的代码,您可以使用以下 Git 命令:
$ 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 中打开。
如果您想查看此 Codelab 的解决方案代码,请前往 GitHub 查看。
10. 总结
REST Web 服务
- Web 服务是通过互联网提供的基于软件的功能,可让您的应用发出请求并获取返回的数据。
- 常见 Web 服务使用的是 REST 架构。提供 REST 架构的 Web 服务称为 RESTful 服务。RESTful Web 服务是使用标准网络组件和协议构建的。
- 您可通过 URI 以标准化方式向 REST Web 服务发出请求。
- 若要使用 Web 服务,应用必须建立网络连接,然后与该服务进行通信。然后,应用必须接收响应数据,并将该数据解析成应用可以使用的格式。
- Retrofit 库是一个客户端库,可让应用向 REST Web 服务发出请求。
- 使用转换器指示 Retrofit 如何处理它发送至 Web 服务的数据,以及它从 Web 服务获取的返回数据。例如,
ScalarsConverter
会将 Web 服务数据视为String
或其他基元。 - 如需让应用能够连接到互联网,请在 Android 清单中添加
"android.permission.INTERNET"
权限。 - 延迟初始化会将对象创建操作委派为在首次使用时执行。它会创建引用,但不会创建对象。在首次访问对象后,此后每次访问都会创建并使用引用。
JSON 解析
- Web 服务的响应通常会采用 JSON 格式(一种表示结构化数据的通用格式)。
- JSON 对象是键值对的集合。
- JSON 对象集合是一个 JSON 数组。作为 Web 服务的响应,您会得到一个 JSON 数组。
- 键值对中的键会用英文引号引起来。值可以是数字或字符串。
- 在 Kotlin 中,数据序列化工具位于单独的组件 kotlinx.serialization 中。kotlinx.serialization 提供了一系列库,用于将 JSON 字符串转换为 Kotlin 对象。
- Kotlin 序列化转换器库是一个由社区开发的库,适用于 Retrofit:retrofit2-kotlinx-serialization-converter。
- kotlinx.serialization 可将 JSON 响应中的键与具有相同名称的数据对象中的属性进行匹配。
- 如需为键使用不同的属性名称,请使用
@SerialName
注解和 JSON 键value
为该属性添加注解。
11. 了解更多内容
Android 开发者文档:
Kotlin 文档:
其他: