从互联网获取数据

1. 准备工作

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

在此 Codelab 中,您将使用开源和社区驱动的库来构建数据层并从后端服务器获取数据。这极大地简化了数据获取,还有助于应用遵循 Android 最佳实践,例如在后台线程上执行操作。如果互联网速度慢或不可用,您还将显示错误消息,以便用户了解任何网络连接问题。

前提条件

  • 了解如何创建 Composable 函数的基础知识。
  • 了解如何使用 Android 架构组件 ViewModel 的基础知识。
  • 了解如何使用协程执行耗时任务的基础知识。
  • 了解如何在 build.gradle.kts 中添加依赖项的基础知识。

您将学习到什么

您将做什么

  • 修改入门应用,使其发出 Web 服务 API 请求并处理响应。
  • 使用 Retrofit 库为您的应用实现数据层。
  • 使用 kotlinx.serialization 库将 Web 服务的 JSON 响应解析为应用的数据对象列表,并将其附加到 UI 状态。
  • 使用 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 的包文件夹。这是应用的 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 在数据更改时更新应用 UI。

5. Web 服务与 Retrofit

火星照片数据存储在 Web 服务器上。要将此数据获取到您的应用中,您需要建立连接并与互联网上的服务器通信。

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

现今大多数 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。

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

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。稍后您将在后续部分详细了解此主题。

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")

这两个库协同工作。第一个依赖项是 Retrofit2 库本身,第二个依赖项是 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 对象,其中包含基本 URL 和用于转换字符串的转换器工厂。
  • 创建一个接口,用于解释 Retrofit 如何与 Web 服务器通信。
  • 创建一个 Retrofit 服务,并将实例公开给应用的其余部分的 API 服务。

实现以上步骤

  1. 在 Android 项目面板中右键点击包 com.example.marsphotos,然后选择 New > Package
  2. 在弹出的窗口中,在建议的包名末尾附加 network
  3. 在新包下创建一个新的 Kotlin 文件。将其命名为 MarsApiService
  4. 打开 network/MarsApiService.kt
  5. 为 Web 服务的基本 URL 添加以下常量。
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 服务的基本 URL。
  2. 调用 build() 创建 Retrofit 对象。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. 在调用 Retrofit 构建器下方,定义一个名为 MarsApiService 的接口,该接口定义 Retrofit 如何使用 HTTP 请求与 Web 服务器通信。
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 会将您在 Retrofit 构建器中定义的基本 URL 附加到端点 photos,用于发起请求。

  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 服务的一个实例,因此您使用 object declaration 将服务公开给应用的其余部分。

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

  1. MarsApiService.kt 文件中,将 getPhotos() 设为 suspend 函数,使其异步执行且不阻塞调用线程。您将从 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 内部,使用单例对象 MarsApi 调用 retrofitService 接口中的 getPhotos() 方法。将返回的响应保存在一个名为 listResultval 中。
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. 将刚从后端服务器接收到的结果分配给 marsUiStatemarsUiState 是一个可变状态对象,表示最近 Web 请求的状态。
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 中使用的 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 块以从错误中恢复,而不是突然终止应用。

  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. 再次运行应用。请注意,这次应用不会崩溃了。

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

对于 LoadingError 状态,您不需要设置新数据并创建新对象;您只需传递 Web 响应。将 data 类更改为 Object,以创建 Web 响应的对象。

  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 设为 private 以保护对 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. 您可以将 marsUiState 赋值语句移出 try-catch 块。您完成的函数应如下所示
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。此 drawable 是一个动画,它将图像 drawable loading_img.xml 围绕中心点旋转。(您在预览中看不到动画。)

92a448fa23b6d1df.png

  1. 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)
    )
}
  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 服务器检索数据。当您在浏览器中输入此 URL 时,您会看到火星表面的 ID 和图像 URL 列表,采用 JSON 格式!

示例 JSON 响应的结构

showing keys values and JSON object

JSON 响应的结构具有以下特点

  • JSON 响应是一个数组,由方括号表示。该数组包含 JSON 对象。
  • JSON 对象由花括号包围。
  • 每个 JSON 对象包含一组由逗号分隔的键值对。
  • 冒号分隔键和值。
  • 名称用引号包围。
  • 值可以是数字、字符串、布尔值、数组、对象(JSON 对象)或 null。

例如,img_src 是一个 URL,它是一个字符串。当您将 URL 粘贴到 Web 浏览器中时,您会看到一张火星表面图像。

b4f9f196c64f02c3.png

现在,在您的应用中,您正在从 Mars Web 服务获取 JSON 响应,这是一个很好的开始。但要显示图像,您真正需要的是 Kotlin 对象,而不是一串很大的 JSON 字符串。这个过程称为 反序列化

序列化 是将应用程序使用的数据转换为可通过网络传输的格式的过程。与 序列化 相反,反序列化 是从外部源(如服务器)读取数据并将其转换为运行时对象的过程。它们都是大多数通过网络交换数据的应用程序的重要组成部分。

方法 kotlinx.serialization 提供了一系列库,可将 JSON 字符串转换为 Kotlin 对象。有一个社区开发的第三方库与 Retrofit 配合使用,即 Kotlin Serialization Converter

在本任务中,您将使用 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,使用新依赖项重新构建项目。

实现 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

  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. 通过使用 @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

  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 语句。您稍后会修复其他错误。

移除

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() 方法中,listResult 不再是 String,而是 List<MarsPhoto>。该列表的大小是已接收和解析的照片数量。

  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 服务使用标准 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 文档

其他