Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(34)-從Internet取得Data

使用第三方 library Retrofit 將 app 連線至後端 server,並瞭解 REST web service。

使用以 open source 開發的 libraries 建構網路層(network layer),並從後端 server 取得 data。這樣可大幅簡化資料擷取作業,還可讓 app 符合 Android 最佳做法,例如在背景執行緒(background thread)上執行作業。若網際網路(internet)連線速度緩慢或無法使用,您也可更新 app 的 UI,以讓使用者隨時掌握任何網路連線問題。

學習目標

  • 什麼是 REST web service。
  • 使用 Retrofit library 連線至 internet 上的 REST web service 並取得 response(回應)。
  • 使用 Moshi library 將 JSON response 解析(parse)成 data object

建構項目

  • 修改 starter app,以發出 web service API要求(request)處理回應(response)
  • 使用 Retrofit library 為 app 實作網路層(network layer)
  • 使用 Moshi library,將 web service 中的 JSON response 解析(parse)成 app 的 LiveData objects
  • 使用 Retrofit 提供的 coroutines(協程)來簡化程式碼。

App 總覽

在本課程中,您將使用名為 MarsPhotos 的範例 app,顯示火星表面的圖片。此 app 會連線至 web service,以擷取和顯示火星的相片。這些圖片是自 NASA 火星漫遊者擷取的火星實景相片。以下是最後一個 app 的螢幕截圖,其中包含以 RecyclerView 建構的縮圖屬性圖片。

注意:以上螢幕截圖是您在下個程式碼研究室結束時,於課程結束時所建構最終 app 的螢幕截圖。本程式碼研究室中顯示的螢幕截圖,可協助您更加瞭解整體的app功能。

您在本程式碼研究室中建構的 app 版本不會採用大量視覺閃光特效:其著重於 app 的網路層部分,以連線至 internet 並透過 web service 下載原始屬性資料(raw property data)。為確保系統正確擷取和剖析資料,請直接在 text view 輸出從後端 server 接收的相片數量


探索 MarsPhotos starter app

下載範例程式碼

如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-mars-photos-app。在 Android Studio 中開啟專案時,請選取這個資料夾。

當您編譯和執行 app 時,應會在下列畫面畫面中央看見預留位置文字。完成本程式碼研究室後,您會將此預留位置文字更新為已擷取的相片數量。

範例程式碼逐步操作說明

在此工作中,您將會熟悉專案的結構。以下是關於專案中重要檔案與資料夾的逐步操作說明。

  1. OverviewFragment:
  • 此為 MainActivity 中顯示的 fragment。您在上個步驟中看到的 placeholder text 會顯示於此 fragment。
  • 在下一個程式碼研究室中,此 fragment 會顯示自 Mars photos 後端 server 接收的 data
  • 此類別會保留 OverviewViewModel 物件的引用。
  • OverviewFragmentonCreateView() 函式會使用 Data Binding 來加載 fragment_overview layout,將 binding lifecycle owner 設定為自己,並在其 binding object 中設定 viewModel 變數
  • 指派 lifecycle owner 後,系統會自動觀察 Data Binding 中使用的任何 LiveData 是否有任何變更,並據以更新 UI
  1. OverviewViewModel:
  • 此為 OverviewFragment 的對應 view model
  • 此類別包含名為 _statusMutableLiveData 屬性及其 backing 屬性。更新此屬性的值時,會一併更新畫面上顯示的 placeholder text
  • getMarsPhotos() 方法會更新 placeholder response。稍後在程式碼研究室中,您將使用此程式碼來顯示從 server 擷取(fetched)的 data。本程式碼研究室的目標,在於使用從 internet 取得的 real data 來更新 ViewModel 中的 status LiveData
  1. res/layout/fragment_overview.xml
  • 此 layout 已設為使用 data binding,且由單一 TextView 組成。
  • 其會宣告 OverviewViewModel 變數,然後statusViewModel binds 至 TextView
  1. MainActivity.kt
    此 activity 的唯一工作是載入(load)該 activity 的 layout activity_main

  2. layout/activity_main.xml:
    main activity layout 具有指向 fragment_overview 的單一 FragmentContainerView,overview fragment 會在 app 啟動(launched)時執行個體化(instantiated)。

App Overview

在本程式碼研究室中,您會建立網路服務層來與後端 server 進行溝通,以及擷取(fetch)必要的 data。您將使用名為 Retrofit 的第三方 library 實作此步驟。您將在稍後進一步瞭解相關資訊。ViewModel 會直接與該網路層進行通訊,app 的其餘部分則會對此實作公開。

OverviewViewModel 負責執行 network call 以取得 Mars photos data。在 ViewModel 中,您會使用 LiveData 搭配生命週期感知(lifecycle-aware) data binding,在 data 變更時更新 app UI。


Web services 與 Retrofit

Mars photos data 會儲存在 web server 中。如要讓 app 取得此 data,您必須建立連線(establish a connection)並與 internet 上的 server 通訊(communicate)。

現今的大部分 web servers 會使用稱為 REST 的一般無狀態網路架構(stateless web architecture),其中 RE「Representational」(表示法) 的縮寫,S「State」(狀態) 的縮寫,T「Transfer」(傳輸) 的縮寫。提供此架構的 web services,稱為 RESTful services

系統會透過 URI 以規範的方式向 RESTful web services 發出請求(requests)URI (Uniform Resource Identifier/統一資源標識符)依 name 來識別 server 中的資源(resource),而不會暗示其位置(location)或存取方式(access)。舉例來說,在本課程的 app 中,您將使用下列 server URI 來擷取(retrieve) image urls (此 server 會代管(hosts) Mars real-estate 和 Mars photos):
android-kotlin-fun-mars-server.appspot.com

URL (Uniform Resource Locator/統一資源定位符) 是一種 URI,其會指定運作或取得資源(resource)表示法的方式,亦即同時指定的主要存取機制(access mechanism)網路位置(network location)

URI vs URL
假設我要設計一個API

  • URI 為:
    /auth/sound/:name
  • URL 為:
    music.com/auth/sound/bird
    (URL 可讓其他人來使用)

在以上的例子中,/auth/sound/:name 是一個 URI,它用於識別和定位一個資源,其中 :name 是一個路由參數,可以在實際使用時被具體的值替換。

而提供給他人使用的 music.com/auth/sound/bird 是一個 URL,它是一個具體的資源位址,包含了協定 (http 或 https)、主機名稱 (music.com)、路徑 (/auth/sound/bird) 等元素,用於定位並存取資源

因此,URI 是用於識別和定位資源的通用概念,而 URL 是 URI 的一種具體實現方式,用於指定資源的位址和定位方式,則 /auth/sound/:name 是 URI,music.com/auth/sound/bird 是其對應的 URL。

點此了解更多

例如:
下列 URL 會列出 Mars 上所有可用的 real estate list!
https://android-kotlin-fun-mars-server.appspot.com/realestate

下列 URL 會取得 Mars photos 的 list:
https://android-kotlin-fun-mars-server.appspot.com/photos

這些 URLs 是指識別的資源(resource identified),例如 /realestate/photos,您可透過 Hypertext Transfer Protocol (http:) 從網路(network)中取得。您將在本程式碼研究室中使用 /photos 端點(endpoint)。

注意:熟悉的 web URL 實際上屬於 URI 類型。在本課程中會取決於呼叫 API 的方式,交替使用 URL 和 URI。

Web service request

每個 web service request 都包含一個 URI,並透過 Chrome 等網路瀏覽器使用的 HTTP 通訊協定傳輸至 server。HTTP 要求包含指示 server 處置方式的作業

常見的 HTTP 作業包括:

  • GET 用於抓取 server data
  • POSTPUT 用於 add/create/update server 的 new data
  • DELETE 用於刪除 server 中的 data

App 會向 server 傳送包含 Mars photos 資訊(information)HTTP GET request,接著 server 會對 app 傳回 response,包括 image urls

Web service 的 response 通常會採用 XMLJSON 這種常見的網路格式進行格式化 (在 key-value 組合中代表結構化資料(structured data) 格式)。我們將在後續工作中進一步瞭解 JSON。

在這項工作中,您要建立與 server 的連線(network connection)與 server 通訊(communicate),以及接收 JSON response。您將使用已寫入的後端 server。在本程式碼研究室中,您將使用第三方 library Retrofit 來與後端 server 進行通訊(communicate)。

External Libraries

外部程式庫(External Libraries)第三方 libraries 就像是 core Android API擴充功能(extensions)。這些 libraries 大多為 open source、由社群開發,並由全球廣大 Android 社群集體貢獻心力負責維護。這讓包括您在內的 Android developers 能夠打造出更優異的 app。

Retrofit Library

在本程式碼研究室中,您會使用 Retrofit library 來與 RESTful Mars web service 通訊,其為具備完善支援和維護的理想範例 library。只要瀏覽其 GitHub 網頁,查看尚未解決的問題 (部分為功能要求) 和已解決的問題,即可感受上述優勢。若 developers 有在解決問題並定期回應功能要求,表示此 library 維護良好,且極為適合在 app 中使用。此外,亦提供 Retrofit documentation 網頁。

Retrofit library與後端(backend)通訊。其會根據傳遞的參數(parameters)建立 web service 的 URI。您將在稍後的章節中看到更多內容。

新增 Retrofit dependencies

Android Gradle 可讓您將外部 libraries 新增至 project。除了 library dependency 外,亦應包括代管(hosted) library 的 repository。例如來自 Jetpack librariesViewModelLiveDataGoogle libraries,是由 Google repository 負責代管(hosted)。大部分的 community libraries (例如 Retrofit) 皆是由 GoogleMavenCentral repositories 代管。

  1. 開啟 project 的頂層 build.gradle(Project: MarsPhotos) 檔案。請注意在 repositories 區塊下方列出的 repositories。您應該會看到以下兩個 repositories:google()mavenCentral()
1
2
3
4
repositories {
google()
mavenCentral()
}
  1. 開啟 module 層級 Gradle 檔案 build.gradle (Module: MarsPhots.app)
  2. dependencies 區段中,為 Retrofit libraries 新增以下幾行:
1
2
3
4
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Scalar Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
  • 第一個 dependency 針對 Retrofit2 library 本身提供,第二個 dependency 則用於 Retrofit scalar converter(純量轉換工具)。此 converter 可讓 Retrofit 以 String 的形式傳回 JSON 結果。這兩個 libraries 會搭配運作。
  1. 按一下「Sync Now」,使用新的 dependencies rebuild project。

新增 Java 8 語言功能支援

包括 Retrofit2 在內的眾多第三方 libraries,皆使用 Java 8 語言功能(features)。Android Gradle 外掛程式(plugin)提供使用特定 Java 8 語言功能的內建支援。

  1. 如要使用內建功能(built-in features,),您必須在 module 的 build.gradle 檔案中加入下列程式碼。系統已為您完成此步驟,請確認在您的 build.gradle(Module: MarsPhotos.app) 中顯示下列程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
android {
...

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}
}

連線至 Internet

您會使用 Retrofit library 與 Mars web service 進行通訊,並將原始 JSON response 顯示為 String。預留位置 TextView顯示傳回的 JSON response 字串,或顯示連線錯誤的訊息

Retrofit 會根據 web service 的內容為 app 建立 network API。其會從 web service 擷取 data,並透過獨立的轉換工具(converter) library 執行 data 轉送 ,該 library 瞭解 data 解碼方式,且會String 等 objects 形式傳回 data。Retrofit 提供諸如 XML 和 JSON 等熱門 data 格式的內建支援。Retrofit 最終會建立程式碼來呼叫和耗用這項服務,包括在 background threads 執行 requests 之類的重要詳細資訊。

在此工作中,您會將網路層新增至 MarsPhotos project,以供 ViewModel 用於與 web service 通訊。您必須按照下列步驟實作 Retrofit service API

  • 建立網路層 (MarsApiService class)。
  • 使用 base URLconverter factory 來建立 Retrofit object
  • 建立用於說明 Retrofit 如何與 web server 通訊interface
  • 建立 Retrofit service,並向 app 的其餘部分公開 API service instance

實作上述步驟:

  1. 建立名為 network 的新 package。在「Android」project 案窗格中,以滑鼠右鍵按一下套件 com.example.android.marsphotos。依序選取「New」(新增) >「Package」(套件)。在彈出式視窗中,將 network 附加至建議的 package name 結尾處。
  2. 在新 package network 中,建立新的 Kotlin 檔案。將其命名為 MarsApiService
  3. 開啟 network/MarsApiService.kt。為 web service 的 base URL 新增下列常數(constant)。
1
2
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
  1. 在該常數下方,新增 Retrofit builder建立和新增Retrofit object
1
private val retrofit = Retrofit.Builder()
  • 在系統提示時 import retrofit2.Retrofit
  1. Retrofit 需要 web service 的 base URIconverter factory,以 build web services API。converter 會向 Retrofit 告知如何處理從 web service 傳回的 data。在此範例中,您希望 Retrofit 從 web service 擷取 JSON response,並以 String 形式傳回。Retrofit 具備支援字串(strings)和其他原始類型(primitive types)的 ScalarsConverter,因此您會在具有 ScalarsConverterFactory instance 的 builder 上呼叫 addConverterFactory()
1
2
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
  • 在系統提示時 import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 使用 baseUrl() 方法新增 web service 的 base URI。最後,呼叫 build() 以建立 Retrofit object。
1
2
3
4
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
  1. 在 Retrofit builder 的呼叫下方,定義名為 MarsApiService 的 interface,此 interface 會定義 Retrofit 使用 HTTP requestsweb server 通訊的方式
1
2
interface MarsApiService {
}
  1. MarsApiService interface 當中,新增名為 getPhotos() 的函式,以從 web service 取得 response 字串
1
2
3
interface MarsApiService {
fun getPhotos()
}
  1. 使用 @GET 註解,向 Retrofit 表明此為 GET request,並指定該 web service 方法的端點(endpoint)。在此範例中,endpoint 就是 photos。如上個工作中所述,您可在本程式碼研究室中使用 /photos endpoint。
1
2
3
4
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
  • 依要求 import retrofit2.http.GET
  1. 叫用 getPhotos() 方法時,Retrofit 會將 endpoint photos 附加至在 Retrofit builder 中定義的 base URL,以用於啟動 request。將函式的 return type 新增至 String
1
2
3
4
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}

物件宣告

在 Kotlin 中,物件宣告(object declarations)是用來宣告單例模式物件(singleton objects)單例模式(singleton pattern)可確保僅建立一個 object instance,且對該 object 僅有一個全域存取點(global point of access)。物件宣告的初始化為執行緒安全(thread-safe),且會在初次存取時完成。

Kotlin 可讓您輕鬆宣告 singletons。以下是 object 宣告及其存取權的示例。object 宣告在 object 關鍵字後方一律帶有 name。

範例:

1
2
3
4
5
6
7
8
9
10
11
12
// 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 object 上呼叫 create() 函式的代價非常高,且 app 只需要單一 Retrofit API service instance。因此,您可以使用物件宣告,向 app 的其餘部分公開 service

  1. MarsApiService interface 宣告之外,定義名為 MarsApipublic object,以初始化 Retrofit service。這是可從 app 其餘部分存取的 public singleton object。
1
2
3
object MarsApi {

}
  1. MarsApi object 宣告當中,新增名為 retrofitService 的 type MarsApiService 延遲(lazy)初始化 Retrofit object 屬性。執行此延遲初始化的用意,在於確保其在第一次使用時已初始化。您將在後續步驟中修正錯誤。
1
2
3
4
object MarsApi {
val retrofitService : MarsApiService by lazy {
}
}
  1. 透過 MarsApiService interface,使用 retrofit.create() 方法初始化 retrofitService 變數。
1
2
3
4
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java) }
}
  • :: 表示把一個方法當作一個參數,傳遞到另一個方法中進行使用(通俗來說就是引用一個方法)。

Retrofit 設定完成!每當 app 呼叫 MarsApi.retrofitService 時,呼叫端就會存取在第一次存取時建立的同個 singleton Retrofit object 來實作 MarsApiService。在下一個工作中,您將使用先前實作的 Retrofit object。

注意:提醒您,延遲執行個體(lazy instantiation)是指在實際需要 object 之前特意延遲建立該 object,以避免不必要的運算或使用其他運算資源。Kotlin 針對延遲執行個體化(lazy instantiation)提供一流的支援

在 OverviewViewModel 中呼叫 web service

在此步驟中,您會實作 getMarsPhotos() 方法來呼叫 Retrofit service,然後處理傳回的 JSON 字串(string)

ViewModelScope

ViewModelScope 是在 app 中為每個 ViewModel 定義的內建 coroutine scope。若已清除 ViewModel,系統就會自動取消此範圍內啟動(launched)的所有協程(coroutine)

您會使用 ViewModelScope 來啟動協程(coroutine),並在背景執行 Retrofit network 呼叫

  1. MarsApiService 中,將 getPhotos() 設為 suspend 函式。這樣就能在 coroutine 中呼叫此方法。
1
2
@GET("photos")
suspend fun getPhotos(): String
  1. 開啟 overview/OverviewViewModel。向下捲動至 getMarsPhotos() 方法。刪除將 status response 設為 “Set the Mars API Response here!”. 的行。方法 getMarsPhotos() 應已空白。
1
2
3
private fun getMarsPhotos() {

}
  1. getMarsPhotos() 當中,使用 viewModelScope.launch 啟動 coroutine。
1
2
3
4
private fun getMarsPhotos() {
viewModelScope.launch {
}
}
  • 在系統提示時 import androidx.lifecycle.viewModelScopekotlinx.coroutines.launch
  1. viewModelScope 當中,使用 singleton object MarsApi retrofitService interface 呼叫 getPhotos() 方法。將傳回的 response 儲存於名為 listResultval
1
2
3
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
  • 在系統提示時 import com.example.android.marsphotos.network.MarsApi
  1. 將剛從後端 server 收到的結果指派至 _status.value
1
2
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = listResult
  1. 執行 app,請注意 app 會立即關閉,且不一定會顯示錯誤彈出式視窗。

  2. 按一下 Android Studio 中的「Logcat」分頁標籤,會顯示錯誤「missing INTERNET permission」。

此錯誤訊息代表 app 可能缺少 INTERNET 權限(permissions)。在下一個工作中,您會新增 app 的 internet permissions 來解決這個問題。


新增 Internet 權限與處理 Exception

Android Permissions

Android 系統的權限(permissions)旨在保護 Android 使用者的隱私權。Android apps 必須宣告或要求權限,以存取諸如聯絡人、通話記錄等敏感使用者資料,以及例如 camera 或 internet 等特定系統功能。

您必須具備 INTERNET permissions,才可讓 apps 存取 internet。連線至 internet 後會引發安全性疑慮,因此根據預設,apps 無 internet 連線。您必須明確宣告 app 需要存取 internet。這視為一般權限。如要進一步瞭解 Android permissions 及其類型,請參閱說明文件

在此步驟中,app 會在 AndroidManifest 檔案中加入 <uses-permission> 標記,以宣告所需的權限。

  1. 開啟 manifests/AndroidManifest.xml。在 <application> 標記前方加上這一行:
1
<uses-permission android:name="android.permission.INTERNET" />
  1. 編譯並再次執行 app。若您有可用的 internet 連線,應會看到內含 Mars photos 相關資料的 JSON text。稍後您可在程式碼研究室中進一步瞭解 JSON 格式。
  1. 輕觸裝置或模擬器中的「Back」按鈕,關閉 app。

  2. 將裝置或模擬器設為飛航模式,以模擬網路連線錯誤。從最近用過的選單重新開啟 app,或從 Android Studio 重新啟動 app。

  3. 按一下 Android Studio 中的「Logcat」分頁標籤,然後記下 Log 中的嚴重例外狀況(fatal exception),如下所示:

處理 Exception

例外狀況(Exception) 是指在執行階段期間(runtime)【非編譯期間(compile time)】 可能發生的錯誤,會在未通知使用者的情況下突然終止 app。這會對使用者體驗造成負面影響。例外狀況處理(Exception handling)是一種機制,可避免 app 突然終止,並以使用者容易理解的方式處理

發生 exceptions 的原因可能很單純,例如以零為除數或網路發生錯誤。這些 exceptions 與您在先前程式碼研究室中學到的 NumberFormatException 類似。

連線至 server 時可能發生的問題範例:

  • API 使用的 URL 或 URI 不正確
  • server 無法使用,且 app 無法連線至 server
  • 網路延遲(Network latency)問題。
  • 裝置的網際網路(internet)連線狀況不良無網際網路(internet)連線

編譯期間(compile time)無法擷取這些 exceptions。您可以使用 try-catch 區塊來處理執行階段(runtime)中的 exceptions。如要進一步瞭解,請參閱說明文件

Try-catch 區塊的範例語法

1
2
3
4
5
6
try {
// some code that can cause an exception.
}
catch (e: SomeException) {
// handle the exception to avoid abrupt termination.
}

try 區塊當中,執行預期發生 exception 的所在程式碼,這在 app 中稱為 network call。您必須在 catch 區塊中實作程式碼,以避免 app 突然終止。若發生 exception,系統將執行 catch 區塊來復原錯誤,而不會突然終止 app。

  1. 開啟 overview/OverviewViewModel.kt。向下捲動至 getMarsPhotos() 方法。在啟動區塊當中,在 MarsApi 呼叫周圍新增 try 區塊來處理 exception。在 try 區塊後方新增 catch 區塊:
1
2
3
4
5
6
7
8
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = listResult
} catch (e: Exception) {

}
}
  1. catch {} 區塊當中處理 failure response。將 e.message 設為 _status.value,以向使用者顯示 error message。
1
2
3
catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
  1. 開啟飛航模式,並再次執行 app。此時不會突然關閉 app,但會改為顯示 error message。
  1. 關閉手機或模擬器的飛航模式。執行並測試您的 app,確定一切運作正常,且您能夠查看 JSON 字串。

使用 Moshi 剖析 JSON response

JSON

requested data 通常為常用的資料格式,例如 XMLJSON。每次呼叫都會傳回結構化資料(structured data),而 app 必須瞭解該結構的內容,才能讀取 response 中的 data。

舉例來說,您將在此 app 中從下列 server 擷取 data:https://android-kotlin-fun-mars-server.appspot.com/photos 。若您在瀏覽器中輸入此 URL,即會顯示 JSON 格式的 Mars surface IDsimage URLs list

範例 JSON 回應結構:

  • JSON response陣列(array),以方括號 [] 表示。陣列(array)包含 JSON objects
  • JSON objects 會以大括號 {} 括住。
  • 每個 JSON objects 皆內含一組 name-value 配對,並以半形逗號 , 分隔。
  • 配對的 namevalue 會以半形冒號 : 分隔。
  • name 會以引號 "" 括住。
  • value 可以是數字(numbers)、字串(strings)、布林值(boolean)、陣列(array)、物件 (JSON object) 或空值(null)。

舉例來說,img_src 是一個 url 字串。若將 url 貼至網路瀏覽器中,就會看到 Mars surface image。

您現可從 Mars web service 取得 JSON response,這是個不錯的起點。但您真正需要的是 Kotlin objects,而非大型 JSON 字串。此外還有一個名為 Moshi 的外部 library,這個 Android JSON 剖析器(parser)將 JSON 字串轉換為 Kotlin objects。Retrofit 具備可與 Moshi 搭配使用的轉換工具(converter),是非常適合在這裡使用的優異 library。

在此工作中,您會使用 Moshi library 搭配 Retrofit,將 web service 中的 JSON response 剖析為呈現 Mars photos 的實用 Kotlin objects。App 會改為顯示傳回的 Mars photos 數量,而非顯示原始 JSON。

新增 Moshi library dependencies

  1. 開啟 build.gradle (Module: app)
  2. 在 dependencies 區段新增以下程式碼,以包含 Moshi dependency。此 dependency 會新增使用 Kotlin support 的 Moshi JSON library support。
1
2
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
  1. dependencies 區塊中找出 Retrofit scalar converter 行,並將以下 dependencies 變更為使用 converter-moshi

將以下

1
2
3
4
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with scalar Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

替換為

1
2
// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
  1. 按一下「Sync Now」(立即同步處理),使用新 dependencies rebuild project。

注意:Project 可能會顯示與已移除 Retrofit scalar dependency 相關的編譯器錯誤。您會在接下來的步驟中修正這些錯誤。

實作 Mars Photos data class

從 web service 取得的 JSON response 範例項目看起來會像這樣,如下所示:

1
2
3
4
5
[{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]

在上述範例中,請注意每個 Mars photo 項目皆具有以下的 JSON key 與 value 配對:

  • id:屬性的 ID,以 string 表示。由於其已納入 " ",因此屬於 String 類型而非 Integer
  • img_src:image 的 URL,以 string 表示。

Moshi 會剖析此 JSON data,然後將其轉換為 Kotlin objects。如要這麼做,Moshi 必須具有 Kotlin data class儲存剖析結果(將轉換完成的 Kotlin objects 儲存在 data class),因此您會在此步驟中建立 data class MarsPhoto

  1. 在 network 套件上按一下滑鼠右鍵,然後依序選取「New」>「Kotlin File/Class」。
  2. 在彈出式視窗中選取「Class」,然後輸入 MarsPhoto 做為 class name。這麼做會在 network package 中建立名為 MarsPhoto.kt 的新檔案。
  3. 在 class 定義前方新增 data keyword,以將 MarsPhoto 設為 data class。將 {} 括號變更為 () 括號。這樣會發生錯誤,因為 data class 必須定義至少一個屬性。
1
2
data class MarsPhoto(
)
  1. 將下列屬性新增至 MarsPhoto 類別定義。
1
2
3
data class MarsPhoto(
val id: String, val img_src: String
)
  • 請注意,MarsPhoto class 中的每個變數皆會對應至 JSON object 中的 key name。如要比對特定 JSON response 中的 types,請為所有 value 使用 String object。

Moshi 剖析 JSON 時,會根據 name 比對 key,並在 data object 中填入適當的 value

@Json 註解

有時,JSON response 中的 key name 可能導致 Kotlin 屬性有所混淆,或與建議的程式設計樣式不符;舉例來說,在 JSON 檔案中,img_src key 會使用底線,而屬性的 Kotlin 慣例會使用大小寫字母 (駝峰式大小寫)。

如要在 data class 中使用與 JSON response 中 key name 不同的變數名稱,請使用 @Json 註解。在此範例中,data class 中的變數名稱為 imgSrcUrl。您可使用 @Json(name = "img_src") 將變數對應至 JSON 屬性 img_src

  1. img_src key 這行替換為以下顯示的行。
1
@Json(name = "img_src") val imgSrcUrl: String
  • 依要求 import com.squareup.moshi.Json

更新 MarsApiService 和 OverviewViewModel

在此工作中,您會使用 Moshi builder 來建立 Moshi object,做法與 Retrofit builder 類似。

您會ScalarsConverterFactory 替換為 KotlinJsonAdapterFactory,以讓 Retrofit 知道可以使用 Moshi 將 JSON response 轉換為 Kotlin object。接著會更新 network APIViewModel,以使用 Moshi object

  1. 開啟 network/MarsApiService.kt。注意 ScalarsConverterFactory 的未解決 reference 錯誤。這是因為您在先前的步驟中已變更 Retrofit dependency。刪除 ScalarConverterFactory 的 import 作業。您會在不久之後修正其他錯誤。

移除:

1
import retrofit2.converter.scalars.ScalarsConverterFactory
  1. 在檔案頂端 (在 Retrofit builder 之前),新增下列程式碼以建立 Moshi object,類似 Retrofit object。
1
private val moshi = Moshi.Builder()
  • 依照要求 import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactorycom.squareup.moshi.Moshi
  1. 若要讓 Moshi 註解與 Kotlin 順利搭配運作,請在 Moshi builder 中新增 KotlinJsonAdapterFactory,然後呼叫 build()
1
2
3
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
  1. retrofit object 宣告中,將 Retrofit builder 變更為使用 MoshiConverterFactory 而非 ScalarConverterFactory,並傳遞您剛建立的 moshi instance
1
2
3
4
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
  • 依要求 import retrofit2.converter.moshi.MoshiConverterFactory
  1. 您已將 MoshiConverterFactory 設定妥當,現在可以要求 Retrofit 從 JSON array 傳回 MarsPhoto objects list,而非傳回 JSON string。更新 MarsApiService interface,讓 Retrofit 傳回 MarsPhoto objects list,而非傳回 String
1
2
3
4
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
  1. viewModel 進行類似的變更,開啟 OverviewViewModel.kt。向下捲動至 getMarsPhotos() 方法。

  2. 在方法 getMarsPhotos() 中,listResultList<MarsPhoto> 而不再是 String。該 list 大小為已接收和剖析的 photos 數量。如要輸出已擷取的 photos 數量,請按照下列方式更新 _status.value

1
_status.value = "Success: ${listResult.size} Mars photos retrieved"
  • 在系統提示時 import com.example.android.marsphotos.network.MarsPhoto
  1. 確認裝置或模擬器已關閉飛航模式。編譯並執行 app。此時 message 應會顯示從 web service 傳回的屬性數量,而非大型 JSON string:

總結

REST web services

  • web service 是透過 internet 提供的軟體功能,,可讓 app 傳送 requests 和傳回 data
  • 一般的 web service 使用 REST 架構提供 REST 架構的 web service 稱為 RESTful services。符合 RESTful web services,均使用標準 web 元件(components)通訊協定(protocols)建構而成。
  • 透過 URIs 以標準化方式向 REST web service 傳送 request
  • 如要使用 web service,app 必須建立 network connection,並與 service 通訊。接著,app 必須接收 response data,並將其剖析為可供 app 使用的格式。
  • Retrofit library 是一個 client library,可讓 app 向 REST web service 發出 requests
  • 使用 converters 向 Retrofit 告知該如何處理傳送至 web service 的 data,以及從 web service 傳回的 data。舉例來說, ScalarsConverter converter 會將 web service data 視為 String 或其他原始檔案(primitive)。
  • 如要讓 app 連上 internet,請在 Android manifest 中新增 "android.permission.INTERNET" 權限。

JSON 剖析

  • web service 的 response 通常會以 JSON 格式表示,這是一種代表結構化資料的常用格式。
  • JSON object 是一組 key-value 組合。
  • 一組 JSON object 稱為 JSON array。您可以從 web service 取得 JSON array 做為 response
  • key-value 組合的 keys 前後會加上半形引號 ""。values 可以是 numbers 或 strings
  • Moshi library 為 Android JSON 剖析器(parser),可JSON string 轉換為 Kotlin objects。Retrofit 具備可與 Moshi 搭配使用的 converter
  • Moshi 會比對 JSON response 中的 keys 與 data object 中的同名屬性
  • 如要為某個 key 使用不同的屬性名稱,請為該屬性加上 @Json 註解JSON key name