从互联网获取数据

1. 准备工作

市场中的大多数 Android 应用都需要连接到互联网才能执行网络操作,例如从后端服务器检索电子邮件、消息或其他信息。Gmail、YouTube 和 Google 相册等应用就需要连接到互联网才能显示用户数据。

在此 Codelab 中,您将使用依托于社区的开源库来构建数据层,并从后端服务器获取数据。这样可以简化数据提取,还有利于应用遵循 Android 最佳实践,例如在后台线程上执行操作。此外,如果互联网速度较慢或不可用,应用界面将显示一条错误消息,让用户及时获知网络连接问题。

前提条件

  • 了解有关如何创建可组合函数的基础知识。
  • 了解有关如何使用 Android 架构组件 ViewModel 的基础知识。
  • 了解有关如何使用协程处理长时间运行的任务的基础知识。
  • 了解有关如何在 build.gradle.kts 中添加依赖项的基础知识。

学习内容

实践内容

  • 修改起始应用以发出 Web 服务 API 请求,并处理响应。
  • 使用 Retrofit 库为您的应用实现数据层。
  • 使用 kotlinx.serialization 库将 Web 服务的 JSON 响应解析为应用的数据对象列表,并将其附加到界面状态。
  • 使用 Retrofit 对协程的支持来简化代码。

所需条件

  • 一台安装了 Android Studio 的计算机
  • Mars Photos 应用的起始代码

2. 应用概览

您将使用名为 Mars Photos 的应用,其中显示了火星表面的图片。该应用需要连接到 Web 服务,才能检索并显示火星照片。这些图片是由 NASA 的火星探测器拍摄的真实照片。下图是最终应用的屏幕截图,其中以网格形式列出了多张图片。

68f4ff12cc1e2d81.png

您在此 Codelab 中构建的应用版本不会有很多视觉上的亮点;此 Codelab 侧重于应用的数据层部分,旨在连接到互联网并使用 Web 服务下载原始资源数据。为了确保该应用正确检索和解析这些数据,您可以在 Text 可组合项中输出从后端服务器接收的照片数。

a59e55909b6e9213.png

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 代码库中浏览该代码。

运行起始代码

  1. 在 Android Studio 中打开下载的项目。该项目的文件夹名称为 basic-android-kotlin-compose-training-mars-photos
  2. Android 窗格中,展开 app > kotlin + java。请注意,该应用有一个名为 ui 的软件包文件夹。这是应用的界面层。

de3d8666ecee9d1c.png

  1. 运行应用。编译和运行应用时,您会看到以下界面,其中占位符文本居中显示。在此 Codelab 结束时,您将使用检索的照片数更新此占位文本。

95328ffbc9d7104b.png

起始代码演示

在此任务中,您将熟悉项目的结构。下面列出了项目中的重要文件和文件夹。

ui\MarsPhotosApp.kt

  • 此文件包含可组合项 MarsPhotosApp,它会在屏幕上显示内容,例如顶部应用栏和 HomeScreen 可组合项。上一步中的占位符文本会显示在此可组合项中。
  • 在下一个 Codelab 中,此可组合项将显示从火星照片后端服务器接收的数据。

screens\MarsViewModel.kt

  • 此文件是 MarsPhotosApp 的对应视图模型。
  • 此类包含一个名为 marsUiStateMutableState 属性。更新此属性的值会更新屏幕上显示的占位符文本。
  • getMarsPhotos() 方法会更新占位符响应。您稍后将在此 Codelab 中使用此方法显示从服务器提取的数据。此 Codelab 的目标是使用您从互联网获得的真实数据更新 ViewModel 中的 MutableState

screens\HomeScreen.kt

  • 此文件包含 HomeScreenResultScreen 可组合项。ResultScreen 具有一个简单的 Box 布局,它会在 Text 可组合项中显示 marsUiState 的值。

MainActivity.kt

  • 此 activity 的唯一任务就是加载 ViewModel 并显示 MarsPhotosApp 可组合项。

4. Web 服务简介

在此 Codelab 中,您将为网络服务创建一个层,与后端服务器进行通信并提取所需数据。您将使用名为 Retrofit 的第三方库来完成此任务。我们稍后会对此进行详细介绍。ViewModel 与该数据层进行通信,其余应用对此实现是透明的。

76551dbe9fc943aa.png

MarsViewModel 负责发出网络调用以获取火星照片数据。在 ViewModel 中,您使用 MutableState,以在数据发生更改时更新应用界面。

5. Web 服务和 Retrofit

火星照片数据存储在网络服务器中。为了将这些数据传输到您的应用中,需要与互联网上的服务器建立连接并进行通信。

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

如今,大多数 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 请求,然后服务器会返回对应用的响应(包括图片网址)。

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

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。我们将在后面的部分中详细了解此主题。

26043df178401c6a.png

添加 Retrofit 依赖项

Android Gradle 允许您将外部库添加到项目中。除了库依赖项之外,您还需要添加托管库的代码库。

  1. 打开模块级 Gradle 文件 build.gradle.kts (Module :app)
  2. 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。

  1. 点击 Sync Now,以使用新的依赖项重建项目。

6. 连接互联网

您将使用 Retrofit 库与 Mars Web 服务通信,并将原始 JSON 响应显示为 String。占位符 Text 将显示返回的 JSON 响应字符串或指明连接错误的消息。

Retrofit 会根据 Web 服务的内容为应用创建网络 API。它从 Web 服务提取数据,并通过独立的转换器库来路由数据。该库知道如何解码数据,并以 String 等对象形式返回这些数据。Retrofit 内置对 XML 和 JSON 等常用数据格式的支持。Retrofit 最终会创建一个代码来为您调用和使用此服务,包括关键详细信息(例如在后台线程上运行请求)。

8c3a5c3249570e57.png

在此任务中,您将向 Mars Photos 项目添加一个数据层,供 ViewModel 用来与 Web 服务通信。您将按照以下步骤实现 Retrofit 服务 API。

  • 创建一个数据源:MarsApiService 类。
  • 使用基准网址和转换器工厂创建 Retrofit 对象,以转换字符串。
  • 创建一个可说明 Retrofit 如何与网络服务器通信的接口。
  • 创建一个 Retrofit 服务,并向应用的其余 API 服务公开实例。

实现上述步骤:

  1. 右键点击 Android 项目窗格中的 com.example.marsphotos 软件包,然后依次选择 New > Package
  2. 在弹出式窗口中,将 network 附加到建议软件包名称的末尾。
  3. 在新软件包“network”下创建新的 Kotlin 文件。将该文件命名为 MarsApiService
  4. 打开 network/MarsApiService.kt
  5. 为 Web 服务的基础网址添加以下常量。
private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. 在该常量正下方添加 Retrofit 构建器,用于构建和创建 Retrofit 对象。
import retrofit2.Retrofit

private val retrofit = Retrofit.Builder()

Retrofit 需要 Web 服务的基础 URI 和转换器工厂来构建 Web 服务 API。转换器会告知 Retrofit 如何处理它从 Web 服务获取的数据。在这种情况下,您需要 Retrofit 从 Web 服务提取 JSON 响应,并将该响应作为 String 返回。Retrofit 包含一个 ScalarsConverter,它支持字符串和其他基元类型。

  1. 使用 ScalarsConverterFactory 实例对构建器调用 addConverterFactory()
import retrofit2.converter.scalars.ScalarsConverterFactory

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. 使用 baseUrl() 方法为 Web 服务添加基础网址。
  2. 调用 build() 以创建 Retrofit 对象。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. 在对 Retrofit 构建器的调用的下方,定义一个名为 MarsApiService 的接口,该接口定义 Retrofit 如何使用 HTTP 请求与网络服务器通信。
interface MarsApiService {
}
  1. MarsApiService 接口添加一个名为 getPhotos() 的函数,以从 Web 服务中获取响应字符串。
interface MarsApiService {
    fun getPhotos()
}
  1. 使用 @GET 注解告知 Retrofit 这是 GET 请求,并为该 Web 服务方法指定端点。在这种情况下,端点为 photos。如上个任务中所述,您将在此 Codelab 中使用 /photos 端点。
import retrofit2.http.GET

interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

调用 getPhotos() 方法时,Retrofit 会将端点 photos 附加到您用于启动请求的基准网址(由您在 Retrofit 构建器中定义)。

  1. 将函数的返回值类型添加到 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 服务实例,因此,您可以使用对象声明向应用的其余部分公开该服务。

  1. MarsApiService 接口声明外,定义一个名为 MarsApi 的公共对象,以初始化 Retrofit 服务。此对象是应用的其余部分可以访问的公开单例对象。
object MarsApi {}
  1. MarsApi 对象声明内,添加一个名为 retrofitService、类型为 MarsApiService 的延迟初始化的 Retrofit 对象属性。您可以进行这种延迟初始化,确保其在首次使用时进行初始化。忽略这个错误,您将在后续步骤中修复它。
object MarsApi {
    val retrofitService : MarsApiService by lazy {}
}
  1. 使用带有 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,因此,即使应用发生配置更改,请求也会继续发出。

  1. MarsApiService.kt 文件中,将 getPhotos() 设置为挂起函数,使其异步,并且不会阻塞发起调用的线程。您可以从 viewModelScope 内调用此函数。
@GET("photos")
suspend fun getPhotos(): String
  1. 打开 ui/screens/MarsViewModel.kt 文件。向下滚动到 getMarsPhotos() 方法。删除用于将状态响应设置为 "Set the Mars API Response here!" 的代码行,使 getMarsPhotos() 方法为空。
private fun getMarsPhotos() {}
  1. getMarsPhotos() 中,使用 viewModelScope.launch 启动协程。
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

private fun getMarsPhotos() {
    viewModelScope.launch {}
}
  1. viewModelScope 中,使用单例对象 MarsApiretrofitService 接口调用 getPhotos() 方法。将返回的响应保存在名为 listResultval 中。
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. 将刚刚从后端服务器收到的结果分配给 marsUiStatemarsUiState 是一个可变状态对象,表示最近的网络请求的状态。
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
  1. 运行应用。请注意,该应用会立即关闭,不一定会显示错误弹出窗口。应用发生了崩溃。
  2. 点击 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> 标签来声明它所需的权限。

  1. 打开 manifests/AndroidManifest.xml。将下面这行代码添加到 <application> 标签的前面:
<uses-permission android:name="android.permission.INTERNET" />
  1. 编译并再次运行应用。

如果互联网连接正常,您应该会看到包含火星照片相关数据的 JSON 文本。观察每条图片记录的 idimg_src 的重复规律。此 Codelab 稍后会详细介绍 JSON 格式。

b82ddb79eff61995.png

  1. 点按设备或模拟器中的返回按钮,关闭应用。

异常处理

代码中存在一个 bug。若要查看,请执行以下步骤:

  1. 将设备或模拟器设为飞行模式,以模拟网络连接错误。
  2. 从“最近”菜单中重新打开应用,或从 Android Studio 中重启应用。
  3. 点击 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 代码块,以从错误中恢复,而不是突然终止应用。

  1. getMarsPhotos() 中的 launch 代码块内,围绕 MarsApi 调用添加一个 try 代码块来处理异常。
  2. try 代码块之后添加一个 catch 代码块。
import java.io.IOException

viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}
  1. 再次运行该应用。请注意,应用这次不会崩溃。

添加状态界面

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 数据类添加一个构造函数参数。

对于 LoadingError 状态,您无需设置新数据和创建新对象;只需传递网络响应即可。将 data 类更改为 Object,以便为网络响应创建对象。

  1. 打开 ui/MarsViewModel.kt 文件。在 import 语句后,添加 MarsUiState 封装接口。添加后,MarsUiState 对象的值就会变得详尽。
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. MarsViewModel 类中,更新 marsUiState 定义。将类型更改为 MarsUiState,将 MarsUiState.Loading 作为其默认值。将 setter 设为不公开,以保护写入 marsUiState 的内容。
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  1. 向下滚动到 getMarsPhotos() 方法。将 marsUiState 值更新为 MarsUiState.Success,并传递 listResult
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
  1. catch 代码块内部,处理故障响应。将 MarsUiState 设为 Error
catch (e: IOException) {
   marsUiState = MarsUiState.Error
}
  1. 您可以从 try-catch 代码块中取出 marsUiState 分配。完成后的函数应如以下代码所示:
private fun getMarsPhotos() {
   viewModelScope.launch {
       marsUiState = try {
           val listResult = MarsApi.retrofitService.getPhotos()
           MarsUiState.Success(listResult)
       } catch (e: IOException) {
           MarsUiState.Error
       }
   }
}
  1. screens/HomeScreen.kt 文件中,对 marsUiState 添加一个 when 表达式。如果 marsUiStateMarsUiState.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()
        )
    }
}
  1. when 代码块内,为 MarsUiState.LoadingMarsUiState.Error 添加检查项。让该应用显示 LoadingScreenResultScreenErrorScreen 可组合项,稍后您会实现这些可组合项。
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())
    }
}
  1. 打开 res/drawable/loading_animation.xml。该可绘制对象是围绕中心点旋转图片可绘制对象 loading_img.xml 的动画。(您在预览中看不到这段动画。)

92a448fa23b6d1df.png

  1. 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)
    )
}
  1. 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))
    }
}
  1. 再次运行应用,保持飞行模式开启状态。应用这次不会突然关闭,而是会显示错误消息:

28ba37928e0a9334.png

  1. 在手机或模拟器上,关闭飞行模式。运行并测试您的应用,确保一切正常,并且您能够看到 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 对象都包含一组以英文逗号分隔的键值对。
  • 键值对中的键和值以英文冒号分隔。
  • 名称会用英文引号引起来。
  • 值可以是数字、字符串、布尔值、数组、对象(JSON 对象)或 null。

例如,img_src 是一个网址,而该网址是一个字符串。如果将该网址粘贴到网络浏览器中,您会看到一张火星表面图片。

b4f9f196c64f02c3.png

现在,在您的应用中,您将从 Mars Web 服务获得 JSON 响应,这是一个不错的开始。但显示图片实际上需要的是 Kotlin 对象,而不是大型 JSON 字符串。这个过程称为“反序列化”。

序列化是将应用所用的数据转换成可通过网络传输的格式的过程。与“序列化”相反,“反序列化”是从外部来源(如服务器)读取数据并将其转换为运行时对象的过程。这两者是大多数通过网络交换数据的应用的必备组件。

kotlinx.serialization 提供了一系列库,用于将 JSON 字符串转换为 Kotlin 对象。Kotlin 序列化转换器是一个社区开发的第三方库,可与 Retrofit 配合使用。

在此任务中,您将使用 kotlinx.serialization 库,将 Web 服务的 JSON 响应解析成表示火星照片的有用 Kotlin 对象。您需要更改应用,而不是显示原始 JSON,而该应用会显示返回的火星照片数。

添加 kotlinx.serialization 库依赖项

  1. 打开 build.gradle.kts (Module :app)
  2. plugins 代码块中,添加 kotlinx serialization 插件。
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
  1. dependencies 部分,添加以下代码以包含 kotlinx.serialization 依赖项。此依赖项可为 Kotlin 项目提供 JSON 序列化。
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
  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")
  1. 点击 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

  1. 右键点击 network 软件包,然后依次选择 New > Kotlin File/Class
  2. 在对话框中,选择 Class,然后输入 MarsPhoto 作为类的名称。系统将在 network 软件包中创建一个名为 MarsPhoto.kt 的新文件。
  3. 在类定义前添加 data 关键字,使 MarsPhoto 成为数据类。
  4. 将英文大括号 {} 更改为英文圆括号 ()。此更改会引发错误,因为数据类必须至少定义一个属性。
data class MarsPhoto()
  1. 将以下属性添加到 MarsPhoto 类定义中。
data class MarsPhoto(
    val id: String,  val img_src: String
)
  1. 使 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

  1. img_src 键所在的代码行替换为如下所示的代码行。
import kotlinx.serialization.SerialName

@SerialName(value = "img_src")
val imgSrc: String

更新 MarsApiService 和 MarsViewModel

在此任务中,您将使用 kotlinx.serialization 转换器将 JSON 对象转换为 Kotlin 对象。

  1. 打开 network/MarsApiService.kt
  2. 请注意 ScalarsConverterFactory 的未解析引用错误。这些错误是由上一部分中的 Retrofit 依赖项更改导致的。
  3. 删除 ScalarConverterFactory 的导入作业。您稍后将修复另一个错误。

移除

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 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 字符串。

  1. 更新 MarsApiService 接口,以便 Retrofit 返回 MarsPhoto 对象列表,而不是返回 String
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}
  1. viewModel 进行类似的更改。打开 MarsViewModel.kt,并向下滚动到 getMarsPhotos() 方法。

getMarsPhotos() 方法中,listResultList<MarsPhoto>,而不再是 String。该列表的大小就是已接收和解析的照片数。

  1. 如需输出检索的照片数,请按如下方式更新 marsUiState
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. 确保在设备或模拟器中关闭飞行模式。编译并运行应用。

这一次,消息应显示 Web 服务返回的资源数,而不是较大的 JSON 字符串:

a59e55909b6e9213.png

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 文档:

其他: