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

5,157 阅读19分钟

翻译说明:

原标题: How-To: Retrofit, Moshi, Coroutines & Recycler View for REST Web Service Operations with Kotlin for Android

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

原文作者: Andreas Jakl

Android应用程序中选择访问Web服务的最佳方式可能会令人难以招架。也许你想要的只是从Web服务解析JSON并将它显示在Android上的Kotlin应用,同时仍然可以使用像Retrofit这样的库来面向未来。作为奖励,如果您还可以执行 CRUD,那就太棒了。

您可以从基本的Java风格的HTML请求中进行选择,或者使用新的Android 架构组件 进行全面的 MVVM 设计模式。 根据您选择的方法,您的源代码看起来会完全不同 - 因此在开始时做出正确的选择非常重要。

在本文中,我将展示使用许多最新组件的演示,以获得现代解决方案:

案例:入门项目和Web服务

我们正在上一篇文章的基础上,我们使用RecyclerView创建了一个列表,然后添加了一个点击监听器。您可以下载入门项目。

该案例是假设工厂的项目管理软件。但它是通用的。您可以轻松地根据需要调整代码 - 无论您是要创建待办事项列表,还是从Web服务加载天气数据或高分列表。

本地Web服务器

测试我们的应用程序的最简单方法是灵活的本地模拟Web服务器。完成Android代码后,您只需切换实时目标网址即可。但是使用本地服务器进行测试要容易得多,因为您可以完全控制双方。

创建本地Web服务的一种很好的方法是使用typicodeJSON Server项目。您将在几分钟内拥有一个完全正常工作的模拟restful Web服务器。首先,确保你有Node.js

接下来,创建一个启动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中文社区