目前你已经掌握了 HttpURLConnection 和 OkHttp 的用法,知道了如何发起 HTTP 请求,以及解析服务器返回的数据,但也许你还没有发现,之前我们的写法其实是很有问题的。因为ー个应用程序很可能会在许多地方都使用到网络功能,而发送 HTTP 请求的代码基本都是相同的,如果我们每次都去编写一遍发送 HTTP 请求的代码,这显然是非常差劲的做法。
没错,通常情况下我们都应该将这些通用的网络操作提取到一个公共的类里,并提供一个静方法,当想要发起网络请求的时候,只需简单地调用一下这个方法即可。比如使用如下的写法:
object HttpUtil {
fun sendHttpRequest(address:String):String{
var connection :HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
return response.toString()
}catch (e:Exception){
e.printStackTrace()
return e.message.toString()
}finally {
connection?.disconnect()
}
}
}
以后每当需要发起一条 HTTP 请求的时候就可以这样写:
val address = "https://www.baidu.com"
HttpUtil.sendHttpRequest(address)
在获取到服务器响应的数据后,我们就可以对它进行解析和处理了。但是需要注意,网络请求通常都是属于耗时操作,而 sendHttpRequest() 方法的内部并没有开启线程,这样就有可能导致在调用 sendHttpRequest() 方法的时候使得主线程被阻塞住。
你可能会说,很简单嘛,在 sendHttpRequest() 方法内部开启一个线程不就解决这个问题了吗?其实没有你想象中的那么容易,因为如果我们在 sendHttpRequest() 方法中开启了一个线程来发起 HTTP 请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest() 方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。
那么遇到这种情况时应该怎么办呢?其实解决方法并不难,只需要使用 Java 的回调机制就可以了,下面就让我们来学习一下回调机制到底是如何使用的。
首先需要定义一个接口,比如将它命名成 HttpCallbackListener,代码如下所示:
interface HttpCallbackListener {
fun onFinish(response:String)
fun onError(e:Exception)
}
可以看到,我们在接口中定义了两个方法,onFinish() 方法表示当服务器成功响应我们请求的时候调用,onError() 表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish() 方法中的参数代表着服务器返回的数据,而 onError()方法中的参数记录着错误的详细信息。
接着修改 HttpUtil 中的代码,如下所示:
object HttpUtil {
fun sendHttpRequest(address:String,listener: HttpCallbackListener){
thread {
var connection :HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
listener.onFinish(response.toString())
}catch (e:Exception){
e.printStackTrace()
listener.onError(e)
}finally {
connection?.disconnect()
}
}
}
}
我们首先给 sendHttpRequest()方法添加了一个 HttpCallbackListener 参数,并在方法的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。注意,子线程中是无法通过 return 语句来返回数据的,因此这里我们将服务器响应的数据传人了 Http CallbackListener 的 onFinish() 方法中,如果出现了异常就将异常原因传入到 onError()方法中。
现在 sendHttpRequest()方法接收两个参数了,因此我们在调用它的时候还需要将 HttpCallbackListener的实例传人,如下所示:
HttpUtil.sendHttpRequest(address,object :HttpCallbackListener{
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: java.lang.Exception) {
// 在这里对异常情况进行处理
}
})
这样的话,当服务器成功响应的时候,我们就可以在 onFinish() 方法里对响应数据进行处理了。类似地,如果出现了异常,就可以在 onError() 方法里对异常情况进行处理。如此一来我们就巧妙地利用回调机制将响应数据成功返回给调用方了。
不过你会发现,上述使用 HttpURLConnection 的写法总体来说还是比较复杂的,那么使用OkHttp 会变得简单吗?答案是肯定的,而且要简单得多,下面我们来具体看一下。在 Httputil 中加入一个 sendOkHttpRequest() 方法,如下所示:
fun sendOkHttpRequest(address: String,callback: okhttp3.Callback){
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)
}
可以看到 sendOkHttpRequest() 方法中有一个 okhttp3.Callback 参数,这个是 OkHttp 库中自带的一个回调接口,类似于我们刚オ自己编写的 HttpCallbackListener。然后在 client.newCall()之后没有像之前那样一直调用 execute() 方法,而是调用了一个 enqueue() 方法,并把 okhttp3.Callback 参数传入。相信聪明的你已经猜到了 OHttp 在enqueue ()方法的内部已经帮我们开好子线程了,然后会在子线程中去执行 HTTP 请求,并将最终的请求结果回调到 okhttp3.Callback 当中。
那么我们在调用 sendOkHttpRequest() 方法的时候就可以这样写:
val address = "https://www.baidu.com"
HttpUtil.sendOkHttpRequest(address,object :Callback{
override fun onFailure(call: Call, e: IOException) {
// 在这里对异常情况进行处理
}
override fun onResponse(call: Call, response: Response) {
// 得到服务器返回的具体内容
val responseData = response.body?.string()
}
})
由此可以看出,OkHttp 的接口设计得确实非常人性化,它将一些常用的功能进行了很好的封装,使得我们只需编写少量的代码就能完成较为复杂的网络操作。当然这并不是 OkHttp 的全部,后面我们还会继续学习它的其他相关知识。
另外需要注意的是,不管是使用 HttpURLConnection 还是 OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里执行任何的 UI 操作,除非借助 runOnUIThread() 方法来进行线程转换。至于具体的原因,我们很快就会在下一章中学刁到了。