【譯】如何在 Android 中使用 Retrofit, Moshi, Coroutines & Recycler View

翻譯說明:

原標題: How-To: Retrofit, Moshi, Coroutines & Recycler View for REST Web Service Operations with Kotlin for Androidhtml

原文地址: www.andreasjakl.com/how-to-retr…java

原文做者: Andreas Jaklnode

Android應用程序中選擇訪問Web服務的最佳方式可能會使人難以招架。也許你想要的只是從Web服務解析JSON並將它顯示在Android上的Kotlin應用,同時仍然可使用像Retrofit這樣的庫來面向將來。做爲獎勵,若是您還能夠執行 CRUD,那就太棒了。android

您能夠從基本的Java風格的HTML請求中進行選擇,或者使用新的Android 架構組件 進行全面的 MVVM 設計模式。 根據您選擇的方法,您的源代碼看起來會徹底不一樣 - 所以在開始時作出正確的選擇很是重要。git

在本文中,我將展現使用許多最新組件的演示,以得到現代解決方案:github

案例:入門項目和Web服務

咱們正在上一篇文章的基礎上,咱們使用RecyclerView建立了一個列表,而後添加了一個點擊監聽器。您能夠下載入門項目。web

該案例是假設工廠的項目管理軟件。但它是通用的。您能夠輕鬆地根據須要調整代碼 - 不管您是要建立待辦事項列表,仍是從Web服務加載天氣數據或高分列表。shell

本地Web服務器

測試咱們的應用程序的最簡單方法是靈活的本地模擬Web服務器。完成Android代碼後,您只需切換實時目標網址便可。可是使用本地服務器進行測試要容易得多,由於您能夠徹底控制雙方。數據庫

建立本地Web服務的一種很好的方法是使用typicodeJSON Server項目。您將在幾分鐘內擁有一個徹底正常工做的模擬restful Web服務器。首先,確保你有Node.jsnpm

接下來,建立一個啓動JSON文件,服務器將其用做數據庫。將其命名爲db.json並將其存儲到空目錄中。

{
    "parts": [
        { 
            "id": 100411, 
            "itemName": "LED Green 568 nm, 5mm" 
        },
        { 
            "id": 101119, 
            "itemName": "Aluminium Capacitor 4.7μF" 
        },
        { 
            "id": 101624, 
            "itemName": "Potentiometer 500kΩ" 
        }   ,
        { 
            "id": 103532, 
            "itemName": "LED Red 630 nm, 7mm" 
        }
    ]
}

複製代碼

如今,使用命令行打開此目錄。鍵入如下內容以經過npm包管理器將json-server模塊安裝到共享位置。若是您使用管理員權限打開powershell窗口,它可能會有所幫助。

npm install -g json-server
複製代碼

最後,只需啓動服務器便可。做爲參數,指定剛剛建立的JSON文件。這將用做數據庫並定義restful服務器的CRUD操做的URL。

json-server --watch db.json
複製代碼

JSON服務器模塊已啓動並運行咱們的db.json文件,該文件定義數據以及默認的CRUD操做。

當您打開終端中指定的URL時,您將看到服務器返回的JSON。請注意,在下面的屏幕截圖中,它被Firefox解析並變得更漂亮; 但它固然與咱們提供的數據庫文件徹底相同。可是,服務器甚至容許經過標準REST調用添加,更新和刪除項目。db.json填充將始終相應更新。

從模擬Web服務器檢索完整列表爲JSON。

默認狀況下,您的Web服務器將運行localhost地址 - 若是您使用模擬器訪問服務器,這很好。要從同一本地網絡中的移動電話訪問它,請使用計算機的IP地址啓動json-server。首先,在Powershell窗口中使用ipconfig檢查您的地址。例如,您的計算機的本地IP多是10.2.1.205。而後,您將啓動服務器:

json-server --watch db.json --host 10.2.1.205
複製代碼

您能夠嘗試經過其Web瀏覽器和計算機的IP從手機訪問服務器。端口保持不變(默認爲3000)。

在Android中使用Retrofit訪問服務器

Android容許許多不一樣的選項來訪問Web服務。在普通的很是規Java 很容易理解,但到目前爲止尚未強大和Web服務不夠靈活。在Android世界中,一般使用兩個庫:

  • Volley by Google:你但願它成爲Android的「官方」網絡庫。在GitHub上,它已出演2000次左右。Apache 2.0許可證。
  • Retrofit by Square:使用相同的Apache 2.0許可證,它在GitHub上得到了31,000顆星。 二者在工做方式上都存在一些差別,二者都是不錯的選擇。你會發現不少關於哪一個庫更好的熱烈討論。

根據個人經驗,Retrofit彷佛在更普遍地使用。我爲本教程選擇Retrofit的主要緣由是:Google也在其最新架構組件的示例代碼中使用它。

準備您的應用程序:依賴關係

讓咱們的應用程序準備好使用Retrofit。首先,在應用程序模塊的build.gradle的插件列表末尾添加Kotlin-Kapt插件。Kapt是一個註釋預處理器。它容許咱們爲咱們的Kotlin數據類添加註釋,以幫助Moshi將代碼轉換爲JSON,反之亦然:

apply plugin: 'kotlin-kapt'
複製代碼

接下來,將所需的依賴項添加到app模塊的build.gradle。咱們將討論除了之後改造以外的全部其餘依賴項。

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation "com.squareup.retrofit2:converter-moshi:2.5.0"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"

// Moshi
implementation "com.squareup.moshi:moshi:1.8.0"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"

// Kotlin Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'

複製代碼

在JSON和Kotlin類之間轉換:Moshi

對於本機應用程序,您最終須要一組數據對象,以便在UI中輕鬆顯示它並與內容進行交互。JavaScript直接將JSON轉換爲類。但對於本機代碼,咱們但願得到更多控制權。應該在咱們的應用程序中預先定義類的確切結構,以便在從JSON轉換期間,能夠檢查全部內容而且類型安全。

困難的部分是JSON和咱們本身的類之間的映射。例如,在某些狀況下,您但願調用屬性的方式與JSON中項目的名稱不一樣。這就是轉換器的用武之地。

Retrofit有許多現成的轉換器。兩個最突出:

Moshi的主要開發人員之一顯然首先建立了Gson,但從那時起就離開了谷歌而且以爲他想要建立一個新的轉換器來解決Gson的一些很是低的問題,基本上須要重寫。結果:莫西。

Moshi擁有出色的Kotlin支持以及編譯時代碼生成功能,可使應用程序更快更小。你能夠 - 但你不須要在你的應用程序中添加一個大的通用庫。因此讓咱們試試Moshi吧。咱們以前添加的依賴項部分中的一行在編譯期間觸發代碼生成。這裏再次供參考:

kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
複製代碼

爲Moshi註釋Kotlin數據類

如何指示Moshi爲咱們的數據類自動生成適配器?只需在類聲明以前添加一個註釋。如下是存儲項目ID和項目名稱的完整數據類。

package com.andresjakl.partslist

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class PartData ( var id: Long, var itemName: String)

複製代碼

能夠添加進一步的註釋,例如,爲屬性提供與其JSON對應物不一樣的名稱。但爲簡單起見,咱們會堅持使用相同的名稱; 因此不須要任何進一步的映射。

這就是您從JSON映射到Kotlin所需的所有內容。當您編譯應用程序時,Moshi實際上會添加一個額外的,自動生成的適配器類,爲您處理全部事情。

客戶端API接口和調用適配器

映射JSON不足以訪問Web服務。咱們還須要一種簡單的方法將服務器的界面映射到Kotlin函數。

此部分位於Web服務與應用程序其他部分之間的鏈接處。所以,它受處處理Web請求的異步性質的影響很大。像往常同樣,您有多種選擇。

一個常用的庫叫作RxJava 2Retrofit包括一些用於RxJava和其餘的現成適配器。從本質上講,目標始終是使異步調用比標準Java更容易。

Kotlin 協程

咱們正在Kotlin寫咱們的應用程序。雖然RxJava固然兼容,但Kotlin最近添加了一個使人興奮的新功能:coroutines。它使異步編程成爲一種本地語言特性 - 其語法與C#處理異步調用的方式有點相似。在我看來,Kotlin協程具備更大的靈活性,但在C#中有些零散優雅的async/await

協同程序是一項很棒的功能,可讓您的生活更輕鬆。我不會在這裏詳細介紹,咱們只會使用它們。使用協同程序,您的異步代碼看起來幾乎與同步代碼相同。你不須要再寫繁瑣的回調了。您能夠在Kotlin文檔中閱讀有關協同程序的更多信息。谷歌還提供了一個長期的Coroutine代碼實驗室

在本文的前面部分中,咱們已經包含了Kotlin依賴的協同程序擴展。Jake Warthon是最着名的Android開發者之一,他還爲Kotlin 協程建立了一個Retrofit Call Adapter。它仍然是0.9.2版本,但我但願這種方法成爲在Kotlin中使用異步代碼的將來。

Retrofit Kotlin協程和客戶端API接口

在許多狀況下,您只須要HTTP GET操做。可是,在本文中,我想向您展現Web服務可能實現的全部四種可能的CRUD操做

將新文件添加到項目中,此次是類型接口。讓咱們分析四行代碼。

package com.andresjakl.partslist

import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.http.*

interface PartsApiClient {
    @GET("parts") fun getPartsAsync(): Deferred<Response<List<PartData>>>
    @POST("parts") fun addPartAsync(@Body newPart : PartData): Deferred<Response<Void>>
    @DELETE("parts/{id}") fun deletePartAsync(@Path("id") id: Long) : Deferred<Response<Void>>
    @PUT("parts/{id}") fun updatePartAsync(@Path("id") id: Long, @Body newPart: PartData) : Deferred<Response<Void>>
}

複製代碼

每行定義一個不一樣的操做:GET,POST,DELETE和PUT。這些中的每個都做爲普通的Kotlin函數提供。

對於從Web服務檢索數據的普通GET請求,咱們在函數定義前使用@GET註釋。註釋的參數表示Web服務的路徑。在這種狀況下,這意味着GET請求應映射到:http://127.0.0.1/parts。當調用該URL時,該服務但願得到一個JSON,其中包含Moshi須要將其轉換爲PartData類實例列表的全部數據。

延遲響應做爲函數返回變量

爲了分析函數的複雜返回值,咱們從內到外:

Deferred<Response<List>>

顯然,咱們但願磨石解析JSON並返回一個列表的PartData實例。這很簡單。

該列表包含在Response類中。這來自Retrofit,提供對服務器HTTP響應的徹底訪問權限。在大多數狀況下,這也很重要; 畢竟,您須要知道請求是否成功。

GET一般在其響應主體中返回JSON數據。DELETE等其餘函數一般不包含要解析的響應正文數據; 因此咱們須要查看HTTP響應標頭以查看請求是否成功。

外部類是延遲的。這來自Kotlin Coroutines。它定義了一個具備結果的做業。從本質上講,它是讓咱們的應用程序等待Web服務器結果的神奇之處,而不會阻塞應用程序的其他部分。

POST,DELETE和PUT請求

其餘三個CRUD操做的代碼是可比較的,一些細微的細節發生了變化。

@POST(「parts」) fun addPartAsync(@Body newPart : PartData): Deferred>

POST(添加一個新項目)還須要一個請求體:咱們發送給Web服務器的新項目的完整JSON。所以,該函數須要一個咱們能夠發送JSON的參數。莫西再次負責轉換; 因此咱們只須要使用Kotlin課程。所述@Body註釋能夠確保在HTTP請求的主體這個數據結束。咱們的測試服務器在其響應中不返回正文數據; 因此函數返回值是Void。

@DELETE(「parts/{id}」) fun deletePartAsync(@Path(「id」) id: Long) : Deferred>

@PUT(「parts/{id}」) fun updatePartAsync(@Path(「id」) id: Long, @Body newPart: PartData) : Deferred>

DELETEPUT還有另外一個特色:它們須要在HTTP URL中刪除/修改對象的ID。它在路徑定義中標記。附加的@Path註釋告訴庫哪一個參數應該用於路徑。

  • DELETE:生成的請求URL應爲:http://127.0.0.1/parts/123456,DELETE爲HTTP方法。
  • PUT(修改現有項)http ://127.0.0.1/parts/123456,PUT做爲HTTP方法更改對象,新數據的JSON做爲請求體。

Kotlin中的Retrofit單例

咱們的項目應該只有一個特定URL的Retrofit HTTP客戶端實例。這可確保Retrofit正確管理其與Web服務器的鏈接。所以,將Retrofit客戶端直接綁定到Activity是一個壞主意。特別是在Android的生命週期中,每次旋轉顯示時都會從新建立類。更好的方法是新的LiveData組件,它具備生命週期感知功能。

因爲咱們的Retrofit實例實際上不是LiveData的數據持有者,所以最好使用單例模式在第一次使用時爲整個應用程序建立單個Retrofit實例。這也使咱們可以從多個活動中訪問Web服務。

將另外一個新的Kotlin文件/類添加到項目中,而後選擇「Object」類型。要在Java中建立單例,您須要本身編寫相應的代碼。若是考慮多線程,很容易出錯。所以,Kotlin包含對相似native support for Singleton-like code。您可使用「object」定義它,而不是使用「class」關鍵字。

package com.andresjakl.partslist

import android.util.Log
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

// Singleton pattern in Kotlin: https://kotlinlang.org/docs/reference/object-declarations.html#object-declarations
object WebAccess {
    val partsApi : PartsApiClient by lazy {
        Log.d("WebAccess", "Creating retrofit client")
        val retrofit = Retrofit.Builder()
                // The 10.0.2.2 address routes request from the Android emulator
                // to the localhost / 127.0.0.1 of the host PC
                .baseUrl("http://10.0.2.2:3000/")
                // Moshi maps JSON to classes
                .addConverterFactory(MoshiConverterFactory.create())
                // The call adapter handles threads
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()

        // Create Retrofit client
        return@lazy retrofit.create(PartsApiClient::class.java)
    }
}

複製代碼

在這個類中,咱們只須要一個屬性:API客戶端的一個實例。經過在變量類型定義以後添加關鍵字「by lazy」,咱們告訴Kotlin它應該在類第一次嘗試訪問partsApi變量時執行如下lambda代碼。以後,它將返回建立的實例。咱們不須要爲它編寫任何代碼。另外,它是線程安全的!

我還在上面的代碼中添加了一條日誌消息,以便您能夠在應用程序運行時檢查並查看此代碼的執行時間。

構建Retrofit

這個lambda的主要代碼包含一個來自Retrofit構建器的大型函數調用。

首先,咱們添加Web服務的基本URL。目前,咱們將使用Google Android模擬器測試該應用。所以,在模擬器中,127.0.0.1指向模擬器自己。可是,咱們但願訪問在模擬器外部的OS中運行的Web服務。默認狀況下,模擬器將計算機的localhost映射到模擬器的幻數是10.0.2.2。正如您在建立咱們的JSON服務器時所記得的那樣,它正在端口3000上運行。

##轉換器和調用適配器 接下來,咱們告訴Retrofit使用哪一個轉換器和調用適配器。咱們已經將二者做爲依賴項包含在咱們的應用程序中。Moshi是咱們對Kotlin轉換器的JSON。Coroutine調用適配器應該負責管理異步流。

在lambda的最後一行,咱們讓Retrofit根據咱們的Web服務的映射接口建立本身。這就完成了用Kotlin單首創建Retrofit!

使用Kotlin協程改進GET請求

惟一剩下的任務是觸發異步Web請求。讓咱們從GET請求開始,從Web服務中檢索項目列表。

爲此,咱們使用Kotlin協程。關於協程如何工做的最好的介紹性文章之一是由Joffrey Bion撰寫的。

咱們在經過Deferred類型設置接口時使用了掛起功能。這意味着該函數將暫停,直到結果可用。咱們的應用程序代碼的其他部分能夠在此期間繼續運行,應用程序將保持響應。

您能夠從另外一個暫停功能中調用一個暫停功能。但在某些時候,你須要「橋接」到正常世界。咱們的UI界面監聽器沒有設置suspend關鍵字; 所以,它不能在函數中間暫停。

構建協程

該解決方案是一個協同程序構建器。它建立一個新的協同程序並從正常功能啓動它。你只須要知道上下文:協程屬於誰?它應該綁定到父級,它應該在單獨的線程中運行仍是在Android的UI線程中運行?

協程必須具備附加的範圍。使用活動自己是有問題的:因爲從新建立的活動,旋轉屏幕會在正在運行的異步任務下拉開示波器。

範圍和生命週期

最簡單的解決方案是使用GlobalScope。這意味着即便咱們的活動被破壞,任務也能夠繼續。若是任務中出現錯誤而且它成爲孤兒,這也多是一個問題。Kotlin文檔包含如何確保在活動被銷燬時取消做業的示例Marko TopolnikStackOverflow上發佈了一個更具體的Android示例。

所以,稍微好一點的解決方案是使用Android架構組件中的ViewModel。可是,因爲ViewModels須要對咱們的代碼進行更重大的更改,所以GlobalScope適用於咱們的簡單Web請求,而且能夠開始使用協同程序。

發起協程上下文

因此,讓咱們從一個函數啓動協同程序。首先,咱們使用coroutine builder。在這種狀況下,Dispatchers.Main會啓動一個新的協程,而不會阻塞當前線程。它返回對Job的引用,這將容許咱們取消正在運行的協同程序。咱們這裏不使用它。

做爲參數,咱們指定調度程序。Dispatchers.Main特定於Android Coroutines擴展。它在UI線程上運行咱們的代碼。這容許咱們從協程中更新UI。

class MainActivity : AppCompatActivity() {
    // Reference to the RecyclerView adapter
    private lateinit var adapter: PartAdapter
  
    private fun loadPartsAndUpdateList() {
        // Launch Kotlin Coroutine on Android's main thread GlobalScope.launch(Dispatchers.Main) { // Execute web request through coroutine call adapter & retrofit val webResponse = WebAccess.partsApi.getPartsAsync().await() if (webResponse.isSuccessful) { // Get the returned & parsed JSON from the web response. // Type specified explicitly here to make it clear that we already // get parsed contents. val partList : List<PartData>? = webResponse.body() Log.d(tag, partList?.toString()) // Assign the list to the recycler view. If partsList is null, // assign an empty list to the adapter. adapter.partItemList = partList ?: listOf() // Inform recycler view that data has changed. // Makes sure the view re-renders itself adapter.notifyDataSetChanged() } else { // Print error information to the console Log.d(tag, "Error ${webResponse.code()}") Toast.makeText(this@MainActivity, "Error ${webResponse.code()}", Toast.LENGTH_SHORT).show() } } } // For reference: shortened code of onCreate. See the full example on Github for // commented code. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // rv_parts is the recyclerview UI element in the XML file rv_parts.layoutManager = LinearLayoutManager(this) // Create the adapter for the recycler view, which manages the contained items adapter = PartAdapter(listOf(), { partItem : PartData -> partItemClicked(partItem) }) rv_parts.adapter = adapter // Start loading recycler view items from the web loadPartsAndUpdateList() } // ... } 複製代碼

隨着乘機加入到呼叫getPartsAsync() ,咱們將暫停拉姆達的執行,直到WebResponse的結果是,咱們不須要爲此編寫一個回調了!咱們的代碼簡潔明瞭。

請注意,咱們能夠切換到IO上下文以阻止此調用的網絡操做。這將確保網絡代碼不會在UI線程上執行。可是,彷佛底層庫已經解決了這個問題。不然,Android根本不容許咱們執行網絡呼叫。因此,咱們應該在Main調度程序上保留咱們本身的代碼。

接下來,咱們檢查Web請求是否成功。若是是,咱們獲取項目列表並將其分配給回收站視圖適配器。當咱們使用Moshi時,它已經爲咱們執行了JSON響應到類實例列表的映射。

網絡錯誤的IOException

使用上面的代碼,您的應用程序將處理Web服務器返回的錯誤。可是,對於更多基本錯誤,它仍然會崩潰。示例:您的Web服務器未運行,或者用戶沒有活動數據鏈接。

IOException會拋出這些類型的錯誤。使用try / catch環繞實際的Web服務調用,以通知用戶該問題。改進的函數代碼:

private fun loadPartsAndUpdateList() {
    GlobalScope.launch(Dispatchers.Main) {
        try {
            // Execute web request through coroutine call adapter & retrofit
            val webResponse = WebAccess.partsApi.getPartsAsync().await()

            if (webResponse.isSuccessful) {
                // Get the returned & parsed JSON from the web response.
                // Type specified explicitly here to make it clear that we already
                // get parsed contents.
                val partList: List<PartData>? = webResponse.body()
                Log.d(tag, partList?.toString())
                // Assign the list to the recycler view. If partsList is null,
                // assign an empty list to the adapter.
                adapter.partItemList = partList ?: listOf()
                // Inform recycler view that data has changed.
                // Makes sure the view re-renders itself
                adapter.notifyDataSetChanged()
            } else {
                // Print error information to the console
                Log.e(tag, "Error ${webResponse.code()}")
                Toast.makeText(this@MainActivity, "Error ${webResponse.code()}", Toast.LENGTH_LONG).show()
            }
        } catch (e: IOException) {
            // Error with network request
            Log.e(tag, "Exception " + e.printStackTrace())
            Toast.makeText(this@MainActivity, "Exception ${e.message}", Toast.LENGTH_LONG).show()
        }
    }
}

複製代碼

添加,更新和刪除操做

添加其餘三個CRUD操做是相似的。您只需確保提供咱們指定的接口的正確參數。如下是一些觸發這些操做的簡單函數:

private fun addPart(partItem: PartData) {
    GlobalScope.launch(Dispatchers.Main) {
        val webResponse = WebAccess.partsApi.addPartAsync(partItem).await()
        Log.d(tag, "Add success: ${webResponse.isSuccessful}")
        // TODO: Re-load list for the recycler view
    }
}

private fun deletePart(itemId : Long) {
    GlobalScope.launch(Dispatchers.Main) {
        val webResponse = WebAccess.partsApi.deletePartAsync(itemId).await()
        Log.d(tag, "Delete success: ${webResponse.isSuccessful}")
    }
}

private fun updatePart(originalItemId: Long, newItem: PartData) {
    GlobalScope.launch(Dispatchers.Main) {
        val webResponse = WebAccess.partsApi.updatePartAsync(originalItemId, newItem).await()
        Log.d(tag, "Update success: ${webResponse.isSuccessful}")
    }
}

複製代碼

結束思考和更多信息

雖然您須要瞭解不少概念,但優雅訪問Web服務的實際代碼量卻不多。考慮一下你得到的東西:一個適用於任何Web服務的徹底可銷售的流程。因爲RecyclerView的效率,您能夠無限地加載物品。

您能夠從GitHub下載完成的示例代碼。請注意,它配置爲使用在本文開頭建立的本地測試服務器在模擬器中運行。要使用真實服務器運行它,請更新WebAccess.kt中的IP地址。

如開頭所述,有許多替代方法能夠實現此方案。Okta發佈了另外一個很好的例子,它使用RxJava和Gson代替Kotlin Coroutines和Moshi。固然,您也可使用新的Android架構組件,並使用ViewModelsLiveData經過RetroFit訪問Web服務。但這是一個不一樣的故事

歡迎關注 Kotlin 中文社區!

中文官網:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公衆號:Kotlin

知乎專欄:Kotlin

CSDN:Kotlin中文社區

掘金:Kotlin中文社區

簡書:Kotlin中文社區

相關文章
相關標籤/搜索