从互联网获取数据

1. 开始之前

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

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

先决条件

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

你将学到什么

你将做什么

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

你需要什么

  • 一台装有 Android Studio 的电脑
  • 火星照片应用的启动代码

2. 应用概述

您将使用名为火星照片的应用,该应用显示火星表面的图像。此应用连接到网络服务以检索和显示火星照片。这些图像是来自 NASA 火星探测器的火星实况照片。下图是最终应用的屏幕截图,其中包含一个图像网格。

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

您可以在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:

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

4. 网络服务的介绍

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

76551dbe9fc943aa.png

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

5. 网络服务和 Retrofit

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

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

如今大多数网络服务器都使用称为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。

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

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

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

外部库

外部库,或第三方库,就像Android核心API的扩展一样。本课程中使用的库是开源的,由社区开发和维护,依靠来自全球庞大Android社区的集体贡献。这些资源帮助像您一样的Android开发者构建更好的应用。

Retrofit库

在本Codelab中,您使用Retrofit库与RESTful火星网络服务进行通信,这是一个良好支持和维护的库的良好示例。您可以通过查看其GitHub页面并查看已解决和未解决的问题(有些是功能请求)来判断这一点。如果开发者定期解决问题并响应功能请求,则该库可能维护良好,并且是应用中使用的良好候选者。您还可以参考Retrofit文档以了解更多关于该库的信息。

Retrofit库与REST后端进行通信。它生成代码,但您需要根据传递给它的参数提供网络服务的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库与火星网络服务通信,并将原始JSON响应作为String显示。占位符Text显示返回的JSON响应字符串或指示连接错误的消息。

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

8c3a5c3249570e57.png

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

  • 创建一个数据源,MarsApiService类。
  • 创建一个带有基本URL和转换工厂的Retrofit对象来转换字符串。
  • 创建一个接口来解释Retrofit如何与网络服务器通信。
  • 创建一个Retrofit服务,并将该实例公开给应用的其余部分。

实现上述步骤

  1. 右键单击Android项目窗格中的包com.example.marsphotos,然后选择新建 > 包
  2. 在弹出窗口中,将network添加到建议的包名称的末尾。
  3. 在新包下创建一个新的Kotlin文件。将其命名为MarsApiService
  4. 打开network/MarsApiService.kt
  5. 为网络服务添加以下基本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需要网络服务的基URI和一个转换工厂来构建网络服务API。转换器告诉Retrofit如何处理从网络服务返回的数据。在本例中,您希望Retrofit从网络服务获取JSON响应并将其作为String返回。Retrofit有一个ScalarsConverter支持字符串和其他基本类型。

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

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. 使用baseUrl()方法添加网络服务的基URL。
  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()的函数,以从网络服务获取响应字符串。
interface MarsApiService {    
    fun getPhotos()
}
  1. 使用@GET注解告诉Retrofit这是一个GET请求,并为该网络服务方法指定一个端点。在本例中,端点是photos。如前面的任务中所述,您将在本Codelab中使用/photos端点。
import retrofit2.http.GET


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

调用getPhotos()方法时,Retrofit会将端点photos附加到在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对象声明中,添加一个名为retrofitService的延迟初始化Retrofit对象属性,类型为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中调用网络服务

在此步骤中,您将实现getMarsPhotos()方法,该方法调用REST服务,然后处理返回的JSON字符串。

ViewModelScope

一个viewModelScope是为应用中的每个ViewModel定义的内置协程作用域。如果ViewModel被清除,则在此作用域中启动的任何协程都会自动取消。

您可以使用viewModelScope启动协程并在后台发出网络服务请求。由于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. 点击设备或模拟器上的**返回**按钮关闭应用。

异常处理

您的代码中存在错误。请执行以下步骤查看错误。

  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类中,最近一次网页请求的状态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数据类添加一个构造函数参数。

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. 您可以将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

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

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

kotlinx.serialization 提供了一套库,用于将 JSON 字符串转换为 Kotlin 对象。社区开发了一个与 Retrofit 兼容的第三方库,Kotlin Serialization Converter

在本任务中,您将使用 kotlinx.serialization 库,将网络服务的 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 数据类

您从网络服务获得的 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** 包,然后选择 **新建 > Kotlin 文件/类**。
  2. 在对话框中,选择 **类** 并输入 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. 确保设备或模拟器上的飞行模式已关闭。编译并运行应用程序。

这次,消息应显示从网络服务返回的属性数量,而不是大型 JSON 字符串

a59e55909b6e9213.png

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

其他