从互联网获取数据

1. 开始之前

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

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

先决条件

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

您将学到什么

您将做什么

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

您需要什么

  • 装有 Android Studio 的计算机
  • 火星照片应用的启动器代码

2. 应用概述

您将使用名为 火星照片 的应用,它显示火星表面的图像。该应用连接到网络服务以检索和显示火星照片。这些图像是从美国宇航局的火星漫游者拍摄的火星真实照片。下图是最终应用的屏幕截图,其中包含一个图像网格。

68f4ff12cc1e2d81.png

您在本 Codelab 中构建的应用版本不会有太多视觉效果。本 Codelab 侧重于应用的数据层部分,以连接到互联网并使用网络服务下载原始属性数据。为了确保应用正确检索和解析此数据,您可以在 Text 可组合项中打印从后端服务器接收的照片数量。

a59e55909b6e9213.png

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

您可以在 火星照片 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:

  • 该活动的唯一任务是加载 ViewModel 并显示 MarsPhotosApp 可组合项。

4. 网络服务的介绍

在本 Codelab 中,您将创建一个用于网络服务的层,该层与后端服务器通信并获取所需数据。您将使用一个名为 Retrofit 的第三方库来执行此任务。您将在后面了解有关它的更多信息。该 ViewModel 与数据层通信,应用的其余部分对该实现是透明的。

76551dbe9fc943aa.png

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

5. 网络服务和 Retrofit

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

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

如今,大多数网络服务器都使用一种称为 REST 的常用无状态网络架构来运行网络服务,REST 代表 REpresentational State Transfer。提供此架构的网络服务被称为 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。

Web 服务请求

每个网络服务请求都包含一个 URI,并使用与 Chrome 等 Web 浏览器使用的相同的 HTTP 协议传输到服务器。HTTP 请求包含一个操作,用于告知服务器要执行的操作。

常见的 HTTP 操作包括

  • GET 用于检索服务器数据。
  • POST 用于在服务器上创建新数据。
  • PUT 用于更新服务器上的现有数据。
  • DELETE 用于从服务器中删除数据。

您的应用将向服务器发出 HTTP GET 请求以获取火星照片信息,然后服务器将返回响应给您的应用,包括图像 URL。

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

网络服务的响应采用一种常见的数据格式进行格式化,例如 XML(可扩展标记语言)或 JSON(JavaScript 对象表示法)。JSON 格式以键值对的形式表示结构化数据。应用使用 JSON 与 REST API 通信,您将在后面的任务中了解有关 JSON 的更多信息。

在本任务中,您将建立与服务器的网络连接,与服务器通信并接收 JSON 响应。您将使用一个已经为您编写好的后端服务器。在本 Codelab 中,您将使用 Retrofit 库(一个第三方库)与后端服务器通信。

外部库

外部库或第三方库类似于核心 Android API 的扩展。您在本课程中使用的库是开源的,由社区开发,并通过来自世界各地庞大的 Android 社区成员的共同贡献来维护。这些资源帮助像您这样的 Android 开发人员构建更好的应用。

Retrofit 库

您在本 Codelab 中用来与 RESTful 火星网络服务通信的 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. 单击 **立即同步** 以使用新的依赖项重建项目。

6. 连接到互联网

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

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

8c3a5c3249570e57.png

在此任务中,您将数据层添加到 **火星照片** 项目中,您的 ViewModel 使用它与 Web 服务进行通信。您将使用以下步骤实现 Retrofit 服务 API

  • 创建一个数据源,MarsApiService 类。
  • 使用基本 URL 和转换器工厂创建一个 Retrofit 对象以转换字符串。
  • 创建一个接口来解释 Retrofit 如何与 Web 服务器进行通信。
  • 创建一个 Retrofit 服务并将该实例公开到 api 服务,供应用程序的其余部分使用。

执行上述步骤

  1. 在 Android 项目窗格中的包 com.example.marsphotos 上单击右键,然后选择 **新建 > 包**。
  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。如前一任务中所述,您将在本代码实验室中使用 /photos 端点。
import retrofit2.http.GET


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

当调用 getPhotos() 方法时,Retrofit 会将端点 photos 附加到基本 URL(您在 Retrofit 生成器中定义的 URL),以启动请求。

  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 对象声明中,添加一个类型为 MarsApiService 的名为 retrofitService 的延迟初始化 retrofit 对象属性。您进行此延迟初始化以确保在首次使用时对其进行初始化。忽略错误,您将在接下来的步骤中修复它。
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 时,调用方都会访问同一个单例 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 是一个可变状态对象,它表示最近 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 权限。下一任务将介绍如何将 Internet 权限添加到应用程序并解决此问题。

7. 添加 Internet 权限和异常处理

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 如何在每个图像记录中重复。您将在后面的代码实验室中详细了解 JSON 格式。

b82ddb79eff61995.png

  1. 点按设备或模拟器上的“后退”按钮以关闭应用程序。

异常处理

您的代码中有一个错误。执行以下步骤以查看它

  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

此错误消息表示应用程序尝试连接并超时。像这样的异常在实时中非常常见。与权限问题不同,此错误不是您可以修复的,但您可以处理它。在下一步中,您将学习如何处理此类异常。

异常

异常 是在运行时(而不是编译时)可能发生的错误,它们会突然终止应用程序,而不会通知用户。这会导致糟糕的用户体验。异常处理 是一种机制,通过它您可以防止应用程序突然终止并以用户友好的方式处理这种情况。

异常的原因可能很简单,例如除以零或网络连接错误。这些异常类似于以前的代码实验室讨论的 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 类中,最近的网页请求的状态 marsUiState 作为可变状态对象保存。但是,此类缺乏保存不同状态的能力:加载、成功和失败。

  • 加载 状态表示应用程序正在等待数据。
  • 成功 状态表示数据已成功从 Web 服务中检索。
  • 错误 状态表示任何网络或连接错误。

要表示应用程序中的这三种状态,您使用密封接口。一个 密封接口 通过限制可能的值来简化状态管理。在火星照片应用程序中,您将 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 文件。在导入语句之后,添加 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. 您可以将 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。此可绘制对象是围绕中心点旋转图像可绘制对象 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 服务器检索数据。当您在浏览器中输入此 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

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

序列化 是将应用程序使用的數據轉換為可透過網路傳輸的格式的過程。與序列化相反,反序列化是从外部来源(例如服务器)读取数据并将其转换为运行时对象的過程。它们都是大多数在网络上传输数据的应用程序的基本组成部分。

kotlinx.serialization 提供了一组库,这些库可以将 JSON 字符串转换为 Kotlin 对象。有一个社区开发的第三方库可以与 Retrofit 协同工作,Kotlin 序列化转换器

在此任务中,您使用 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. 单击 **立即同步** 以使用新的依赖项重建项目。

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

getMarsPhotos() 方法中,listResult 是一个 List<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 服务是使用标准 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 对象。
  • 有一个针对 Retrofit 的社区开发的 Kotlin 序列化转换器库:retrofit2-kotlinx-serialization-converter.
  • kotlinx.serialization 将 JSON 响应中的键与数据对象中具有相同名称的属性进行匹配。
  • 要为键使用不同的属性名,请使用 @SerialName 注解该属性以及 JSON 键 value

11. 了解详情

Android 开发者文档

Kotlin 文档

其他