既然我们这一章讲解Android 网络技术,那么就不得不提到Retrofit ,因为它实在是太好用了。Retrofit,同样是一款由Square 公司开发的网络库,但是它和OkHttp 的定位完全不同。OkHttp 侧重的是底层通信的实现,而Retrofit 侧重的是上层接口的封装。事实上,Retrofit 就是Square 公司在OkHttp 的基础上进一步开发出来的应用层网络通信库,是的我们可以用更加面向对象的思维进行网络操作。Retrofit 的项目主页地址是:https://github.com/square/retrofit。
那么本节我们就来学习一下Retrofit 的用法:新建一个RetrofitTest 项目,然后马上开始吧。
11.6.1 Retrofit 的基本用法
首先我想读一读 Retrofit 的基本设计思想。Retrofit 的设计基于以下几个事实。
同一款应用程序中所发起的网络请求绝大多数指向的是同一个服务器域名。这个很好理解,因为任何公司的产品和服务器都是配套的,很难想象一个客户端一会去这个服务器获取数据,一会又要去另一个服务器获取数据吧?
另外,服务器提供的接口通常是可以根据功能来归类的。比如新增用户、修改用户数据。查询用户数据这几个接口就可以归为一类,上架新书、销售图书、查询可供销售图书这几个接口也可以归为一类。将服务器接口合理归类能够让代码结构变得更加合理,从而提高可阅读性和可维护性。
最后,开发者肯定更加习惯于“调用一个接口,获取它的返回值”这样的编码方式,但上调用的是服务器接口时,却很难想象该如何使用这样的编码方式。其实大多数人并不关心网络的具体通信细节,但是传统我网络库的用法却需要编写太多网络相关的代码。
而Retrofit 的用法就是基于以上几点来设计的,首先我们可以配置好一个根路径,然后在指定服务器接口地址时只需要使用相对路径即可,这样就不用每次都指定完整的URL 地址了。
另外,Retrofit 允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件当中,从而让代码结构变得更加合理。
最后,我们也完全不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们在程序中调该方法时,Retrofit 会自动向对应的服务器接口发起请求,并将相应的数据解析成返回值声明的类型。这就是的我们可以更加面向对象的思维来进行网络操作。
Retrofit 的基本设计思想差不多就是这些,下面就让我们通过一个具体的例子来快速体验一下Retrofit 的用法。
想要使用Retrofit ,我们需要先在项目中添加必要的依赖库。编辑app/build.gradle 文件,在dependencies 闭包中添加如下内容:
dependencies {
...
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}
由于Retrofit 是基于OKHttp 开发的,因此添加上诉第一条依赖会自动将Retrofit 、OkHttp 和Okio 这几个库一起下载,我们无序再动手引入OkHttp 库。另外,Retrofit 还将服务器返回的JSON 数据自动解析成对象,因此上诉第二条依赖就是一个Retrofit 的转换库,它是借助GSON 来解析JSON 数据的,所以会自动将GSON 库一起下载下来,这样我们也不用手动引入GSON 库了。除了GSON之外,Retrofit 还支持各种其他主流的JSON 解析库,包括Jackson,Moshi 等,不过毫无疑问GSON 是最常用的。
这里我们打算继续使用11.4 接提供的JSON 数据接口,由于Retrofit 会借助GSON 将JSON 数据转换成对象,因此这里同样需要新增一个App 类,并加入id、name 和 version 这3个字段,如下所示:
class App (val id:String,val name:String,val version:String)
接下来,我们可以根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中定义对应具体服务器接口的方法。不过由于我们的Apache 服务器上其实只有一个获取JSON 数据的接口,因此这里只需要定义一个接口文件,并包含一个方法即可。新建AppService 接口,代码如下所示:
interface AppService {
@GET("get_data.json")
fun getAppData():Call>
}
通常Retrofit 的接口文件建议以具体的功能种类名开头,并以Service 结尾,这是一种比较好的命名习惯。
上诉代码中有两点需要我们注意。第一就是getAppData() 方法上面添加的注解,这里使用了一个@GET 注解,表示当调用getAppData() 方法时Retrofit 会发起一条GET 请求,请求的地址就是我们在@GET 注解中传入的具体参数。注意,这里只需要传入请求地址的相对路径即可,根路径我们会在稍后设置。
第二就是getAppData() 方法的返回值必须声明成Retrofit 中内置的Call 类型,并通过泛型来指定服务器响应数据应该转换成什么对象。由于服务器响应的是一个包含App数据的JSON 数组,因此这里我们将泛型声明成List
定义好了AppService 接口之后,接下来的问题就是该如何使用它。为了方便测试,我们还得在界面上添加一个按钮才行。修改activity_main.xml 中的代码,如下所示:
很简单的,这里在布局文件中增加了一个Button 控件,我们在它的点击事件中处理具体的网络请求逻辑即可。
现在修改MainActivity 中的代码,如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
appService.getAppData().enqueue(object : Callback> {
override fun onFailure(call: Call>, t: Throwable) {
t.printStackTrace()
}
override fun onResponse(call: Call>, response: Response>) {
val list = response.body()
if (list != null){
for (app in list){
Log.d("MainActivity","id is ${app.id}")
Log.d("MainActivity","name is ${app.name}")
Log.d("MainActivity","version is ${app.version}")
}
}
}
})
}
}
}
可以看到,在“Get App Data” 按钮的点击事件当中,首先使用了Retrofit.Builder 来构建一个Retrofit 对象,其中base.Url() 方法用于指定所有Retrofit 请求的跟路径,addConverterFactory() 方法用于指定Retrofit 在解析数据时所使用的转换库,这里指定成 GsonConverterFactory。 注意这两个方法都是必须调用的。
用了Retroift 对象之后,我们就可以调用它的create() 方法,并传入具体Service 接口所对应的Class 类型,创建一个该接口的动态代理对象。如果你并不熟悉什么是动态代理也没有关系,你只需要知道有了动态代理对象之后,我们就可以随意调用接口中定义的所有方法,而Retrofit 会自动执行具体的处理就可以了。
对应到上诉的代码当中,当调用了AppService 的 getAppData() 方法时,会返回一个Call> 对象,这时我们再调用一下她的enqueue() 方法,Retrofit 就会根据注解中配置的服务器接口地址去进行网络请求了,服务器响应的数据会回调到enqueue() 方法中传入的Callback 实现里面。需要注意的是,当发起请求的时候,Retrofit 会自动在内部开启子线程,当数据返回到Callback 中之后,Retrofit 又会自动切换回主线程,整个操作过程中我们都不用考虑线程切换的问题。在Callback 的onResponse() 方法中,调用response.body() 方法将会得到Retrofit 解析后的对象,也就是List
接下来就可以进行一下测试了,不过由于这里使用的服务器接口仍然是HTTP,因此我们还要按照11.3.1 小节所出的步骤来进行网络安全配置才行。先从NetworkTest 项目中复制network_config.xml 文件到RetrofitTest 项目当中,然后修改Androidmanifest.xml 中的代码,如下所示:
这里设置了允许使用铭文的方式来进行网络请求,同时声明了网络权限。现在运行RetroiftTest 项目,然后点击“Get App Data” 按钮,观察Logcat 的打印日志,如图:
可以看到,服务器响应的数据已经被成功解析出来了,说明我们编写的代码确实已经正常工作了。
以上就是使用Retrofit进行网络操作的基本用法。虽然本小节中我们便的示例程序非常简单,但其实这些都是Retrofit 用法中最常用且最这要的部分。在了解了基本用法之后,接下来我们就可以去学习一些细节方面的知识了。
11.6.2 处理复杂的接口地址类型
在上一小节中,我们通过示例向一个非常简单的服务器接口地址发送请求: http://10.0.2.2/get_data.json ,然而在真实的开发环境当中,服务器所提供的接口地址不可能的一直如此简单。如果你在使用浏览器上网观察一下浏览器上的网址,你会发现这些网址可能会是千变万化的,那么本小节我们就来学习一下如何使用Retrofit 来应对这些千变万化的情况。
为了方便距离,这里先定义一个Data ,并包含id 和 content 这两个字段,如下所示:
class Data (val id:String,val content:String)
然后我们先从最简单的看起,比如服务器的接口地址如下所示:
GET http://example.com/get_data.json
这是最简单的一种情况,接口地址是静态的,永远不会改变,那么对应到Retrofit 当中,使用如下的写法即可:
interface ExampleService {
@GET("get_data.json")
fun getData():Callback
}
这也是我们在上一小节中已经学过的部分,理解起来应该非常简单吧?
但是显然服务器不可能的总是给我们提供静态类型的接口,在很多场景下,接口地址中的部分内容可能会是动态变化的,比如如下的接口地址:
GET http://example.com/
这个接口当中,
interface ExampleService {
@GET("{page}/get_data.json")
fun getData(@Path("page") page:Int):Callback
}
在@GET 注解指定接口的地址当中,这里使用了一个{page} 的占位符,然后有在getData() 方法中添加了一个page 参数,并使用@Path("page") 逐渐哦来声明这个参数。这样当调用getData() 方法发情请求时,Retrofit 就会自动将page 参数值替换到占位符的位置,从而组成一个合法的请求地址。
另外,很多服务器接口还会要求我们传入一系列的参数,如下所示:
GET http://example.com/get_data.json?u=
这是另一种标准的带参数GET 请求的格式。接口地址的最后使用问号来连接参数部分,每个参数都是一个使用等号连接的键值对,多个参数之间使用”&“ 符号进行分隔。那么很显然,在上诉地址中,服务器要求我们传入user 和 token 这两个参数的值。对于这种格式的服务器接口,我们可以使用钢材所学的@Path 注解的方式来解决,但是这样会有些麻烦,Retrofit 针对这种带参入的GET 请求,专门提供了一种语法支持。
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user:String,@Query("t") token:String):Callback
}
这里getData() 方法中添加了user 和 token 这两个参数,并使用@Query 注解 对它们进行声明。这样当发起网络请求的时候,Retrofit 就会自动按照参数GET 请求的格式将这两个参数构建到请求地址当中。
学习了以上内容之后,现在你在一定程度上已经可以应对千变万化的服务器接口地址了。不过HTTP 并不是只有GET 请求一种类型,而是有很多种,其中比较常用的有GET、POST、PUT、PATCH、DELETE 这几种。它们之间的分工也很明确,简单概括的话,
GET 请求用于从服务器获取数据;
POST 请求用于向服务器提交数据;
PUT 和PATCH 请求用于修改服务器上的数据;
DELETE 请求用于删除服务器上的数据;
而Retrofit 对所有常用的HTTP 请求类型都进行了支持,使用@GET、@POST、@PUT、@PATCH、@DELETE 注解,就可以让Retrofit 发出相应类型的请求了。
比如服务器提供了如下接口地址:
DELETE http://exmaple.com/data/
这种接口通常意味着要根据id 删除一条指定的数据,而我们在Retrofit 当中想要发出这种请求就可以这样写:
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id")id:String):Call
}
这里使用了@DELETE 注解来发出DELETE 类型的请求,并使用了@PATH 注解来动态指定id ,这些都很好理解。但是返回值声明的时候,我们将Call 的泛型指定成了ResponseBody ,这是什么意思呢?
由于POST、PUT、PATCH、DELETE、 这几种请求类型与GET 请求不同,它们更多是用于操作服务器上的数据,而不是获取服务器上的数据,所以通常它们对于服务器的响应数据并不关心。这个时候就可以使用ResponseBody ,表示Retrofit 能够接收任意类型的响应数据,并不会对响应数据进行解析。
那么如果我们需要向服务器提交数据该怎么写呢?比如如下的接口地址:
POST http://example.com/data/create
{"id":1,"content":"The description for this data."}
使用POST 请求来提交数据,需要将数据放到HTTP 的Body 部分,这个功能在Retrofit 中可以借助@Body 注解来完成:
interface ExampleService {
@POST("data/create")
fun createData(@Body data:Data):Call
}
可以看到,这里我们在createData() 方法中声明了一个Data 类型的参数,并给它加上了@Body 注解。这样当Retrofit 发出POST 请求时,就会自动将Data 对象中的数据转换成JSON 格式的文本,并放到 HTTP 请求的Body 部分,服务器在收到请求之后只需要从Body 中将这部分数据解析出来即可。这种写法同样也可以用来给PUT、PATCH、DELETE 类型的请求提交数据。
最后,有些服务器接口还可能会要求我们在HTTP 请求的header 中指定参数,比如:
GET http://example.com/get_data.json
User-Agent:okhttp
Cache-Control : max-age = 0
这些header 参数其实就是一个个的键值对,我们可以在Retrofit 中直接使用@Headers 注解来对它们进行声明:
interface ExampleService {
@Headers("User-Agent : okhttp","Cache-Control : max - age = 0")
@GET("get_data.json")
fun getData():Call
}
但是这种写法只能进行静态header 声明,如果想要动态指定header 的值,则需要使用@Header 注解,如下所示:
interface ExampleService {
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent:String,@Header("Cache-Control") cacheControl:String):Call
}
现在当发起网络请求的时候,Retrofit 就会自动将参数传入的值设置到User-Agent 和 Cache-Control 这两个header 当中,从而实现了动态指定header 值的功能。
好了,这样我们就将使用Retrofit 处理复杂接口地址类型的内容基本学习完了,现在不管服务器给你提供什么样类型的接口,相信你都可以从容面对了吧?
11.6.3 Retrofit 构建器的最佳写法
学到这里,其实还有一个问题我们没有正视过,就是获取Service 接口的动态代理对象实在是太麻烦了。先回顾一下之前的写法吧,大致代码如下所示:
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
我们想要得到AppService 的动态代理对象,需要先使用Retrofit.Builder 构建出一个Retrofit 对象,然后再调用Retrofit 对象的create() 方法创建动态代理的对象。如果只是写一次还好,每次调用任何服务器接口时都要这样写一遍的话,肯定没有人能受得了。
事实上,确实也没有每次都写一遍的必要,因为构建出的Retrofit 对象是全局通用的,只需要在调用create() 方法时针对不用的Service 传入响应的Class 类型即可。因此,我们可以将通用部分功能封装起来,而从简化获取Service 接口动态代理对象的过程。
新建一个ServiceCreator 单例类,代码如下所示:
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun create(serviceClass:Class) : T = retrofit.create(serviceClass)
}
这里我们使用object 关键字让ServiceCreator 成为了一个单例类,并在它的内部定义了一个BASE_URL 常量,用于指定Retrofit 的根路径。然后同样是在内部使用Retrofit.Builder 构建一个Retrofit 对象,注意这些都是用private 修饰符来声明的,相当于对外部而言它们都是不可见的。
最后,我们提供了一个外部可见的create() 方法,并接收一个Class 类型的参数。当在外部调用这个方法时,实际上就是调用了Retrofit 对象的create() 方法,从而创建出相应Service 接口的动态代理对象。
经过这样的封装之后,Retrofit 的用法将会变得异常简单,比如我们想获取一个AppService 接口的动态代理对象,只需要使用如下写法即可:
val appService = ServiceCreator.create(AppService::class.java)
之后就可以随意调用AppService 接口中定义的任何方法了。
不过上诉代码其实仍然还有优化空间,还记得我们在上一章的Kotlin 课堂中学习的泛型实例化功能吗?这里立马就可以应用起来了。修改ServiceCreator 中的代码,如下所示:
inline fun create() = create(T::class.java)
可以看到,我们又定义了一个不带参数的create() 方法,并使用inline 关键字来修饰方法,使用reified 关键字来修饰泛型,这是泛型实化的两大前提条件。接下来就可以使用T::class.java 这种语法了,这里调用刚才定义的带有Class 参数的create() 方法即可。
那么现在我们就又有了一种新的方式获取AppService 接口的动态代理对象,如下所示:
val appService = ServiceCreator.create()
代码是不是变得更加简洁了?
好了,关羽Retrofit 的使用就先讲到这里,我们会在第15章的实战环节学习如何在实际的项目当中应用Retrofit。那么接下来,又该进入本章的Kotlin 课堂了,这次我们来学习一项特别神奇的技术 ----- 携程。