1. 开始之前
市场上大多数 Android 应用都会连接互联网以执行网络操作,例如从后端服务器检索电子邮件、消息或其他信息。Gmail、YouTube 和 Google 相册就是连接互联网以显示用户数据的示例应用。
在本 Codelab 中,您将使用开源和社区驱动的库来构建数据层并从后端服务器获取数据。这极大地简化了数据获取,并帮助应用遵循 Android 最佳实践,例如在后台线程上执行操作。如果互联网速度慢或不可用,您还将显示错误消息,这将使用户了解任何网络连接问题。
先决条件
- 创建可组合函数的基本知识。
- 使用 Android 架构组件
ViewModel
的基本知识。 - 使用协程执行长时间运行任务的基本知识。
- 在
build.gradle.kts
中添加依赖项的基本知识。
你将学到什么
- 什么是REST 网络服务。
- 如何使用Retrofit 库连接到互联网上的 REST 网络服务并获取响应。
- 如何使用序列化 (kotlinx.serialization) 库将 JSON 响应解析为数据对象。
你将做什么
- 修改启动应用以发出网络服务 API 请求并处理响应。
- 使用 Retrofit 库为您的应用实现数据层。
- 使用kotlinx.serialization库将网络服务的 JSON 响应解析到应用程序的数据对象列表中,并将其附加到 UI 状态。
- 使用 Retrofit 对协程的支持来简化代码。
你需要什么
- 一台装有 Android Studio 的电脑
- 火星照片应用的启动代码
2. 应用概述
您将使用名为火星照片的应用,该应用显示火星表面的图像。此应用连接到网络服务以检索和显示火星照片。这些图像是来自 NASA 火星探测器的火星实况照片。下图是最终应用的屏幕截图,其中包含一个图像网格。
您在本 Codelab 中构建的应用版本不会有很多视觉效果。本 Codelab 侧重于应用的数据层部分,以连接到互联网并使用网络服务下载原始属性数据。为了确保应用正确检索和解析此数据,您可以在Text
可组合项中打印从后端服务器接收的照片数量。
3. 探索火星照片启动应用
下载启动代码
要开始,请下载启动代码
或者,您可以克隆代码的 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
的包文件夹。这是应用的 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
:
- 此活动唯一要做的任务是加载
ViewModel
并显示MarsPhotosApp
可组合项。
4. 网络服务的介绍
在本 Codelab 中,您将创建一个网络服务层,该层与后端服务器通信并获取所需数据。您将使用名为Retrofit 的第三方库来实现此任务。您稍后将了解更多信息。ViewModel
与数据层通信,应用的其余部分对此实现是透明的。
MarsViewModel
负责进行网络调用以获取火星照片数据。在ViewModel
中,您使用MutableState
在数据更改时更新应用 UI。
5. 网络服务和 Retrofit
火星照片数据存储在网络服务器上。要将此数据添加到您的应用中,您需要建立连接并与互联网上的服务器通信。
如今大多数网络服务器都使用称为REST(代表表示性状态转移)的通用无状态网络架构运行网络服务。提供此架构的网络服务称为 RESTful 服务。
通过统一资源标识符 (URI) 以标准化方式向 RESTful 网络服务发出请求。URI 通过名称标识服务器中的资源,而不暗示其位置或如何访问它。例如,在本课程的应用中,您使用以下服务器 URI 检索图像 URL。(此服务器同时托管火星房地产和火星照片)
android-kotlin-fun-mars-server.appspot.com
URL(统一资源定位符)是 URI 的一个子集,它指定资源存在的位置以及检索它的机制。
例如
以下 URL 获取火星上可用的房地产列表
https://android-kotlin-fun-mars-server.appspot.com/realestate
以下 URL 获取火星照片列表
https://android-kotlin-fun-mars-server.appspot.com/photos
这些 URL 指的是已识别的资源,例如/realestate 或/photos,可以通过超文本传输协议 (http:) 从网络获取。您在本 Codelab 中使用/photos 端点。端点是允许您访问在服务器上运行的网络服务的 URL。
网络服务请求
每个网络服务请求都包含一个 URI,并使用与 Chrome 等网络浏览器使用的相同的 HTTP 协议传输到服务器。HTTP 请求包含一个操作,以告诉服务器要做什么。
常见的 HTTP 操作包括
- GET 用于检索服务器数据。
- POST 用于在服务器上创建新数据。
- PUT 用于更新服务器上的现有数据。
- DELETE 用于从服务器删除数据。
您的应用向服务器发出 HTTP GET 请求以获取火星照片信息,然后服务器向您的应用返回响应,包括图像 URL。
网络服务的响应采用常用数据格式之一进行格式化,例如 XML(可扩展标记语言)或 JSON(JavaScript 对象表示法)。JSON 格式以键值对的形式表示结构化数据。应用使用 JSON 与 REST API 通信,您将在后面的任务中了解更多信息。
在此任务中,您将建立与服务器的网络连接,与服务器通信并接收 JSON 响应。您将使用已为您编写的后端服务器。在本 Codelab 中,您将使用 Retrofit 库(一个第三方库)与后端服务器通信。
外部库
外部库,或第三方库,就像Android核心API的扩展一样。本课程中使用的库是开源的,由社区开发和维护,依靠来自全球庞大Android社区的集体贡献。这些资源帮助像您一样的Android开发者构建更好的应用。
Retrofit库
在本Codelab中,您使用Retrofit库与RESTful火星网络服务进行通信,这是一个良好支持和维护的库的良好示例。您可以通过查看其GitHub页面并查看已解决和未解决的问题(有些是功能请求)来判断这一点。如果开发者定期解决问题并响应功能请求,则该库可能维护良好,并且是应用中使用的良好候选者。您还可以参考Retrofit文档以了解更多关于该库的信息。
Retrofit库与REST后端进行通信。它生成代码,但您需要根据传递给它的参数提供网络服务的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")
这两个库协同工作。第一个依赖项是Retrofit2库本身,第二个依赖项是Retrofit标量转换器。Retrofit2是Retrofit库的更新版本。此标量转换器使Retrofit能够将JSON结果作为String
返回。JSON是一种用于在客户端和服务器之间存储和传输数据的格式。您将在后面的章节中学习JSON。
- 单击立即同步以使用新的依赖项重建项目。
6. 连接到互联网
您使用Retrofit库与火星网络服务通信,并将原始JSON响应作为String
显示。占位符Text
显示返回的JSON响应字符串或指示连接错误的消息。
Retrofit根据网络服务的内容为应用创建网络API。它从网络服务获取数据,并将其通过一个单独的转换器库,该库知道如何解码数据并将其以对象的形式返回,例如String
。Retrofit内置支持流行的数据格式,例如XML和JSON。Retrofit最终为您创建调用和使用此服务的代码,包括关键细节,例如在后台线程上运行请求。
在此任务中,您将数据层添加到您的火星照片项目中,您的ViewModel
使用它与网络服务进行通信。您将通过以下步骤实现Retrofit服务API:
- 创建一个数据源,
MarsApiService
类。 - 创建一个带有基本URL和转换工厂的Retrofit对象来转换字符串。
- 创建一个接口来解释Retrofit如何与网络服务器通信。
- 创建一个Retrofit服务,并将该实例公开给应用的其余部分。
实现上述步骤
- 右键单击Android项目窗格中的包com.example.marsphotos,然后选择新建 > 包。
- 在弹出窗口中,将network添加到建议的包名称的末尾。
- 在新包下创建一个新的Kotlin文件。将其命名为
MarsApiService
。 - 打开
network/MarsApiService.kt
。 - 为网络服务添加以下基本URL常量。
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- 在该常量下方添加一个Retrofit构建器来构建和创建Retrofit对象。
import retrofit2.Retrofit
private val retrofit = Retrofit.Builder()
Retrofit需要网络服务的基URI和一个转换工厂来构建网络服务API。转换器告诉Retrofit如何处理从网络服务返回的数据。在本例中,您希望Retrofit从网络服务获取JSON响应并将其作为String
返回。Retrofit有一个ScalarsConverter
支持字符串和其他基本类型。
- 使用
ScalarsConverterFactory
的实例在构建器上调用addConverterFactory()
。
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
- 使用
baseUrl()
方法添加网络服务的基URL。 - 调用
build()
来创建Retrofit对象。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- 在对Retrofit构建器的调用下方,定义一个名为
MarsApiService
的接口,该接口定义了Retrofit如何使用HTTP请求与网络服务器通信。
interface MarsApiService {
}
- 向
MarsApiService
接口添加一个名为getPhotos()
的函数,以从网络服务获取响应字符串。
interface MarsApiService {
fun getPhotos()
}
- 使用
@GET
注解告诉Retrofit这是一个GET请求,并为该网络服务方法指定一个端点。在本例中,端点是photos
。如前面的任务中所述,您将在本Codelab中使用/photos端点。
import retrofit2.http.GET
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
调用getPhotos()
方法时,Retrofit会将端点photos
附加到在Retrofit构建器中定义的用于启动请求的基URL。
- 将函数的返回类型添加到
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
的延迟初始化Retrofit对象属性,类型为MarsApiService
。您进行此延迟初始化是为了确保在第一次使用时初始化它。忽略错误,您将在接下来的步骤中修复它。
object MarsApi {
val retrofitService : MarsApiService by lazy {}
}
- 使用
retrofit.create()
方法和MarsApiService
接口初始化retrofitService
变量。
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
Retrofit设置已完成!每次您的应用调用MarsApi.retrofitService
时,调用者都会访问实现MarsApiService
的相同的单例Retrofit对象,该对象在第一次访问时创建。在下一个任务中,您将使用您实现的Retrofit对象。
在MarsViewModel中调用网络服务
在此步骤中,您将实现getMarsPhotos()
方法,该方法调用REST服务,然后处理返回的JSON字符串。
ViewModelScope
一个viewModelScope
是为应用中的每个ViewModel
定义的内置协程作用域。如果ViewModel
被清除,则在此作用域中启动的任何协程都会自动取消。
您可以使用viewModelScope
启动协程并在后台发出网络服务请求。由于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格式的信息。
- 点击设备或模拟器上的**返回**按钮关闭应用。
异常处理
您的代码中存在错误。请执行以下步骤查看错误。
- 将您的设备或模拟器置于飞行模式以模拟网络连接错误。
- 从最近使用的应用菜单重新打开应用,或从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中使用的URL或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) {
}
}
- 再次运行应用。注意这次应用不会崩溃。
添加状态UI
在MarsViewModel
类中,最近一次网页请求的状态marsUiState
被保存为一个可变状态对象。但是,此类缺少保存不同状态的能力:加载、成功和失败。
- **加载**状态表示应用正在等待数据。
- **成功**状态表示已从网络服务成功检索数据。
- **错误**状态表示任何网络或连接错误。
要在您的应用程序中表示这三种状态,您可以使用一个密封接口。sealed interface
通过限制可能的值来简化状态管理。在火星照片应用中,您可以将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
}
- 您可以将
marsUiState
赋值提升到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
文件中,在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
。此drawable是一个动画,它围绕中心点旋转图像drawableloading_img.xml
。(您在预览中看不到动画。)
- 在
screens/HomeScreen.kt
文件中,在HomeScreen
可组合项下方,添加以下LoadingScreen
可组合函数以显示加载动画。loading_img
drawable 资源包含在入门代码中。
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服务器检索数据。当您在浏览器中输入此URL时,您将看到火星表面的ID和图像URL列表,它们采用JSON格式!
示例JSON响应的结构
JSON响应的结构具有以下特征:
- JSON响应是一个数组,由方括号表示。数组包含JSON对象。
- JSON对象用大括号括起来。
- 每个JSON对象都包含一组用逗号分隔的键值对。
- 冒号分隔键值对中的键和值。
- 名称用引号括起来。
- 值可以是数字、字符串、布尔值、数组、对象(JSON对象)或null。
例如,img_src
是一个URL,它是一个字符串。当您将URL粘贴到Web浏览器中时,您会看到火星表面的图像。
在您的应用中,您现在正在从火星网络服务获取JSON响应,这是一个很好的开始。但是,您真正需要显示图像的是Kotlin对象,而不是一个很大的JSON字符串。此过程称为反序列化。
序列化是将应用程序使用的数据转换为可以通过网络传输的格式的过程。与序列化相反,反序列化是从外部源(如服务器)读取数据并将其转换为运行时对象的过程。它们都是大多数通过网络交换数据的应用程序的重要组成部分。
kotlinx.serialization
提供了一套库,用于将 JSON 字符串转换为 Kotlin 对象。社区开发了一个与 Retrofit 兼容的第三方库,Kotlin Serialization Converter。
在本任务中,您将使用 kotlinx.serialization
库,将网络服务的 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")
- 单击立即同步以使用新的依赖项重建项目。
实现 Mars Photo 数据类
您从网络服务获得的 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
:图像的 URL,作为字符串。
kotlinx.serialization 解析此 JSON 数据并将其转换为 Kotlin 对象。为此,kotlinx.serialization 需要一个 Kotlin 数据类来存储解析结果。在此步骤中,您将创建数据类 MarsPhoto
。
- 右键单击 **network** 包,然后选择 **新建 > Kotlin 文件/类**。
- 在对话框中,选择 **类** 并输入
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 字符串。
- 更新 Retrofit 的
MarsApiService
接口,使其返回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"
)
- 确保设备或模拟器上的飞行模式已关闭。编译并运行应用程序。
这次,消息应显示从网络服务返回的属性数量,而不是大型 JSON 字符串
9. 解决方案代码
要下载完成的代码实验室的代码,您可以使用以下 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 中打开它。
如果您想查看此代码实验室的解决方案代码,请在 GitHub 上查看。
10. 总结
REST 网络服务
- 网络服务是通过互联网提供的基于软件的功能,它使您的应用程序能够发出请求并获取数据。
- 常见的网络服务使用 REST 架构。提供 REST 架构的网络服务被称为 *RESTful* 服务。RESTful 网络服务是使用标准 Web 组件和协议构建的。
- 您可以通过 URI 以标准化方式向 REST 网络服务发出请求。
- 要使用网络服务,应用程序必须建立网络连接并与服务进行通信。然后,应用程序必须接收和解析响应数据,使其成为应用程序可以使用的格式。
- Retrofit 库是一个客户端库,使您的应用程序能够向 REST 网络服务发出请求。
- 使用转换器来告诉 Retrofit 如何处理发送到网络服务的数据以及从网络服务获取的数据。例如,
ScalarsConverter
将网络服务数据视为String
或其他基本类型。 - 要在 Android 清单中启用应用程序进行互联网连接,请添加
"android.permission.INTERNET"
权限。 - 延迟初始化将对象的创建委托给第一次使用它的时候。它创建引用但不创建对象。当第一次访问对象时,将创建一个引用,并在以后每次使用时都使用它。
JSON 解析
- 网络服务的响应通常采用 JSON 格式,这是一种表示结构化数据的常用格式。
- JSON 对象是键值对的集合。
- JSON 对象的集合是 JSON 数组。您将从网络服务获得 JSON 数组作为响应。
- 键值对中的键用引号括起来。值可以是数字或字符串。
- 在 Kotlin 中,数据序列化工具位于单独的组件 kotlinx.serialization 中。kotlinx.serialization 提供了一套库,用于将 JSON 字符串转换为 Kotlin 对象。
- 有一个针对 Retrofit 的社区开发的 Kotlin Serialization Converter 库:retrofit2-kotlinx-serialization-converter。
- kotlinx.serialization 将 JSON 响应中的键与具有相同名称的数据对象中的属性匹配。
- 要对键使用不同的属性名称,请使用
@SerialName
注解和 JSON 键value
注解该属性。
11. 了解更多
Android 开发者文档
Kotlin 文档
其他