从互联网获取数据

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

在此 Codelab 中,您将使用开源开发的库构建网络层,并从后端服务器获取数据。这样可以简化数据获取,还有利于应用符合 Android 最佳做法,例如在后台线程上执行操作。如果互联网速度较慢或不可用,应用的界面也会更新;这样一来,用户即可及时获知网络连接问题。

您应当已掌握的内容

  • 如何创建和使用 fragment。
  • 如何使用 Android 架构组件 ViewModelLiveData
  • 如何在 Gradle 文件中添加依赖项。

学习内容

  • 什么是 REST 网络服务。
  • 使用 Retrofit 库连接到互联网上的 REST 网络服务,并获取响应。
  • 使用 Moshi 库将 JSON 响应解析为数据对象。

您应执行的操作

  • 修改起始应用以发出网络服务 API 请求,并处理响应。
  • 使用 Retrofit 库为您的应用实现网络层。
  • 使用 Moshi 库将网络服务的 JSON 响应解析为应用的 LiveData 对象。
  • 使用 Retrofit 对协程的支持,以简化代码。

所需条件

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

在此衔接课程中,您将使用名为 MarsPhotos 的起始应用。该应用会显示火星表面的图片,需要连接到网络服务,才能检索并显示火星照片。这些图片是由 NASA 的火星探测器拍摄的真实照片。以下是最终应用的屏幕截图,其中包含使用 RecyclerView 构建的缩略图属性图片网格。

6c26142b52c51285.png

您在此 Codelab 中构建的应用版本不会有很多视觉闪存:它侧重于应用的网络层部分,以连接到互联网并使用网络服务下载原始属性数据。为了确保正确检索和解析数据,需要在文本视图中输出从后端服务器收到的照片数:

e98a0641540fcb73.png

下载起始代码

此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码可能既包含您在之前的 Codelab 中已熟悉的代码,也包含您不熟悉的代码。这些您尚不熟悉的代码将在后续 Codelab 中进行更详细的介绍。

如果您使用 GitHub 中的起始代码,请注意文件夹名称为 android-basics-kotlin-mars-photos-app。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
  2. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg 以构建并运行应用。请确保该应用可以正常使用。
  5. Project 工具窗口中浏览项目文件,了解应用的实现方式。

运行起始代码

  1. 在 Android Studio 中打开下载的项目。该项目的文件夹名称为 android-basics-kotlin-mars-photos-app。起始代码文件夹结构应如下所示。
  2. Android 窗格中,展开 app > java。请注意,该应用有一个名为 overview 的软件包文件夹。这是应用的界面层。

39027f91862361f1.png

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

4886b6b36023a53f.png

  1. 浏览文件以了解起始代码。对于布局文件,您可以使用右上角的 Split 选项同时查看布局和 XML。

起始代码演示

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

OverviewFragment:

  • 这是 MainActivity 中显示的 fragment。您在上一步中看到的占位符文本会显示在此 fragment 中。
  • 在下一个 Codelab 中,此 fragement 将显示从火星照片后端服务器接收的数据。
  • 此类保留对 OverviewViewModel 对象的引用。
  • OverviewFragment 有一个 onCreateView() 函数,该函数使用数据绑定扩充 fragment_overview 布局,将绑定生命周期所有者设置为自身,并为其设置绑定对象中的 viewModel 变量。
  • 由于分配了生命周期所有者,因此系统会自动观察数据绑定中使用的任何 LiveData 是否有任何更改,并相应地更新界面。

OverviewViewModel:

  • 这是 OverviewFragment 的对应视图模型。
  • 此类包含一个名为 _statusMutableLiveData 属性及其备份属性。更新此属性的值会更新屏幕上显示的占位符文本。
  • getMarsPhotos() 方法会更新占位符响应。在此 Codelab 中,您将使用此方法显示从服务器获取的数据。此 Codelab 的目标是使用您从互联网获得的真实数据更新 ViewModel 中的 status LiveData

res/layout/fragment_overview.xml

  • 此布局设置为使用数据绑定,由单个 TextView 组成。
  • 它声明一个 OverviewViewModel 变量,然后将 ViewModel 中的 status 绑定到 TextView

MainActivity.kt:唯一适合此 activity 的任务是加载 activity 的布局 activity_main

layout/activity_main.xml::这是一个主要 activity 布局,其中有一个 FragmentContainerView 指向 fragment_overview,系统会在应用启动时实例化概览 fragment。

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

d5a05ab8fd5ff011.png

OverviewViewModel 负责发出网络调用以获取火星照片数据。在 ViewModel 中,结合使用 LiveData 和生命周期感知型数据绑定,以在数据发生更改时更新应用界面。

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

b3ab0ee52bfd791e.png

4a23a1ba3307b2a5.png

如今,大多数网络服务器都使用一种名为 REST(表示移)的常见无状态网络架构运行网络服务。提供此架构的网络服务称为 RESTful 服务。

通过 URI 以标准化方式向 RESTful 网络服务发出请求。URI(统一资源标识符)可按名称识别服务器中的资源,而不会暗示其位置或访问方式。例如,在本节课的应用中,您将使用下面的服务器 URI 检索图片网址(此服务器同时托管火星地产和火星照片):

android-kotlin-fun-mars-server.appspot.com

统一资源定位符(网址)是一个用于指定对资源进行操作或获取资源表示的方式,即指定其主要访问机制和网络位置的网址。

例如:

以下网址将获取 Mars 上所有可用的地产属性列表!

https://android-kotlin-fun-mars-server.appspot.com/realestate

以下网址将获取火星照片列表:

https://android-kotlin-fun-mars-server.appspot.com/photos

这些网址指的是可通过超文本传输协议 (http:) 从网络获取的标识资源,如 /realestate/photos您将在此 Codelab 中使用 /photos 端点。

网络服务请求

每个网络服务请求均包含 URI,并使用网络浏览器(例如 Chrome)所使用的 HTTP 协议传输至服务器。HTTP 请求包含一项用于告知服务器该执行什么操作的操作。

常见的 HTTP 操作包括:

  • GET,用于检索服务器数据
  • POST 或 PUT,用于使用新数据添加/创建/更新服务器
  • DELETE,用于从服务器中删除数据

您的应用将向服务器发出 HTTP GET 请求以获取火星照片信息,然后服务器会返回对应用的响应(包括图片网址)。

9fb57e255df97a4d.png

6da405d572445df9.png

网络服务的响应通常采用 XML 或 JSON 等常见网络格式(表示键值对中结构化数据的格式)进行格式化。您可以在后续任务中详细了解 JSON

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

外部库

外部库或第三方库就像对核心 Android API 的扩展。这些库大多是社区开发的开源库,并通过世界各地的大型 Android 社区共同贡献进行维护。这样,像您这样的 Android 开发者就可以构建更好的应用。

Retrofit 库

您将在此 Codelab 中用来与 RESTful Mars 网络服务通信的 Retrofit 库就是得到良好支持和维护的库的一个很好的例子。您可以查看其 GitHub 页面,查看尚未解决的问题(其中一些是功能请求)以及已解决的问题,从而验证这一点。如果开发者解决问题并定期响应功能请求,则意味着该库得到良好维护,并且非常适合在应用中使用。他们还有一个 Retrofit 文档页面。

Retrofit 库将与后端进行通信。它会根据我们传递给它的参数为网络服务创建 URI。后面几节将对此进行详细介绍。

c9e1034e86327abd.png

添加 Retrofit 依赖项

Android Gradle 允许您将外部库添加到项目中。除了库依赖项之外,您还应添加托管库的代码库。Jetpack 库中的 Google 库(如 ViewModelLiveData)托管在 Google 代码库中。大多数社区库托管在 Japitize 等 JCenter 上。

  1. 打开项目的顶级 build.gradle(Project: MarsPhotos) 文件。请注意 repositories 代码块下列出的代码库。您应该会看到两个代码库,即 google()jcenter()
repositories {
   google()
   jcenter()
}
  1. 打开模块级 Gradle 文件 build.gradle (Module: MarsPhots.app)
  2. dependencies 部分中,为 Retrofit 库添加以下代码行:
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

第一个依赖项是 Retrofit2 库本身,而第二个依赖项则用于 Retrofit 标量转换器。此转换器允许 Retrofit 将 JSON 结果作为 String 返回。这两个库协同工作。

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

添加对 Java 8 语言功能的支持

许多第三方库(包括 Retrofit2)都会使用 Java 8 语言功能。Android Gradle 插件提供对使用某些 Java 8 语言功能的内置支持。

  1. 要使用内置功能,需要在模块的 build.gradle 文件中配置以下代码。我们已代您完成此步骤,请确保您的 build.gradle(Module: MarsPhotos.app). 中存在以下代码。
android {
  ...

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = '1.8'
  }
}

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

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

64fe0b5717a31f71.png

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

  • 创建一个 MarsApiService 类网络层。
  • 使用基础网址和转换器工厂创建 Retrofit 对象。
  • 创建一个可说明 Retrofit 如何与我们的网络服务器通信的界面。
  • 创建一个 Retrofit 服务,并向应用的其余 API 服务公开实例。

实现上述步骤:

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

在出现提示时,导入 retrofit2.Retrofit

  1. Retrofit 需要网络服务的基础 URI 和转换器工厂来构建网络服务 API。转换器会告知 Retrofit 如何处理它从网络服务获取的返回数据。在这种情况下,您希望 Retrofit 从网络服务获取 JSON 响应,并将该响应作为 String 返回。Retrofit 包含一个 ScalarsConverter,它支持字符串和其他基元类型,因此,您可以使用 ScalarsConverterFactory 实例对构建器调用 addConverterFactory()
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())

出现提示时,导入 retrofit2.converter.scalars.ScalarsConverterFactory

  1. 使用 baseUrl() 方法添加网络服务的基础 URI。最后,调用 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 端点。
interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

在收到请求时,导入 retrofit2.http.GET

  1. 调用 getPhotos() 方法时,Retrofit 会将端点 photos 附加到您用于启动请求的基础网址(由您在 Retrofit 构建器中定义)。将函数的返回类型添加到 String
interface MarsApiService {
    @GET("photos")
    fun getPhotos(): String
}

对象声明

在 kotlin 中,对象声明用于声明单例对象。单例模式可确保对于一个对象只创建一个实例,并提供一个对该对象的全局访问点。对象声明的初始化是线程安全操作,在首次访问时完成。

Kotlin 可让您轻松声明单例。以下是对象声明及其访问的示例。对象声明的名称后面跟有 object 关键字。

示例:

// Object declaration
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }
​
    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)

对 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 对象。

在 OverviewViewModel 中调用网络服务

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

ViewModelScope

ViewModelScope 是为应用中的每个 ViewModel 定义的内置协程作用域。在此作用域内启动的协程会在 ViewModel 被清除时自动取消。

您将使用 ViewModelScope 启动协程,并在后台发出 Retrofit 网络调用。

  1. MarsApiService 中,将 getPhotos() 设置为挂起函数。这样,您就可以从协程内调用此方法。
@GET("photos")
suspend fun getPhotos(): String
  1. 打开 overview/OverviewViewModel。向下滚动到 getMarsPhotos() 方法。删除用于将状态响应设置为 "Set the Mars API Response here!". 的代码行。方法 getMarsPhotos() 现在应该为空。
private fun getMarsPhotos() {

}
  1. getMarsPhotos() 中,使用 viewModelScope.launch 启动协程。
private fun getMarsPhotos() {
    viewModelScope.launch {
    }
}

出现提示时,导入 androidx.lifecycle.viewModelScopekotlinx.coroutines.launch

  1. viewModelScope 中,使用单例对象 MarsApiretrofitService 界面调用 getPhotos() 方法。将返回的响应保存在名为 listResultval 中。
viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}

出现提示时,导入 com.example.android.marsphotos.network.MarsApi

  1. 将我们刚刚从后端服务器收到的结果分配给 _status.value.
 val listResult = MarsApi.retrofitService.getProperties()
 _status.value = 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 权限。在下一个任务中,您可以通过向应用添加互联网权限来解决此问题。

Android 权限

Android 上的权限旨在保护 Android 用户的隐私。Android 应用必须声明或请求访问敏感用户数据(如联系人、通话记录和某些系统功能,如相机或互联网)的权限。

应用需要 INTERNET 权限才能访问互联网。连接到互联网会引起安全问题,因此我们默认应用没有连接互联网。您需要明确声明该应用需要访问互联网。系统会将此视为正常权限。如需详细了解 Android 权限及其类型,请参阅文档

在此步骤中,您的应用通过在 AndroidManifest 文件中添加 <uses-permission> 标签来声明它所需的权限。

  1. 打开 manifests/AndroidManifest.xml。将这行代码添加到 <application> 标签的前面:
<uses-permission android:name="android.permission.INTERNET" />
  1. 编译并再次运行应用。如果您有有效的互联网连接,应该会看到包含火星照片相关数据的 JSON 文本。您可以稍后在 Codelab 中详细了解 JSON 格式。

f7ba3feaf864d4cf.png

  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
    java.net.SocketTimeoutException: timeout
...

此错误消息表示应用尝试连接并超时。诸如此类的异常非常常见。在下一步中,您将了解如何处理此类异常。

异常处理

异常是指可能在运行时(并非编译时)出现并最终导致应用突然终止的错误,而不会通知用户。这可能会导致糟糕的用户体验。异常处理是一种机制,您可以利用该机制防止应用突然终止,并以人性化方式进行处理。

异常原因可能非常简单,比如除以零就可能抛出异常,或者是网络错误。这些异常与您在上一个 Codelab 中了解的 NumberFormatException 类似。

连接服务器时可能出现的问题示例:

  • 在 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. 打开 overview/OverviewViewModel.kt。向下滚动到 getMarsPhotos() 方法。在启动代码块内,围绕 MarsApi 调用添加一个 try 代码块来处理异常。在 try 代码块后添加 catch 代码块:
viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       _status.value = listResult
   } catch (e: Exception) {

   }
}
  1. catch {} 代码块内部,处理故障响应。通过将 e.message 设置为 _status.value,向用户显示错误消息。
catch (e: Exception) {
   _status.value = "Failure: ${e.message}"
}
  1. 再次运行应用,保持飞行模式开启状态。这次应用不会突然关闭,而是会显示错误消息。

2fbc318b4fff2f34.png

  1. 在手机或模拟器上,关闭飞行模式。运行并测试您的应用,确保一切正常,并且您能够看到 JSON 字符串。

JSON

请求的数据通常采用 XML 或 JSON 等常见数据格式之一进行格式化。每次调用都会返回结构化数据,并且您的应用需要知道该结构是什么,才能从响应中读取数据。

例如,在该应用中,您将从以下服务器检索数据:https:// android-kotlin-fun-mars-server.appspot.com/photos。如果在浏览器中输入此网址,

您会看到以 JSON 格式显示的火星表面的 ID 和图片网址列表!

示例 JSON 响应的结构:

68fdfa54410ee03e.png

  • JSON 响应是一个数组(以方括号表示)。该数组包含 JSON 对象。
  • JSON 对象括在花括号中。
  • 每个 JSON 对象均包含一组名称-值对。名称和值之间用英文冒号分隔。
  • 名称括在引号中。
  • 值可以是数字、字符串、布尔值、数组、对象(JSON 对象)或 null。

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

17116bdeb21fec0d.png

现在,您将从 Mars 网络服务获得 JSON 响应,这是一个不错的开始。但您实际上需要的是 Kotlin 对象,而不是大型 JSON 字符串。有一个名为 Moshi 的外部库,它是一个 Android JSON 解析器,可将 JSON 字符串转换为 Kotlin 对象。Retrofit 库具有可与 Moshi 配合使用的转换器,在这方面非常适用。

在此任务中,您将配合使用 Moshif 库与 Retrofit,将 JSON 响应从网络服务解析成表示 Kotlin 照片的有用 Kotlin 对象。您将更改应用,而不是显示原始 JSON,而应用会显示返回的火星照片数。

添加 Moshi 库依赖项

  1. 打开 build.gradle (Module: app)
  2. 在依赖项部分中,添加下面所示的代码以包含 Moshi 依赖项。此依赖项添加了对包含 Kotlin 支持的 Moshi JSON 库的支持。
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
  1. dependencies 代码块中,找到 Retrofit 标量转换器的代码行,并将这些依赖项更改为使用 converter-moshi

将下面的代码

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

替换为此代码

// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
  1. 点击 Sync Now,以使用新的依赖项重建项目。

实现火星照片数据类

您从网络服务中获取的 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:图片的网址。

Moshi 会解析此 JSON 数据并将其转换为 Kotlin 对象。为此,Moshi 需要一个 Kotlin 数据类来存储解析后的结果,因此在此步骤中,您将创建数据类 MarsPhoto

  1. 右键点击 network 软件包,然后依次选择 New > Kotlin File/Class
  2. 在弹出式窗口中,选择 Class,然后输入 MarsPhoto 作为类的名称。系统将在 network 软件包中创建一个名为 MarsPhoto.kt 的新文件。
  3. 在类定义前添加 data 关键字,使 MarsPhoto 成为数据类。将 {} 括号更改为 () 圆括号。这会留下错误,因为数据类必须至少定义一个属性。
data class MarsPhoto(
)
  1. 将以下属性添加到 MarsPhoto 类定义中。
data class MarsPhoto(
   val id: String, val img_src: String
)

请注意,MarsPhoto 类中的每个变量都对应于 JSON 对象中的一个键名。为了匹配特定 JSON 响应中的类型,请为所有值使用 String 对象。

Moshi 解析 JSON 时,它会按名称匹配键,并用适当的值填充数据对象。

@Json 注解

有时,JSON 响应中的键名可能会使 Kotlin 属性混淆,或者可能与建议的编码样式不匹配。例如,在 JSON 文件中,img_src 键使用下划线,而 Kotlin 惯例使用大写和小写字母(“驼峰式大小写”)。

如需在数据类中使用与 JSON 响应中的键名不同的变量名称,请使用 @Json 注解。在此示例中,数据类中变量的名称为 imgSrcUrl。可以使用 @Json(name = "img_src") 将变量映射到 JSON 属性 img_src

  1. img_src 键的代码行替换为如下所示的代码行。在收到请求时,导入 com.squareup.moshi.Json
@Json(name = "img_src") val imgSrcUrl: String

更新 MarsApiService 和 OverviewViewModel

在此任务中,您将使用 Moshi 构建器创建 Moshi 对象,该构建器类似于 Retrofit 构建器。

ScalarsConverterFactory 替换为 KotlinJsonAdapterFactory,以告知 Retrofit 其可以使用 Moshi 将 JSON 响应转换为 Kotlin 对象。然后,您将更新网络 API 和 ViewModel,以使用 Moshi 对象。

  1. 打开 network/MarsApiService.kt。请注意 ScalarsConverterFactory 的未解析引用错误。这是因为您在上一步中做出的 Retrofit 依赖项更改。删除 ScalarConverterFactory 的导入作业。您很快将解决其他错误。

移除:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 在文件顶部,在 Retrofit 构建器的前面添加以下代码以创建 Moshi 对象,该对象与 Retrofit 对象类似。
private val moshi = Moshi.Builder()

在收到请求时,导入 com.squareup.moshi.Moshicom.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

  1. 为了让 Moshi 注解能够在 Kotlin 中正常使用,请在 Moshi 构建器中添加 KotlinJsonAdapterFactory,然后调用 build()
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()
  1. retrofit 对象声明中,将 Retrofit 构建器更改为使用 MoshiConverterFactory 而不是 ScalarConverterFactory,并传入您刚刚创建的 moshi 实例。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

在收到请求时,导入 retrofit2.converter.moshi.MoshiConverterFactory

  1. 现在,您已具备 MoshiConverterFactory,可以要求 Retrofit 从 JSON 数组中返回 MarsPhoto 对象列表,而不是返回 JSON 字符串。更新 MarsApiService 界面,让 Retrofit 返回 MarsPhoto 对象列表,而不是返回 String
interface MarsApiService {
   @GET("photo")
   fun getPhotos(): List<MarsPhoto>
}
  1. viewModel 进行类似的更改,打开 OverviewViewModel.kt。向下滚动到 getMarsPhotos() 方法。
  2. getMarsPhotos() 方法中,listResultList<MarsPhoto>,而不再是 String。该列表的大小就是已接收和解析的照片数。如需输出检索的照片数,请按如下方式更新 _status.value
_status.value = "Success: ${listResult.size} Mars photos retrieved"

出现提示时,导入 com.example.android.marsphotos.network.MarsPhoto

  1. 确保在设备或模拟器中关闭飞行模式。编译并运行应用。此时,消息应显示从网络服务返回的属性数,而不是很长的 JSON 字符串:

7da53c64bd36fe74.png

build.gradle(Module : MarsPhotos.app)

下面列出的是要包含的新依赖项。

dependencies {
    ...
    // Moshi
    implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'

    // Retrofit with Moshi Converter
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    ...
}

Manifests/AndroidManifest.xml

添加以下代码段中的互联网权限 <uses-permission..> 代码。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.marsphotos">

    <!-- In order for our app to access the Internet, we need to define this permission. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
    </application>

</manifest>

network/MarsPhoto.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Json

/**
* This data class defines a Mars photo which includes an ID, and the image URL.
* The property names of this data class are used by Moshi to match the names of values in JSON.
*/
data class MarsPhoto(
   val id: String,
   @Json(name = "img_src") val imgSrcUrl: String
)

network/MarsApiService.kt

package com.example.android.marsphotos.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET

private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"

/**
* Build the Moshi object with Kotlin adapter factory that Retrofit will be using.
*/
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

/**
* The Retrofit object with the Moshi converter.
*/
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()

/**
* A public interface that exposes the [getPhotos] method
*/
interface MarsApiService {
   /**
    * Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
    * The @GET annotation indicates that the "photos" endpoint will be requested with the GET
    * HTTP method
    */
   @GET("photos")
   suspend fun getPhotos() : List<MarsPhoto>
}

/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object MarsApi {
   val retrofitService: MarsApiService by lazy { retrofit.create(MarsApiService::class.java) }
}

Overview/OverviewViewModel.kt

package com.example.android.marsphotos.overview

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch

/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {

   // The internal MutableLiveData that stores the status of the most recent request
   private val _status = MutableLiveData<String>()

   // The external immutable LiveData for the request status
   val status: LiveData<String> = _status
   /**
    * Call getMarsPhotos() on init so we can display status immediately.
    */
   init {
       getMarsPhotos()
   }

   /**
    * Gets Mars photos information from the Mars API Retrofit service and updates the
    * [MarsPhoto] [List] [LiveData].
    */
   private fun getMarsPhotos() {
       viewModelScope.launch {
           try {
               val listResult = MarsApi.retrofitService.getPhotos()
               _status.value = "Success: ${listResult.size} Mars photos retrieved"
           } catch (e: Exception) {
               _status.value = "Failure: ${e.message}"
           }
       }
   }
}

REST 网络服务

  • 网络服务是通过互联网提供的基于软件的功能,可让您的应用发出请求并获取返回的数据。
  • 常见网络服务使用的是 REST 架构。提供 REST 架构的网络服务称为 RESTful 服务。RESTful 网络服务是使用标准网络组件和协议构建的。
  • 您可通过 URI 以标准化方式向 REST 网络服务发出请求。
  • 要使用网络服务,应用必须建立网络连接,然后与该服务进行通信。然后,应用必须接收响应数据,并将该数据解析成应用可以使用的格式。
  • Retrofit 库是一个客户端库,可让应用向 REST 网络服务发出请求。
  • 使用转换器指示 Retrofit 如何处理它发送至网络服务的数据,以及它从网络服务获取的返回数据。例如,ScalarsConverter 转换器会将网络服务数据视为 String 或其他基元。
  • 如需让应用能够连接到互联网,请在 Android 清单中添加 "android.permission.INTERNET" 权限。

JSON 解析

  • 网络服务的响应通常会采用 JSON 格式(一种表示结构化数据的通用格式)进行格式化。
  • JSON 对象是键值对的集合。
  • JSON 对象集合是一个 JSON 数组。作为网络服务的响应,您会得到一个 JSON 数组。
  • 键值对中的值用引号括起来。值可以是数字或字符串。
  • Moshi 库是一种 Android JSON 解析器,可将 JSON 字符串转换为 Kotlin 对象。Retrofit 包含一个可配合 Moshi 使用的转换器。
  • Moshi 可将 JSON 响应中的键与具有相同名称的数据对象中的属性进行匹配。
  • 如需为键使用不同的属性名称,请使用 @Json 注解和 JSON 键名为该属性添加注解。

Android 开发者文档:

Kotlin 文档:

其他: