從網際網路取得資料

1. 事前準備

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

在本程式碼研究室中,您將使用開放原始碼和社群導向的程式庫建構資料層,並從後端伺服器取得資料。這樣可大幅簡化資料擷取作業,還可讓應用程式遵守 Android 最佳做法,例如在背景執行緒上執行作業。如果網際網路速度緩慢或無法使用,系統也會顯示錯誤訊息,讓使用者隨時掌握任何網路連線問題。

必要條件

  • 如何建立可組合函式的基本知識。
  • 如何使用 Android 架構元件 ViewModel 的基本知識。
  • 如何使用協同程式來處理長時間執行的工作的基本知識。
  • 如何在 build.gradle.kts 中新增依附元件的基本知識。

課程內容

執行步驟

  • 修改範例應用程式,發出 Web 服務 API 要求並處理回應。
  • 使用 Retrofit 程式庫為應用程式實作資料層。
  • 使用 kotlinx.serialization 程式庫,將 Web 服務中的 JSON 回應剖析為應用程式的資料物件清單,然後附加至 UI 狀態。
  • 使用 Retrofit 提供的協同程式支援簡化程式碼。

軟硬體需求

  • 搭載 Android Studio 的電腦
  • Mars Photos 應用程式的範例程式碼

2. 應用程式總覽

您可使用 Mars Photos 應用程式,顯示火星表面的圖片。此應用程式會連線至 Web 服務,擷取及顯示火星的相片。這些圖片是自 NASA 火星漫遊者擷取的火星實景相片。下圖是最終應用程式的螢幕截圖,其中包含格線狀圖片。

68f4ff12cc1e2d81.png

您在本程式碼研究室中建構的應用程式版本不會採用大量視覺閃光效果。本程式碼研究室著重於應用程式的資料層,這個部分需要連上網際網路,並透過 Web 服務下載原始屬性資料。為確保應用程式正確擷取及剖析這項資料,您可以在 Text 可組合函式中,顯示從後端伺服器接收的相片數量。

a59e55909b6e9213.png

3. 探索 Mars Photos 範例應用程式

下載範例程式碼

如要開始使用,請先下載範例程式碼:

或者,您也可以複製 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. 執行應用程式。當您編譯及執行應用程式時,應會在下列畫面中央看見預留位置文字。在本程式碼研究室結束時,您會將此預留位置文字更新為已擷取的相片數量。

95328ffbc9d7104b.png

範例程式碼逐步操作說明

您可以透過這個工作熟悉專案結構。下列清單提供專案中重要檔案和資料夾的逐步操作說明。

ui\MarsPhotosApp.kt

  • 此檔案包含 MarsPhotosApp 可組合項,這個可組合項會在螢幕上顯示內容,例如頂端應用程式列和 HomeScreen 可組合項。上一個步驟中的預留位置文字會顯示在這個可組合項中。
  • 在下一個程式碼研究室中,此可組合項會顯示從火星相片後端伺服器接收的資料。

screens\MarsViewModel.kt

  • 此檔案是 MarsPhotosApp 的對應檢視模型。
  • 此類別包含名為 marsUiStateMutableState 屬性。更新此屬性的值時,會一併更新畫面上顯示的預留位置文字。
  • getMarsPhotos() 方法會更新預留位置回應。在程式碼研究室的後續部分,您會使用這個方法顯示從伺服器擷取的資料。本程式碼研究室的目標,在於使用從網際網路取得的資料來更新 ViewModel 中的 MutableState

screens\HomeScreen.kt

  • 這個檔案包含 HomeScreenResultScreen 可組合項。ResultScreen 有簡單的 Box 版面配置,會在 Text 可組合項中顯示 marsUiState 的值。

MainActivity.kt

  • 此活動的唯一工作是載入 ViewModel 並顯示 MarsPhotosApp 可組合項。

4. 網路服務簡介

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

76551dbe9fc943aa.png

MarsViewModel 負責執行網路通話以取得火星相片資料。在 ViewModel 中,您可以使用 MutableState 在資料變更時更新應用程式使用者介面。

5. 網路服務與 Retrofit

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

301162f0dca12fcf.png

7ced9b4ca9c65af3.png

現今大部分提供 Web 服務的網路伺服器,都使用了名為 REST 的常見無狀態網路架構。其中 RE 代表「Representational」(表現層),S 代表「State」(狀態),T 則為「Transfer」(轉換)。提供此架構的網路服務,稱為符合 REST 樣式的服務。

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

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

網址 (統一資源定位器) 是 URI 的子集,用於指定資源存在的位置和擷取機制。

例如:

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

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

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

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

這些網址是指識別的資源,例如 /realestate/photos,您可透過超文本傳輸通訊協定 (http:) 從網路取得這些資源。在本程式碼研究室中使用 /相片 端點。端點是 URL,可讓您存取在伺服器中執行的 Web 服務。

網路服務要求

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

常見的 HTTP 作業包括:

  • GET 用於擷取伺服器資料。
  • POST 用於在伺服器上建立新資料。
  • PUT 可用來更新伺服器上的現有資料。
  • DELETE 用於刪除伺服器中的資料

應用程式會向伺服器傳送 HTTP GET 要求,藉此獲取火星相片資訊,接著伺服器會將回應傳回給應用程式,包括圖片網址。

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

Web 服務的回應會採用常用的資料格式,例如 XML (可延伸標記語言) 或 JSON (JavaScript 物件標記法)。JSON 格式代表鍵/值組合中的結構化資料。應用程式使用 JSON 與 REST API 進行通訊,詳情請參閱後續工作。

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

外部程式庫

外部程式庫或第三方程式庫就像是核心 Android API 的擴充功能。您在本課程中使用的程式庫為開放原始碼、由社群開發,並由全球廣大 Android 社群集體貢獻心力負責維護。這些資源可協助 Android 開發人員打造更優質的應用程式。

Retrofit 程式庫

在本程式碼研究室中,您會使用 Retrofit 程式庫來與符合 REST 樣式的「火星」Web 服務通訊。這個程式庫是具備完善支援和維護服務的優質程式庫例子。只要瀏覽其 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 程式庫的更新版本。此 scalar 轉換工具可讓 Retrofit 以 String 的形式傳回 JSON 結果。JSON 是用於在用戶端和伺服器之間儲存和傳輸資料的格式。後續章節會提供 JSON 的更多資訊。

  1. 按一下「Sync Now」,使用新依附元件重新建構專案。

6. 正在連線至網際網路

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

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

8c3a5c3249570e57.png

在此工作中,您會將資料層新增至 ViewModel 用於與 Web 服務通訊的 Mars Photos 專案。請按照下列步驟實作 Retrofit 服務 API。

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

實作上述步驟:

  1. 在 Android 專案窗格中的 com.example.marsphotos 套件上按一下滑鼠右鍵,然後依序選取「New」>「Package」
  2. 在彈出式視窗中,將 network 附加至建議的套件名稱結尾處。
  3. 在新套件 network 中,建立新的 Kotlin 檔案。將其命名為 MarsApiService
  4. 開啟 network/MarsApiService.kt
  5. 為 Web 服務的基準網址新增下列常數。
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 服務的基準網址。
  2. 呼叫 build() 以建立 Retrofit 物件。
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. 在對 Retrofit 建構工具的呼叫下方,定義名為 MarsApiService 的介面,此介面會定義 Retrofit 使用 HTTP 要求與網路伺服器通訊的方式。
interface MarsApiService {
}
  1. 將名為 getPhotos() 的函式新增至 MarsApiService 介面,從 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 附加至在 Retrofit 建構工具中定義的基準網址,用於啟動要求。

  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 的類型 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 物件。

在 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 是可變動的狀態物件,代表最新的網路要求狀態。
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 重複情形。在後續的程式碼研究室中,我們會進一步探討 JSON 格式。

b82ddb79eff61995.png

  1. 輕觸裝置或模擬器中的「Back」按鈕,關閉應用程式。

例外狀況處理

程式碼有錯誤。請按照下列步驟查看:

  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 使用的網址或 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 服務擷取。
  • 錯誤狀態表示所有網路或連線錯誤,

如要在應用程式中表示這三個狀態,請使用密封介面sealed interface 會限制可能的值,讓您輕鬆管理狀態。在 Mars Photos 應用程式中,您會將 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 檔案。在匯入陳述式後,新增 MarsUiState 密封介面。此附加項使,MarsUiState 物件的值就變得完整。
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. MarsViewModel 類別中,更新 marsUiState 定義。將類型變更為 MarsUiStateMarsUiState.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. 您可以從 try-catch 區塊中提出 marsUiState 作業。您完成的函式應如下程式碼所示:
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 伺服器擷取資料。在瀏覽器中輸入此網址時,您會看到 JSON 格式的火星表面 ID 和圖片網址清單!

範例 JSON 回應結構

顯示鍵值和 JSON 物件

JSON 回應的結構具有下列功能:

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

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

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. 按一下「Sync Now」,使用新依附元件重新建構專案。

實作 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: 網址圖片,以字串表示。

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() 方法中,listResultList<MarsPhoto>,而非 String。該清單大小為已接收和剖析的相片數量。

  1. 如要輸出已擷取的相片數量,請按照下列方式更新 marsUiState
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. 確保裝置或模擬器已關閉飛航模式。編譯並執行應用程式。

這時訊息應顯示從 Web 服務傳回的屬性數量,而非大型 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 架構的網路服務稱為「符合 REST 樣式」的服務。符合 REST 樣式的網路服務,均使用標準網路元件和通訊協定建構而成。
  • 您可以透過 URI,以標準化的方式向 REST Web 服務傳送要求。
  • 如要使用 Web 服務,應用程式必須建立網路連線,並與服務通訊。接著,應用程式必須接收回應資料,並將其剖析為可供應用程式使用的格式。
  • Retrofit 程式庫是一個用戶端程式庫,可讓應用程式向 REST Web 服務發出要求。
  • 藉由轉換工具,可告知 Retrofit 該如何處理其與 Web 服務之間傳輸的資料。舉例來說,ScalarsConverter 會將 Web 服務資料視為 String 或其他原始檔案。
  • 如要讓應用程式連上網際網路,請在 Android 資訊清單中新增 "android.permission.INTERNET" 權限。
  • 延遲初始化功能會將物件建立作業委派給首次使用。這會建立參照,但不會建立物件。初次存取物件時,系統會在每次之後建立和使用參照。

JSON 剖析

  • Web 服務的回應通常會採用 JSON 格式,這是一種代表結構化資料的通用格式。
  • JSON 物件是鍵-值組合的集合。
  • 一組 JSON 物件稱為 JSON 陣列。您可以從 Web 服務取得 JSON 陣列做為回應。
  • 鍵/值組合的鍵前後會加上半形引號。值可以是數字或字串。
  • 在 Kotlin 中,資料序列化工具位於獨立的元件 kotlinx.Serialization 中。kotlinx.Serialization 提供了一組程式庫,可將 JSON 字串轉換為 Kotlin 物件。
  • retrofit2-kotlinx-serialization-converter 是由社群為 Retrofit 開發的 Kotlin 序列化轉換工具程式庫。
  • kotlinx.serialization 會比對 JSON 回應中的鍵與資料物件中的同名屬性。
  • 如要為某個鍵使用不同的屬性名稱,請為該屬性加上 @SerialName 註解和 JSON 鍵 value

11. 瞭解詳情

Android 開發人員說明文件:

Kotlin 說明文件:

其他: