安卓网络技术
最近在着手开发一款 App,学习了一点安卓网络通讯技术,觉得甚为精妙,所以有了这篇文章。
安卓系统原生的有一套 HttpURLConnection 进行网络通讯,正当我兴致勃勃准备埋头苦学之时,无意间了解到开源的 OkHttp,发现其封装的更简洁,更优雅,比安卓原生的 HttpURLConnection 有过之无不及,搭配网络库 Retrofit 一起使用,直接起飞。作为初学者,我自然更倾向简洁,优雅的技术。
OkHttp 和 Retrofit 是由大名鼎鼎的 Squeare 公司开发的,不仅在接口封装上简单易用,就连底层实现也是自成一派,已经成为了广大 Android 开发者首选的网络通信库。
OkHttp使用方法
在使用 OkHttp 之前,首先在项目中添加 OkHttp 的依赖,在 app/build.gradle 文件的dependencies闭包中添加以下内容
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
添加上述依赖会自动下载两个库:一个是 OkHttp 库,一个是 Okio 库。后者是前者的通信基础
使用 OkHttp 首先需要创建一个 OkHttpClient 的实例。
val client = OkHttpClient()
然后需要创建一个 Request 对象:
val request = Request.Builder().build()
上面的代码创建的是一个空的 Request 对象,并没有什么实际作用,可以在 build() 函数之前连缀其他方法来丰富这个 Request 对象。比如可以通过 url() 方法设置目标的网络地址:
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
之后调用 OkHttpClient 的 newCall() 方法来创建一个 Call 对象,并调用它的 execute() 方法来发送请求并获取服务器返回的数据,写法如下:
val response = client.newCall(request).execute()
Response 对象就是服务器返回的数据了,可以使用如下写法来得到返回的具体内容:
val responseData = response.body?.string()
如果发起的是一条 POST 请求,会比 GET 请求稍微复杂一点,我们需要先构建一个 Request Body 对象来存放待提交的参数,如下所示:
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build()
然后再 Request.Builder 中调用一下 post() 方法,并将 RequestBody 对象传入:
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody).build()
接下来的操作就和 GET 请求一样,调用 execute() 方法来发送请求并获取服务器返回的数据即可。
Retrofit使用方法
Retrofit 同样是一款有 Square 开发的网络库,但是它和 OkHttp 的定位完全不同,OkHttp侧重的是底层通信的实现,而 Retrofit 侧重的是上层接口封装。事实上,Retrofit 就是 Square 公司在 OKHttp 的基础上进一步开发出来的应用层网络通信库,使我们可以使用面向对象的思维进行网络操作。
浅谈一下 Retrofit 的设计思想吧。
同一款应用程序中所发起的网络请求绝大多数指向的是同一个服务器域名,另外,服务器提供的接口通常是可以根据功能来归类的。比如新增用户,修改用户数据,查询用户数据这几个接口就可以归为一类;上架图书,销售图书,查询可供销售图书这几个接口也可以归为一类。将服务器接口合理化归类能够让代码结构变得更加合理,从而提高可阅读性和可维护性。最后,开发者肯定更加习惯于“调用一个接口,获取它的返回值”这样的编码方式,其实大多数人并不关心网络的具体通信细节,但是传统网络库的用法却需要编写太多网络的相关代码。
Retrofit 就是基于以上几点来设计的:Retrofit允许配置一个根路劲,然后在指定服务器接口地址时只需要使用相对路劲即可。
Retrofit 允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义到同一个接口文件中,从而让代码结构变得更加合理。
最后,我们也不需要关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应的哪个服务器接口,以及需要提供哪些参数。当我们在程序中调用该方法时,Retrofit 会自动向对应的服务器接口发起请求,并将响应的数据解析成返回值声明的类型。
OK,基本思想就是这样吧。下面来学习一下到底如何使用。
首先需要在项目中添加依赖,在 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 库。
第二条依赖是 Retrofit 的转换库,借助 GSON 解析 JSON 数据。
ok,假如说现在我请求 http://www.example.com/get_json 这个接口会返回以下 json 数据。
{
"userid": "1111",
"username": "zhangsan"
}
借助 Retrofit 我们就可以这样发送网络请求。
首先建立一个 javabean (熟悉 java 的人知道,其实就是一个存储数据的类,不过这里使用 kotlin 来写)。
class User(
val userid: String,
val username: String
)
接下来的工作就非常简单了,只需要定义一个接口,并包含一个方法即可。
interface UserService {
@GET("get_json")
fun getUser(): Call<List<User>>
}
通常 Retrofit 的接口文件建议以具体的功能种类名开头,并以 Service 结尾,这是一个好的命名习惯。
上述代码需注意两点:
@GET() 注解:这个注解表示调用 getUser() 方法时 Retrofit 会发起 get 请求,请求的地址就是在注解中设置的参数。需要注意的是,括号中传入的是请求地址的相对路径,根路径稍后会设置。
返回值:getUser() 方法的返回值必须声明为 Retrofit 中内置的 Call 类型,并通过泛型来指定服务器响应的数据应该转换为什么对象。
class TestRetrofit {
fun main() {
val retrofit = Retrofit.Builder()
.baseUrl("http://www.example.com/") //设置根路径
.addConverterFactory(GsonConverterFactory.create()) //指定 Retrofit 解析数据时使用的转换库
.build()
val userService = retrofit.create(UserService::class.java) //创建一个动态代理对象。可以了解一下设计模式
userService.getUser().enqueue(object : Callback<List<User>> {
override fun onResponse(call: Call<List<User>>,
response: Response<List<User>>) {
val list = response.body()
if (list != null) {
for (user in list) {
print("userid is ${user.userid}")
print("username is ${user.username}")
}
}
}
override fun onFailure(call: Call<List<User>>, t: Throwable) {
print("failed")
}
})
}
}
enqueue() 方法会根据注解去进行响应的网络请求,服务器响应的数据会回调到 enqueue() 方法中传入的 Callback 实现里面。
当发起请求的时候,Retrofit 会自动在内部开启子线程,当数据回调到 Callback 中之后,Retrofit 又会切换会主线程,整个操作过程不需要考虑线程切换问题。
上面举例的是一个很简单的用法,更详细的使用方法可以参考 Retrofit 的文档。里面写的很详细。由于本文探讨的是安卓的网络技术使用,这里不再赘述。
Retrofit构建器的最佳写法
其实上面的写法很麻烦,按照上面的写法,每发起一次网络请求,我们就要获取一次 UserService 的动态代理对象,事实上,没有每次都写的必要,可以将 Retrofit 对象声明为全局通用的,只需要在调用 create() 方法时写入不同的 Service 接口对应的 Class 即可。
下面来看如何实现:
object ServiceCreator {
private const val BASE_URL = "http://www.example.com/get_json"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
// 泛型实化
inline fun <reified T> create(): T = create(T::class.java)
}
经过这样封装之后,Retrofit 的用法将会变得异常简单,比如我们想获取一个 UserService 接口的动态代理对象,只需要使用如下写法即可:
val userService = ServiceCreator.create(UserService::class.java)
之后就可以随意调用 UserService 接口中定义的任何方法了。
不过你看到,在 ServiceCreator 中还有一个不带参数的方法,这是泛型实化的应用。这样就可以更简洁的获取 UserService 的代理对象了。
val userService = ServiceCreator.create<UserService>()
利用kotlin的协程简化回调写法
kotlin 有个非常好的语言特性,协程。这里我介绍一个我学习的函数,可以简化上述写法。
按照上述写法,我们在写回调的时候会这样写:
val userService = ServiceCreator.create<AppService>()
userService.getUser().enqueue(object : Callback<List<App>> {
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
// 得到服务器返回的数据
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
// 在这里对异常情况进行处理
}
})
其实这样的回调写多了,也是会感觉非常繁琐的。
在 kotlin 中有一个 suspendCoroutine 函数,此函数必须在协程作用域或挂起函数中才能使用,它接收一个 Lambda 表达式参数,主要作用是讲当前协程立即挂起,然后在一个普通的线程中执行 Lambda 表达式中的代码。Lambda 表达式的参数列表会传入一个 Continuation 参数,调用它的 resume() 或者 resumeWithException() 可以让协程恢复执行。
我们会在 ServiceCreator 上面再封装一层,这一层会帮助我们请求 SerivceCreator 创建动态代理对象,然后发送网络请求,处理请求的结果。之后所有调用网络请求就直接调用这一层即可。
object SunnyWeatherNetwork {
// 使用 ServiceCreator 创建 PlaceService 的动态代理对象
private val userService = ServiceCreator.create(UserService::class.java)
// suspend: 将函数声明为挂起函数,挂起函数之间可以互相调用
suspend fun getUser(query: String) =
userService.getUser(query).await()
/**
* Call<T>.await() 表示将await声明为 Call<T> 类型的扩展函数,
* 任何返回值是Call类型的Retrofit网络请求接口都可以直接调用await()函数
*/
private suspend fun <T> Call<T>.await(): T {
//suspendCoroutine必须在协程作用域或者挂起函数中才能调用,接收一个lambda表达式参数
//作用是:将当前协程立即挂起,然后在一个普通线程中执行lambda中的代码
//lambda参数列表会传入一个Continuation参数,调用它的resume()或者resumeWithException()可以让协程恢复执行
return suspendCoroutine { continuation ->
// enqueue 函数会帮助我们开启子线程
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(
RuntimeException("response body is null")
)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
}
之后的处理就会非常简单。