Kotlin中的OkHttp

在开源盛行的今天,有许多出色的网络通信库可以替代原生的HttpURLConnection,而其中OkHttp无疑是做得最出色的一个。

在使用之前,需要在app/build.gradle文件中的dependencies闭包中添加如下内容:

dependencies {
    ...
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}

GET请求部分:

添加完成后,首先需要创建一个OkHttpClient的实例:

val client = OkHttpClient()

接下来如果要发起一个HTTP请求,则需要创建一个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()

REQUEST请求部分:

若发起的是一条REQUEST请求,则会比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方法来发送请求并获取服务器返回的数据即可。

解析XML格式数据

先下载一个apache,具体操作请搜索。

安装并配置成功后,在浏览器进入127.0.0.1的网址,若成功则说明服务器已启动成功。

接下来在Apache\htdocs目录下,新建一个get_data.xml的文件,内容如下:

<apps> 
    <app> 
        <id>1</id>
        <name>Google Maps</name> 
        <version>1.0</version> 
    </app> 
    <app> 
        <id>2</id> 
        <name>Chrome</name> 
        <version>2.1</version> 
    </app> 
    <app> 
        <id>3</id> 
        <name>Google Play</name> 
        <version>2.3</version> 
    </app> 
</apps>

此时访问127.0.0.1/get_data.xml网址,则会出现上面的内容。接下来让我们获取并解析这段xml。

Pull解析方式

解析xml的方式较为常用的有:Pull和SAX解析。

private fun sendRequestWithOkHttp(){
    thread {
        try {
            val client = OkHttpClient()//获取实例

            //编写请求,并在url中添加访问地址
            val request = Request.Builder()//
                    .url("http://10.0.2.2/get_data.xml")//10.0.2.2在虚拟机中代表本机
                    .build()

            //向服务器提交请求,使用newCall获取服务器返回的数据
            val response = client.newCall(request).execute()
            
            //解析返回的数据response 
            val responseData = response.body?.string()
            if (responseData != null) {
                //传入方法,解析xml
                parseXMLWithPull(responseData)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

private fun parseXMLWithPull(xmlData: String) {
    try {
        //创建一个XmlPullParserFactory实例
        val factory = XmlPullParserFactory.newInstance()

        //借助实例获得XmlPullParser对象
        val xmlPullParser = factory.newPullParser()

        //调用xmlPullParser的setInput方法,将返回的xml数据设置进去,进而解析
        xmlPullParser.setInput(StringReader(xmlData))

        //通过getEventType获取当前的解析事件
        var eventType = xmlPullParser.eventType
        var id = "" 
        var name = "" 
        var version = ""

        //如果当前的解析事件不等于END_DOCUMENT,说明解析未完成
        while (eventType != XmlPullParser.END_DOCUMENT) {

            //通过getName方法获取当前节点的名字
            val nodeName = xmlPullParser.name
            when (eventType) {
                //START_TAG:开始解析获取的节点
                XmlPullParser.START_TAG -> {
                    when (nodeName) {
                        //如果发现节点名等于id、name或version
                        //就调用nextText方法来获取节点内的具体内容
                        "id" -> id = xmlPullParser.nextText()
                        "name" -> name = xmlPullParser.nextText()
                        "version" -> version = xmlPullParser.nextText()
                    }
                }

                //END_TAG:完成解析某个节点
                XmlPullParser.END_TAG -> { 
                    if ("app" == nodeName) { 
                        Log.d("MainActivity", "id is $id") 
                        Log.d("MainActivity", "name is $name")
                        Log.d("MainActivity", "version is $version")
                    } 
                }
            //调用next方法,获取下一个解析事件
            eventType = xmlPullParser.next()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

从Andriod9.0开始,应用程序只允许使用https类型的网络请求,http因为有安全隐患默认不再被支持,而apache的就是http。

为了让程序使用http,需要进行如下配置:右击res->New->Directory新建一个xml目录,接着右击xml->New->File,创建一个network_config.xml文件,写入以下内容:

<?xml version="1.0" encoding="utf-8"?> 
<network-security-config> 
    <base-config cleartextTrafficPermitted="true"> 
        <trust-anchors> 
            <certificates src="system" /> 
        </trust-anchors> 
    </base-config> 
</network-security-config>

此配置文件的意思是,允许我们以明文的方式在网络上传输数据,而http使用的就是明文传输方式。接着修改AndroidManifest.xml如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.example.networktest"> 
    ... 
    <application 
    android:allowBackup="true" 
    android:icon="@mipmap/ic_launcher" 
    android:label="@string/app_name" 
    android:roundIcon="@mipmap/ic_launcher_round" 
    android:supportsRtl="true" 
    android:theme="@style/AppTheme" 
    android:networkSecurityConfig="@xml/network_config"> 
    ... 
    </application> 
</manifest>

SAX解析方式

使用SAX解析,通常会新建一个类继承自DefaultHandler,并重写父类的5个方法。

新建一个ContentHandler类继承DefaultHandler:

class ContentHandler : DefaultHandler() {
    
    private var nodeName = ""
    private lateinit var id: StringBulider
    private lateinit var name: StringBulider
    private lateinit var version: StringBulider

    //在开始xml解析的时候调用,在此处进行初始化
    override fun startDocument() {
        id = StringBuilder()
        name = StringBuilder()
        version = StringBuilder()
    }

    //在开始解析某个节点时调用
    override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { 
        //localName记录了当前节点名
        nodeName = localName 
        Log.d("ContentHandler", "uri is $uri") 
        Log.d("ContentHandler", "localName is $localName") 
        Log.d("ContentHandler", "qName is $qName") 
        Log.d("ContentHandler", "attributes is $attributes") 
    }

    //在获取节点中内容的时候调用
    //需要注意的是,在读取节点中的内容时,此方法可能会被调用多次
    override fun characters(ch: CharArray, start: Int, length: Int) {
        //根据当前节点名判断将内容添加到哪一个StringBuilder对象中
        when (nodeName) { 
            "id" -> id.append(ch, start, length) 
            "name" -> name.append(ch, start, length) 
            "version" -> version.append(ch, start, length) 
        } 
    } 

    //在完成解析某个节点的时候调用
    override fun endElement(uri: String, localName: String, qName: String) {
        if ("app" == localName) { 

            //此处的id、name和version中都可能包括回车或换行符
            //因此需要使用trim方法来去除
            Log.d("ContentHandler", "id is ${id.toString().trim()}")
            Log.d("ContentHandler", "name is ${name.toString().trim()}")
            Log.d("ContentHandler", "version is ${version.toString().trim()}")
            
            //最后要将StringBuilder清空,不然会影响下一次内容的读取
            id.setLength(0) 
            name.setLength(0) 
            version.setLength(0) 
        } 
    } 

    override fun endDocument() { }
}

在MainActivity中调用:

class MainActivity : AppCompatActivity() { 
     ... 
     private fun sendRequestWithOkHttp() { 
         thread { 
             try { 
                 val client = OkHttpClient() 
                 val request = Request.Builder() 
                 // 指定访问的服务器地址是计算机本机
                    .url("http://10.0.2.2/get_data.xml") 
                    .build() 
                 val response = client.newCall(request).execute()
                 val responseData = response.body?.string() 
                 if (responseData != null) { 
                     parseXMLWithSAX(responseData) 
                 } 
             } catch (e: Exception) { 
                 e.printStackTrace() 
             } 
         } 
     } 

     ... 
     private fun parseXMLWithSAX(xmlData: String) { 
         try { 
             //创建一个SAXParserFactory对象,以获取XMLReader对象
             val factory = SAXParserFactory.newInstance() 
             val xmlReader = factory.newSAXParser().XMLReader 

             //将ContentHandler的实例设置到XMLReader中          
             val handler = ContentHandler() 
             xmlReader.contentHandler = handler 

             // 开始执行解析
             xmlReader.parse(InputSource(StringReader(xmlData))) 
         } catch (e: Exception) { 
             e.printStackTrace() 
         } 
     } 
}

解析JSON格式数据

开始前,在Apache\htdocs目录中新建一个get_data.json,内容如下:

[{"id":"5","version":"5.5","name":"Clash of Clans"}, 
{"id":"6","version":"7.0","name":"Boom Beach"}, 
{"id":"7","version":"3.5","name":"Clash Royale"}]

此时访问http://127.0.0.1/get_data.jso,会出现上述内容。

类似的,解析json也有很多种方法,此处我们学习JSONObject和GSON。

JSONObject

class MainActicity : AppCompatActivity() {
    ...
    private fun sendRequestWithOkHttp() {
        thread {
            try {
                val client = OkHttpClient()
                val request = Request.Builder()
                    //10.0.2.2对于模拟器是本机的IP地址
                    .url("http://10.0.2.2/get_data.json")
                    .build()
                val response = client.newCall(request).execute()
                //newCall: 发送请求并获取服务器返回的数据
                val responseData = response.body?.string()
                if (responseData != null) {
                    parseJSONWithJSONObject(responseData)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
    ...
    private fun parseJSONWithJSONObject(jsonData: String) {
        try {
            //先将服务器返回的数据传入一个JSONArray对象中
            val jsonArray = JSONArray(jsonData)

            //循环遍历这个JSONArray对象,从中每取出一个元素都是一个JSONObject对象
            for (i in 0 until jsonArray.length()) {
                val jsonObject = jsonArray.getJSONObject(i)
                val id = jsonObject.getString("id")
                val name = jsonObject.getString("name")
                val version = jsonObject.getString("version")
                Log.e("MainActivity", "id is $id")
                Log.e("MainActivity", "name is $name")
                Log.e("MainActivity", "version is $version")
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

GSON

GSON并没有被添加到Android的官方API中,需要添加依赖。编辑app/build.gradle文件,在dependencies闭包中添加:

dependencies {
    ...
    implementation 'com.google.code.gson:gson:2.8.5'
}

GSON库的强大之处在于,它可以将一段JSON格式的字符串自动映射成一个对象,不需要我们再手动编写代码进行解析。

比如一段JSON格式的数据如下所示:

{"name":"Tom","age":20}

那我们可以定义一个Person类,并可以如name和age这两个字段,然后只需要调用如下代码就可以将JSON数据自动解析成一个Person对象了:

val gson = Gson()
val person = gson.fromJson(jsonData, Person::class.java)

若需要解析的是一个JSON数组,如下:

[{"name":"Tom","age":20}, {"name":"Jack","age":25}, {"name":"Lily","age":22}]

 此时,我们需要借助TypeToken将期望解析成的数据,传入fromJson方法中:

val typeOf = object : TypeToken<List<Person>>() {}.type
val people = gson.fromJson<List<Person>>(jsonData, typeOf)

进入实战。首先新增一个App类,并加入id、name和version3个字段:

class App(val id: String, val name: String, val version: String)

然后修改MainActivity:

class MainActivity : AppCompatActivity() {
    private fun sendRequestWithOkHttp() {
        thread {
            try {
                val client = OkHttpClient()
                val request = Request.Builder()
                        //10.0.2.2对于模拟器是本机的IP地址
                        .url("http://10.0.2.2/get_data.json")
                        .build()
                val response = client.newCall(request).execute()
                //newCall: 发送请求并获取服务器返回的数据
                val responseData = response.body?.string()
                if (responseData != null) {
                    parseJSONWithGSON(responseData)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    private fun parseJSONWithGSON(jsonData: String) {
        val gson = Gson()
        val typeOf = object : TypeToken<List<App>>() {}.type
        val appList = gson.fromJson<List<App>>(jsonData, typeOf)
        for (app in appList) {
            Log.e("MainActivity", "id is ${app.id}")
            Log.e("MainActivity", "name is ${app.name}")
            Log.e("MainActivity", "version is ${app.version}")
        }
    }
}

使用HTTP访问网络

工作原理特别简单,客户端向服务器发送一条http请求,服务器收到请求后返回数据给客户端,然后客户端解析并处理就可以了。

首先需要获取HttpURLConnection的实例,一般只需要创建一个URL对象,并传入目标的网络地址,然后调用openConnection方法打开这个地址:

val url = URL("https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection

在得到了HttpURLConnection的实例后,我们可以设置http请求所使用的方法,即获得(GET)或发送(POST)数据:

connection.requestMethod = "GET"

 接下来就可以进行一些比较自由的定制了,如设置连接超时、读取超时的毫秒数:

connection.connectionTimeout = 8000
connection.readTimeout = 8000

之后再调用getInputStream方法,就可以获取服务器返回的输入流,而最后的任务就是对这个输入流进行读取: 

val input = connection.inputStream

 最后调用disconnect方法来关闭我们打开的http链接:

connection.disconnect()

接着我们进入实战:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sendRequestBtn.setOnClickListener {
            sendRequestWithHttpURLConnection()
        }
    }

    private fun sendRequestWithHttpURLConnection() {
        // 开启线程发起网络请求
        thread {
            //获取HttpURLConnection实例
            var connection: HttpURLConnection? = null
            try {
                val response = StringBuilder()

                //创建一个URL对象,传入目标网址
                val url = URL("https://www.baidu.com")

                //调用openConnection访问目标网址
                connection = url.openConnection() as HttpURLConnection

                //自由定制:传入连接超时、读取超时的毫秒数
                connection.connectTimeout = 8000
                connection.readTimeout = 8000

                //获取服务器返回的输入流
                val input = connection.inputStream

                //对获取到的输入流进行读取
                val reader = BufferedReader(InputStreamReader(input))

                //使用use函数,能够保证调用者在执行完给定的操作后关闭资源
                //因此,use函数仅仅为Closeable的子类所定义使用,如Reader、Writer或Socket
                //由于在use代码块的结尾可以自动关闭bufferedReader,所以再次使用
                reader.use {
                    reader.forEachLine {
                        response.append(it)
                    }
                }
                showResponse(response.toString())
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                //关闭http连接
                connection?.disconnect()
            }
        }
    }

    private fun showResponse(response: String) {
        runOnUiThread {
            //ui操作是不允许在子线程进行操作的
            //因此在这里进行UI操作,将结果显示到界面上
            responseText.text = response
        }
    }
}

 在运行之前,需要声明网络权限:

<uses-permission android:name="android.permission.INTERNET" />

上面我们向服务器请求(GET)了数据,那如果我们要发送(POST)数据呢?只需要将http请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。

需要注意的是,每条发送的数据都要以键值对的形式存在,数据之间使用&符号隔开。 比如我们要向服务器提交用户名和密码:

connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writebytes("username=admin&password=123456")

网络请求回调的实现方式

因为一个app可能会在很多地方都使用到网络功能,而发送http请求的代码基本是相同的,如果我们每次都去编写一次发送http请求的代码,这显然非常差劲。

通常情况我们会使用工具类:

object HttpUtil {
    fun sendHttpRequest(address: String): String {
        var connection: HttpURLConnection? = null
        try {
            val response = StringBuilder()

            //创建URL对象,并传入函数接收的目标地址
            val url = URL(address)
            
            //调用openConnection访问目标网址
            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 {
            //关闭http连接
            connection?.disconnect()
        }
    }
}

此后发起http请求,就可以直接写成:

val address = "https://www.baidu.com"
val response = HttpUtil.sendHttpRequest(address)

需要注意的是,网络请求通常是耗时操作,而sendHttpRequest方法内部并没有开启线程,这有可能导致主线程被阻塞。

而这个问题的解决方式也不能是单纯的开启子线程,因为如果把所有的号是逻辑都放在子线程,那sendHttpRequest方法会在服务器还没来得及相应的时候就执行结束了,因此也无法返回服务器响应的数据。

解决该问题的方法在于使用回调机制。首先需要定义一个接口,我们将其命名为HttpCallbackListener:

interface HttpCallbackListener {
    fun onFinish(response: String)
    fun onError(e: Exception)
}

onFinish方法在服务器响应请求时调用,onError方法在网络操作出错时调用。

修改工具类代码:

object HttpUtil {
    //在sendHttpRequest方法中添加了HttpCallbackListener参数
    fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
        //开启子线程执行具体网络操作
        thread {
            var connection: HttpURLConnection? = null
            try {
                val response = StringBuilder()

                //创建URL对象,并传入函数接收的目标地址
                val url = URL(address)

                //调用openConnection访问目标网址
                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)
                    }
                }

                //回调onFinish方法
                listener.onFinish(response.toString())
            } catch (e: Exception) {
                e.printStackTrace()

                //onError
                listener.onError(e)
            } finally {
                //关闭http连接
                connection?.disconnect()
            }
        }
    }
}

在此我们舍弃了return语句,因为在子线程中是无法通过return来返回数据的,因此我们将服务器传回的数据传入了onFinish方法中。

现在的sendHttpRequest方法接受两个参数,因此我们在调用该方法的时候,还需要传入HttpCallbackListener的实例:

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
     override fun onFinish(response: String) { 
         // 得到服务器返回的具体内容
     } 
     override fun onError(e: Exception) { 
         // 在这里对异常情况进行处理
     } 
})

上述方法使用的是HttpURLConnection的写法,看起来比较复杂,而使用OkHttp会简单许多:

object HttpUtil {
    ...
    //此处传入了okhttp3.Callback,这个是OkHttp自带的回调接口
    fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
        val client = OkHttpClient()
        //编写向服务器发送的请求,通过url方法传入目标地址
        val request = Request.Builder()
                .url(address)
                .build()
        //此处没有像之前那样调用execute方法
        //而是调用了enqueue,并传入okhttp3.Callback参数
        client.newCall(request).enqueue(callback)
    }
}

在最终的“发送请求并获取服务器返回的地址”这一操作中,我们不再使用先前的execute方法,而是使用enqueue方法,这是因为OkHttp在该方法的内部已经帮我们开好了子线程,然后会在这个子线程中执行http请求,并将最终的请求结果回调到okhttp3.Callback中。

此后我们在调用sendOkHttpRequest方法时就可以这样写:

HttpUtil.sendOkHttpRequest(address, object : Callback { 
     override fun onResponse(call: Call, response: Response) { 
         // 得到服务器返回的具体内容
         val responseData = response.body?.string() 
     } 
     override fun onFailure(call: Call, e: IOException) { 
         // 在这里对异常情况进行处理
     } 
})

需要注意的是,无论是使用HttpURLConnection还是OkHttp,最终的回调接口都是在子线程中运行的,因此我们不能在这执行ui操作,除非借助runOnUiThread方法来进行线程转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值