原标题: 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
服务JSON
转换为Kotlin
数据类并返回Web
的线程Kotlin
中的单例模式咱们正在上一篇文章的基础上,咱们使用RecyclerView
建立了一个列表,而后添加了一个点击监听器。您能够下载入门项目。web
该案例是假设工厂的项目管理软件。但它是通用的。您能够轻松地根据须要调整代码 - 不管您是要建立待办事项列表,仍是从Web服务加载天气数据或高分列表。shell
测试咱们的应用程序的最简单方法是灵活的本地模拟Web服务器。完成Android代码后,您只需切换实时目标网址便可。可是使用本地服务器进行测试要容易得多,由于您能够彻底控制双方。数据库
建立本地Web服务的一种很好的方法是使用typicode的JSON 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
复制代码
当您打开终端中指定的URL时,您将看到服务器返回的JSON。请注意,在下面的屏幕截图中,它被Firefox解析并变得更漂亮; 但它固然与咱们提供的数据库文件彻底相同。可是,服务器甚至容许经过标准REST调用添加,更新和删除项目。db.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容许许多不一样的选项来访问Web服务。在普通的很是规Java 很容易理解,但到目前为止尚未强大和Web服务不够灵活。在Android世界中,一般使用两个库:
根据个人经验,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'
复制代码
对于本机应用程序,您最终须要一组数据对象,以便在UI中轻松显示它并与内容进行交互。JavaScript直接将JSON转换为类。但对于本机代码,咱们但愿得到更多控制权。应该在咱们的应用程序中预先定义类的确切结构,以便在从JSON转换期间,能够检查全部内容而且类型安全。
困难的部分是JSON和咱们本身的类之间的映射。例如,在某些状况下,您但愿调用属性的方式与JSON中项目的名称不一样。这就是转换器的用武之地。
Retrofit
有许多现成的转换器。两个最突出:
Moshi
的主要开发人员之一显然首先建立了Gson,但从那时起就离开了谷歌而且以为他想要建立一个新的转换器来解决Gson的一些很是低的问题,基本上须要重写。结果:莫西。
Moshi拥有出色的Kotlin支持以及编译时代码生成功能,可使应用程序更快更小。你能够 - 但你不须要在你的应用程序中添加一个大的通用库。因此让咱们试试Moshi吧。咱们以前添加的依赖项部分中的一行在编译期间触发代码生成。这里再次供参考:
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
复制代码
如何指示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实际上会添加一个额外的,自动生成的适配器类,为您处理全部事情。
映射JSON不足以访问Web服务。咱们还须要一种简单的方法将服务器的界面映射到Kotlin
函数。
此部分位于Web服务与应用程序其他部分之间的链接处。所以,它受处处理Web请求的异步性质的影响很大。像往常同样,您有多种选择。
一个常用的库叫作RxJava 2。Retrofit包括一些用于RxJava和其余的现成适配器。从本质上讲,目标始终是使异步调用比标准Java更容易。
咱们正在Kotlin写咱们的应用程序。虽然RxJava固然兼容,但Kotlin最近添加了一个使人兴奋的新功能:coroutines。它使异步编程成为一种本地语言特性 - 其语法与C#处理异步调用的方式有点相似。在我看来,Kotlin协程具备更大的灵活性,但在C#中有些零散优雅的async
/await
。
协同程序是一项很棒的功能,可让您的生活更轻松。我不会在这里详细介绍,咱们只会使用它们。使用协同程序,您的异步代码看起来几乎与同步代码相同。你不须要再写繁琐的回调了。您能够在Kotlin文档中阅读有关协同程序的更多信息。谷歌还提供了一个长期的Coroutine代码实验室。
在本文的前面部分中,咱们已经包含了Kotlin
依赖的协同程序扩展。Jake Warthon是最着名的Android开发者之一,他还为Kotlin 协程建立了一个Retrofit Call Adapter。它仍然是0.9.2版本,但我但愿这种方法成为在Kotlin中使用异步代码的将来。
在许多状况下,您只须要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服务器结果的神奇之处,而不会阻塞应用程序的其他部分。
其余三个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>
DELETE和PUT还有另外一个特色:它们须要在HTTP URL中删除/修改对象的ID。它在路径定义中标记。附加的@Path注释告诉库哪一个参数应该用于路径。
http://127.0.0.1/parts/123456
,DELETE为HTTP方法。http ://127.0.0.1/parts/123456
,PUT做为HTTP方法更改对象,新数据的JSON做为请求体。咱们的项目应该只有一个特定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代码。以后,它将返回建立的实例。咱们不须要为它编写任何代码。另外,它是线程安全的!
我还在上面的代码中添加了一条日志消息,以便您能够在应用程序运行时检查并查看此代码的执行时间。
这个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!
惟一剩下的任务是触发异步Web请求。让咱们从GET请求开始,从Web服务中检索项目列表。
为此,咱们使用Kotlin协程。关于协程如何工做的最好的介绍性文章之一是由Joffrey Bion撰写的。
咱们在经过Deferred
类型设置接口时使用了挂起功能。这意味着该函数将暂停,直到结果可用。咱们的应用程序代码的其他部分能够在此期间继续运行,应用程序将保持响应。
您能够从另外一个暂停功能中调用一个暂停功能。但在某些时候,你须要“桥接”到正常世界。咱们的UI界面监听器没有设置suspend关键字; 所以,它不能在函数中间暂停。
该解决方案是一个协同程序构建器。它建立一个新的协同程序并从正常功能启动它。你只须要知道上下文:协程属于谁?它应该绑定到父级,它应该在单独的线程中运行仍是在Android的UI线程中运行?
协程必须具备附加的范围。使用活动自己是有问题的:因为从新建立的活动,旋转屏幕会在正在运行的异步任务下拉开示波器。
最简单的解决方案是使用GlobalScope。这意味着即便咱们的活动被破坏,任务也能够继续。若是任务中出现错误而且它成为孤儿,这也多是一个问题。Kotlin文档包含如何确保在活动被销毁时取消做业的示例。Marko Topolnik在StackOverflow上发布了一个更具体的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响应到类实例列表的映射。
使用上面的代码,您的应用程序将处理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架构组件,并使用ViewModels和LiveData经过RetroFit访问Web服务。但这是一个不一样的故事
欢迎关注 Kotlin 中文社区!
中文官网:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公众号:Kotlin
知乎专栏:Kotlin
CSDN:Kotlin中文社区
掘金:Kotlin中文社区
简书:Kotlin中文社区