1. 准备工作
市场上大多数 Android 应用都会连接到互联网以执行网络操作,例如从后端服务器检索电子邮件、消息或其他信息。Gmail、YouTube 和 Google 相册就是连接到互联网以显示用户数据的示例应用。
在此 Codelab 中,您将使用开源和社区驱动的库来构建数据层并从后端服务器获取数据。这极大地简化了数据获取,还有助于应用遵循 Android 最佳实践,例如在后台线程上执行操作。如果互联网速度慢或不可用,您还将显示错误消息,以便用户了解任何网络连接问题。
前提条件
- 了解如何创建 Composable 函数的基础知识。
- 了解如何使用 Android 架构组件
ViewModel
的基础知识。 - 了解如何使用协程执行耗时任务的基础知识。
- 了解如何在
build.gradle.kts
中添加依赖项的基础知识。
您将学习到什么
- 什么是 REST Web 服务。
- 如何使用 Retrofit 库连接到互联网上的 REST Web 服务并获取响应。
- 如何使用 Serialization (kotlinx.serialization) 库将 JSON 响应解析为数据对象。
您将做什么
- 修改入门应用,使其发出 Web 服务 API 请求并处理响应。
- 使用 Retrofit 库为您的应用实现数据层。
- 使用 kotlinx.serialization 库将 Web 服务的 JSON 响应解析为应用的数据对象列表,并将其附加到 UI 状态。
- 使用 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
的包文件夹。这是应用的 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
在数据更改时更新应用 UI。
5. Web 服务与 Retrofit
火星照片数据存储在 Web 服务器上。要将此数据获取到您的应用中,您需要建立连接并与互联网上的服务器通信。
现今大多数 Web 服务器都使用一种常见的无状态 Web 架构来运行 Web 服务,该架构称为 REST,代表 REpresentational State Transfer(表征状态转移)。提供这种架构的 Web 服务称为 RESTful 服务。
对 RESTful Web 服务的请求以标准化方式进行,通过 Uniform Resource Identifiers (URI) 进行。URI 按名称标识服务器中的资源,不暗示其位置或访问方式。例如,在本课程的应用中,您使用以下服务器 URI 检索图像 URL。(此服务器同时托管火星房地产和火星照片)
android-kotlin-fun-mars-server.appspot.com
URL (Uniform Resource Locator,统一资源定位符) 是 URI 的一个子集,它指定了资源存在的位置以及检索资源的方式。
例如
以下 URL 获取火星上可用房地产列表
https://android-kotlin-fun-mars-server.appspot.com/realestate
以下 URL 获取火星照片列表
https://android-kotlin-fun-mars-server.appspot.com/photos
这些 URL 指的是一个已标识的资源,例如 /realestate 或 /photos,可以通过网络上的 Hypertext Transfer Protocol (http:) 获取。您在此 Codelab 中使用的是 /photos 端点。端点是允许您访问服务器上运行的 Web 服务的 URL。
Web 服务请求
每个 Web 服务请求都包含一个 URI,并使用与 Chrome 等 Web 浏览器相同的 HTTP 协议传输到服务器。HTTP 请求包含一个操作,用于告诉服务器要做什么。
常见的 HTTP 操作包括
- GET 用于检索服务器数据。
- POST 用于在服务器上创建新数据。
- PUT 用于更新服务器上的现有数据。
- DELETE 用于删除服务器上的数据。
您的应用向服务器发出 HTTP GET 请求以获取火星照片信息,然后服务器将响应返回给您的应用,其中包括图像 URL。
Web 服务的响应通常采用常见的 数据格式之一进行格式化,例如 XML (eXtensible Markup Language) 或 JSON (JavaScript Object Notation)。JSON 格式以键值对的形式表示结构化数据。应用使用 JSON 与 REST API 通信,您将在后续任务中详细了解 JSON。
在本任务中,您将与服务器建立网络连接,与服务器通信,并接收 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")
这两个库协同工作。第一个依赖项是 Retrofit2 库本身,第二个依赖项是 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 对象,其中包含基本 URL 和用于转换字符串的转换器工厂。
- 创建一个接口,用于解释 Retrofit 如何与 Web 服务器通信。
- 创建一个 Retrofit 服务,并将实例公开给应用的其余部分的 API 服务。
实现以上步骤
- 在 Android 项目面板中右键点击包 com.example.marsphotos,然后选择 New > Package。
- 在弹出的窗口中,在建议的包名末尾附加 network。
- 在新包下创建一个新的 Kotlin 文件。将其命名为
MarsApiService
。 - 打开
network/MarsApiService.kt
。 - 为 Web 服务的基本 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 需要 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 服务的基本 URL。 - 调用
build()
创建 Retrofit 对象。
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- 在调用 Retrofit 构建器下方,定义一个名为
MarsApiService
的接口,该接口定义 Retrofit 如何使用 HTTP 请求与 Web 服务器通信。
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 会将您在 Retrofit 构建器中定义的基本 URL 附加到端点 photos
,用于发起请求。
- 将函数的返回类型设置为
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 服务的一个实例,因此您使用 object declaration 将服务公开给应用的其余部分。
- 在
MarsApiService
接口声明之外,定义一个名为MarsApi
的公共对象来初始化 Retrofit 服务。此对象是应用的其余部分可以访问的公共单例对象。
object MarsApi {}
- 在
MarsApi
对象声明内部,添加一个延迟初始化的 Retrofit 对象属性,命名为retrofitService
,类型为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 中调用 Web 服务
在此步骤中,您将实现 getMarsPhotos()
方法,该方法调用 REST 服务,然后处理返回的 JSON 字符串。
ViewModelScope
一个 viewModelScope
是为应用中的每个 ViewModel
定义的内置协程作用域。在此作用域中启动的任何协程在 ViewModel
被清除时会自动取消。
您可以使用 viewModelScope
启动协程并在后台进行 Web 服务请求。由于 viewModelScope
属于 ViewModel
,即使应用发生配置更改,请求也会继续。
- 在
MarsApiService.kt
文件中,将getPhotos()
设为 suspend 函数,使其异步执行且不阻塞调用线程。您将从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
是一个可变状态对象,表示最近 Web 请求的状态。
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 中使用的 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
类中,最近 Web 请求的状态 marsUiState
被保存为一个可变状态对象。然而,此类缺乏保存不同状态(加载中、成功和失败)的能力。
- 加载中状态表示应用正在等待数据。
- 成功状态表示已从 Web 服务成功检索到数据。
- 错误状态表示任何网络或连接错误。
为了在您的应用中表示这三种状态,您可以使用一个 密封接口。密封接口 sealed interface
通过限制可能的值来方便管理状态。在 Mars Photos 应用中,您将 marsUiState
Web 响应限制为三种状态(数据类对象):加载中、成功和错误,代码如下所示
// No need to copy over
sealed interface MarsUiState {
data class Success : MarsUiState
data class Loading : MarsUiState
data class Error : MarsUiState
}
在上述代码片段中,如果响应成功,您将收到来自服务器的火星照片信息。为了存储数据,向 Success
数据类添加一个构造函数参数。
对于 Loading
和 Error
状态,您不需要设置新数据并创建新对象;您只需传递 Web 响应。将 data
类更改为 Object
,以创建 Web 响应的对象。
- 打开
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 设为 private 以保护对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 浏览器中时,您会看到一张火星表面图像。
现在,在您的应用中,您正在从 Mars Web 服务获取 JSON 响应,这是一个很好的开始。但要显示图像,您真正需要的是 Kotlin 对象,而不是一串很大的 JSON 字符串。这个过程称为 反序列化。
序列化 是将应用程序使用的数据转换为可通过网络传输的格式的过程。与 序列化 相反,反序列化 是从外部源(如服务器)读取数据并将其转换为运行时对象的过程。它们都是大多数通过网络交换数据的应用程序的重要组成部分。
方法 kotlinx.serialization
提供了一系列库,可将 JSON 字符串转换为 Kotlin 对象。有一个社区开发的第三方库与 Retrofit 配合使用,即 Kotlin Serialization Converter。
在本任务中,您将使用 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,使用新依赖项重新构建项目。
实现 Mars Photo 数据类
您从 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
:图像的 URL,作为字符串。
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
)
- 通过使用
@Serializable
注解MarsPhoto
类,使其可序列化。
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 语句。您稍后会修复其他错误。
移除
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
不再是 String
,而是 List<MarsPhoto>
。该列表的大小是已接收和解析的照片数量。
- 要打印检索到的照片数量,按如下方式更新
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 服务使用标准 Web 组件和协议构建。
- 您通过 URI 以标准化方式向 REST Web 服务发出请求。
- 要使用 Web 服务,应用必须建立网络连接并与服务通信。然后应用必须接收响应数据并将其解析为应用可以使用的格式。
- 方法 Retrofit 库是一个客户端库,它使您的应用能够向 REST Web 服务发出请求。
- 使用转换器告诉 Retrofit 如何处理发送到 Web 服务以及从 Web 服务获取的数据。例如,
ScalarsConverter
将 Web 服务数据视为String
或其他原始类型。 - 要使您的应用能够连接到互联网,请在 Android manifest 中添加
"android.permission.INTERNET"
权限。 - 延迟初始化将对象的创建委托给首次使用时进行。它创建引用,但不创建对象。当对象首次被访问时,会创建一个引用,之后每次都会使用该引用。
JSON 解析
- Web 服务的响应通常以 JSON 格式进行格式化,这是一种表示结构化数据的常见格式。
- JSON 对象是键值对的集合。
- JSON 对象的集合是 JSON 数组。您从 Web 服务获取的响应是一个 JSON 数组。
- 键值对中的键用引号包围。值可以是数字或字符串。
- 在 Kotlin 中,数据序列化工具位于一个单独的组件 kotlinx.serialization 中。kotlinx.serialization 提供了一系列库,可将 JSON 字符串转换为 Kotlin 对象。
- 有一个社区开发的用于 Retrofit 的 Kotlin 序列化转换器库:retrofit2-kotlinx-serialization-converter。
- 方法 kotlinx.serialization 将 JSON 响应中的键与数据对象中具有相同名称的属性进行匹配。
- 要为键使用不同的属性名称,请使用
@SerialName
注解和 JSON 键value
为该属性添加注解。
11. 了解更多
Android 开发者文档
Kotlin 文档
其他