從網際網路取得資料

1. 事前準備

市面上大多數的 Android 應用程式皆會連線至網際網路,以執行部分網路作業。例如從後端伺服器擷取電子郵件、訊息或類似資訊。Gmail、YouTube 及 Google 相簿等應用程式都是透過連線至網際網路,顯示使用者資料。

在這個程式碼研究室中,您將使用以開放原始碼開發的程式庫建構網路層,並從後端伺服器取得資料。這樣可大幅簡化資料擷取作業,還可讓應用程式符合 Android 最佳做法,例如在背景執行緒上執行作業。若網際網路連線速度緩慢或無法使用,您也可更新應用程式的使用者介面,以讓使用者隨時掌握任何網路連線問題。

須知事項

  • 如何建立及使用片段。
  • 如何使用 Android 架構元件 ViewModelLiveData
  • 如何在 Gradle 檔案中新增依附元件。

課程內容

  • 什麼是 REST 網路服務。
  • 使用 Retrofit 程式庫連線至網際網路上的 REST 網路服務並取得回應。
  • 使用 Moshi 程式庫將 JSON 回應剖析至資料物件。

要執行的步驟

  • 修改入門應用程式,以發出網路服務 API 要求和處理回應。
  • 使用 Retrofit 程式庫為應用程式實作網路層。
  • 使用 Moshi 程式庫,將網路服務中的 JSON 回應剖析至應用程式的 LiveData 物件。
  • 使用 Retrofit 提供的協同程式支援簡化程式碼。

軟硬體需求

  • 已安裝 Android Studio 的電腦。
  • MarsPhotos 應用程式範例程式碼。

2. 應用程式總覽

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

ea967f35fa98d72b.png

您在本程式碼研究室中建構的應用程式版本不會採用大量視覺閃光特效:其著重於應用程式的網路層部分,以連線至網際網路並透過網路服務下載原始屬性資料。為確保系統正確擷取和剖析資料,請直接在文字檢視區塊輸出從後端伺服器接收的相片數量:

1a7e99791caf8d96.png

3. 探索 MarsPhotos 入門應用程式

下載範例程式碼

本程式碼研究室提供範例程式碼,可延伸至本程式碼研究室所教授的功能。範例程式碼可能含有來自先前程式碼研究室,且對您來說感到熟悉或陌生的程式碼。您將在稍後的程式碼研究室中進一步瞭解關於陌生程式碼的資訊。

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

  1. 前往專案指定的 GitHub 存放區頁面。
  2. 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」

1e4c0d2c081a8fd2.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦上尋找該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下 ZIP 檔案,將檔案解壓縮。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open」

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。

執行範例程式碼

  1. 在 Android Studio 中開啟已下載的專案。專案的資料夾名稱為 android-basics-kotlin-mars-photos-app。範例程式碼的資料夾結構應如下所示。
  2. 在「Android」窗格中,依序展開「app」(應用程式) >「java」。請注意,應用程式有名為「overview」的套件資料夾。這是應用程式的 UI 層。

d428a16d480349d1.png

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

406c7bcc0f07267c.png

  1. 瀏覽檔案以瞭解範例程式碼。針對版面配置檔案,您可以使用右上角的「Split」(分割) 選項,同時查看版面配置和 XML 的預覽畫面。

範例程式碼逐步操作說明

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

OverviewFragment:

  • 此為 MainActivity 中顯示的片段。您在上個步驟中看到的預留位置文字會顯示於此片段。
  • 在下一個程式碼研究室中,此片段會顯示自「火星」相片後端伺服器接收的資料。
  • 此類別會保留 OverviewViewModel 物件的參照。
  • OverviewFragmentonCreateView() 函式會使用「資料繫結」來加載 fragment_overview 版面配置,將繫結生命週期擁有者設定為自己,並在其繫結物件中設定 viewModel 變數。
  • 指派生命週期擁有者後,系統會自動觀察「資料繫結」中使用的任何 LiveData 是否有任何變更,並據以更新使用者介面。

OverviewViewModel:

  • 此為 OverviewFragment 的對應檢視模型。
  • 此類別包含名為 _statusMutableLiveData 屬性及其幕後屬性。更新此屬性的值時,會一併更新畫面上顯示的預留位置文字。
  • getMarsPhotos() 方法會更新預留位置回應。稍後在程式碼研究室中,您將使用此程式碼來顯示從伺服器擷取的資料。本程式碼研究室的目標,在於使用從網際網路取得的實際資料來更新 ViewModel 中的 status LiveData

res/layout/fragment_overview.xml

  • 此版面配置已設為使用資料繫結,且由單一 TextView 組成。
  • 其會宣告 OverviewViewModel 變數,然後將 statusViewModel 繫結至 TextView

MainActivity.kt:此活動的唯一工作是載入該活動的版面配置 activity_main

layout/activity_main.xml: 主要活動版面配置具有指向 fragment_overview 的單一 FragmentContainerView,總覽片段會在應用程式啟動時執行個體化。

4. 應用程式總覽

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

1c2493b9e9e1eef.png

OverviewViewModel 負責執行網路通話以取得火星相片資料。在 ViewModel 中,您會使用 LiveData 搭配生命週期感知資料繫結,在資料變更時更新應用程式使用者介面。

5. 網路服務與 Retrofit

「火星」相片資料會儲存在網路伺服器中。如要讓應用程式取得此資料,您必須建立連線並與網際網路上的伺服器通訊。

37f7c367e182b4f9.png

d99aca47f5947a78.png

現今的大部分網路伺服器會使用稱為 REST 的一般無狀態網路架構,其中 RE 為「Representational」(表示法) 的縮寫,S 為「State」(狀態) 的縮寫,T 為「Transfer」(傳輸) 的縮寫。提供此架構的網路服務,稱為符合 REST 樣式的服務。

系統會透過 URI 以標準化方式向符合 REST 樣式的網路服務發出要求。URI (統一資源識別項) 會依名稱來識別伺服器中的資源,而不會暗示其位置或存取方式。舉例來說,在本課程的應用程式中,您將使用下列伺服器 URI 來擷取圖片網址 (此伺服器會代管「火星」房地產和「火星」相片):

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

「統一資源定位器」(URL) 是一種 URI,其會指定運作或取得資源表示法的方式,亦即同時指定的主要存取機制與網路位置。

例如:

下列網址會列出「火星」上所有可用的房地產資源!

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

下列 URL 會取得火星相片的清單:

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

這些網址是指識別的資源,例如 /realestate/photos,您可透過「超文本傳輸通訊協定」(http:) 從網路中取得。您將在本程式碼研究室中使用 /photos 端點。

網路服務要求

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

常見的 HTTP 作業包括:

  • GET 用於擷取伺服器資料
  • POST 或 PUT 用於新增/建立/更新伺服器的新資料
  • DELETE 用於刪除伺服器中的資料

應用程式會向伺服器傳送「火星」相片資訊的 HTTP GET 要求,接著伺服器會對應用程式傳回回應,包括圖片網址。

bcd50e389186fa98.png

1e08dbc82558a7cd.png

網路服務的回應通常會採用 XML 或 JSON 這種常見的網路格式進行格式化 (在鍵/值組合中代表結構化資料格式)。我們將在後續工作中進一步瞭解 JSON。

在這項工作中,您要建立與伺服器的連線、與伺服器通訊,以及接收 JSON 回應。您將使用已寫入的後端伺服器。在本程式碼研究室中,您將使用 Retrofit 程式庫第三方程式庫,來與後端伺服器進行通訊。

外部程式庫

外部程式庫或第三方程式庫就像是核心 Android API 的擴充功能。這些程式庫大多為開放原始碼、由社群開發,並由全球廣大 Android 社群集體貢獻心力負責維護。這讓包括您在內的 Android 開發人員能夠打造出更優異的應用程式。

Retrofit 程式庫

在本程式碼研究室中,您會使用 Retrofit 程式庫來與符合 REST 樣式的「火星」網路服務通訊,其為具備完善支援和維護的理想範例程式庫。只要瀏覽其 GitHub 網頁,查看尚未解決的問題 (部分為功能要求) 和已解決的問題,即可感受上述優勢。若開發人員有在解決問題並定期回應功能要求,表示此程式庫維護良好,且極為適合在應用程式中使用。此外,亦提供 Retrofit 說明文件網頁。

Retrofit 程式庫將與後端通訊。其會根據傳遞的參數,建立網路服務的 URI。您將在稍後的章節中看到更多內容。

a8f10b735ad998ac.png

新增 Retrofit 依附元件

Android Gradle 可讓您將外部程式庫新增至專案。除了程式庫依附元件外,亦應包括代管程式庫的存放區。諸如來自 Jetpack 程式庫的 ViewModelLiveData 等 Google 程式庫,是由 Google 存放區負責代管。大部分的社群資料庫 (例如 Retrofit) 皆是由 Google 和 MavenCentral 存放區代管。

  1. 開啟專案的頂層 build.gradle(Project: MarsPhotos) 檔案。請注意在 repositories 區塊下方列出的存放區。您應該會看到以下兩個存放區:google()mavenCentral()
repositories {
   google()
   mavenCentral()
}
  1. 開啟模組層級 Gradle 檔案 build.gradle (Module: MarsPhots.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 純量轉換工具。此轉換工具可讓 Retrofit 以 String 的形式傳回 JSON 結果。這兩個程式庫會搭配運作。

  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'
  }
}

6. 正在連線至網際網路

您會使用 Retrofit 程式庫與「火星」網路服務進行通訊,並將原始 JSON 回應顯示為 String。預留位置 TextView 會顯示傳回的 JSON 回應字串,或顯示連線錯誤的訊息。

Retrofit 會根據網路服務的內容為應用程式建立網路 API。其會從網路服務擷取資料,並透過獨立的轉換工具程式庫執行資料轉送,該程式庫瞭解資料解碼方式,且會以 String 等物件形式傳回資料。Retrofit 提供諸如 XML 和 JSON 等熱門資料格式的內建支援。Retrofit 最終會建立程式碼來呼叫和耗用這項服務,包括在背景執行緒執行要求之類的重要詳細資訊。

deb437805232f6a.png

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

  • 建立網路層 (MarsApiService 類別)。
  • 使用基準網址和轉換工具工廠來建立 Retrofit 物件。
  • 建立用於說明 Retrofit 如何與網路伺服器通訊的介面。
  • 建立 Retrofit 服務,並向應用程式的其餘部分公開 API 服務執行個體。

實作上述步驟:

  1. 建立名為網路的新套件。在「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。如上個工作中所述,您可在本程式碼研究室中使用 /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.getPhotos()
 _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 權限。在下一個工作中,您會新增應用程式的網際網路權限來解決這個問題。

7. 新增網際網路權限與例外狀況處理

Android 權限

Android 系統的權限旨在保護 Android 使用者的隱私權。Android 應用程式必須宣告或要求權限,以存取諸如聯絡人、通話記錄等敏感使用者資料,以及諸如相機或網際網路等特定系統功能。

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

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

  1. 開啟 manifests/AndroidManifest.xml。在 <application> 標記前方加上這一行:
<uses-permission android:name="android.permission.INTERNET" />
  1. 編譯並再次執行應用程式。若您有可用的網際網路連線,應會看到內含火星相片相關資料的 JSON 文字。稍後您可在程式碼研究室中進一步瞭解 JSON 格式。

205710014543679a.png

  1. 輕觸裝置或模擬器中的「Back」按鈕,關閉應用程式。
  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
...

此錯誤訊息代表應用程式嘗試連線和逾時。此類例外狀況是極為常見的即時例外狀況。在下一個步驟中,您將瞭解如何處理這類例外狀況。

例外狀況處理

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

發生例外狀況的原因可能很單純,例如以零為除數或網路發生錯誤。這些例外狀況與您在先前程式碼研究室中學到的 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. 開啟飛航模式,並再次執行應用程式。此時不會突然關閉應用程式,但會改為顯示錯誤訊息。

368c746124f57a93.png

  1. 關閉手機或模擬器的飛航模式。執行並測試您的應用程式,確定一切運作正常,且您能夠查看 JSON 字串。

8. 使用 Moshi 剖析 JSON 回應

JSON

要求的資料通常為常用的資料格式,例如 XML 或 JSON。每次呼叫都會傳回結構化資料,而應用程式必須瞭解該結構的內容,才能讀取回應中的資料。

舉例來說,您將在此應用程式中從下列伺服器擷取資料:https:// android-kotlin-fun-mars-server.appspot.com/photos。若您在瀏覽器中輸入此網址,即會顯示 JSON 格式的火星表面 ID 和圖片網址清單!

範例 JSON 回應結構:

fde4f6f199990ae8.png

  • JSON 回應為陣列,以方括號表示。陣列包含 JSON 物件。
  • JSON 物件會以大括號括住。
  • 每個 JSON 物件皆內含一組名稱-值配對,並以半形逗號分隔。
  • 配對的名稱和值會以半形冒號分隔。
  • 名稱會以引號括住。
  • 值可以是數字、字串、布林值、陣列、物件 (JSON 物件) 或空值。

舉例來說,img_src 是一個網址字串。若將網址貼至網路瀏覽器中,就會看到火星表面的圖片。

b4f9f196c64f02c3.png

您現可從「火星」網路服務取得 JSON 回應,這是個不錯的起點。但您真正需要的是 Kotlin 物件,而非大型 JSON 字串。此外還有一個名為 Moshi 的外部程式庫,這個 Android JSON 剖析器可將 JSON 字串轉換為 Kotlin 物件。Retrofit 具備可與 Moshi 搭配使用的轉換工具,是非常適合在這裡使用的優異程式庫。

在此工作中,您會使用 Moshi 程式庫搭配 Retrofit,將網路服務中的 JSON 回應剖析為呈現火星相片的實用 Kotlin 物件。應用程式會改為顯示傳回的火星相片數量,而非顯示原始 JSON。

新增 Moshi 程式庫依附元件

  1. 開啟 build.gradle (Module: app)
  2. 在依附元件區段新增以下程式碼,以包含 Moshi 依附元件。此依附元件會新增使用 Kotlin 支援的 Moshi JSON 程式庫支援。
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
  1. dependencies 區塊中找出 Retrofit 純量轉換工具行,並將以下依附元件變更為使用 converter-moshi

替換為

// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with scalar 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("photos")
   suspend 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 字串:

8f47f004c7f91394.png

9. 解決方案程式碼

build.gradle(Module: MarsPhotos.app)

以下是要包含的新依附元件。

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

    // 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}"
           }
       }
   }
}

10. 摘要

REST 網路服務

  • 網路服務是透過網際網路提供的軟體功能,可讓應用程式傳送要求和傳回資料。
  • 一般的網路服務使用 REST 架構。提供 REST 架構的網路服務稱為「符合 REST 樣式」的服務。符合 REST 樣式的網路服務,均使用標準網路元件和通訊協定建構而成。
  • 透過 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 鍵名。

11. 瞭解詳情

Android 開發人員說明文件:

Kotlin 說明文件:

其他: