《Android》Chap.11 网络技术

WebView的用法

Android中的WebView控件可以实现在应用程序中嵌入一个浏览器,从而展示各种各样的网络
首先在activity_main.xml文件中加入WebView控件

<WebView
    android:id="@+id/webView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

其次在MainActivity中设置浏览器属性

class MainActivity : AppCompatActivity() {

    private lateinit var mainBinding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        mainBinding.webView.settings.javaScriptEnabled = true
        mainBinding.webView.webViewClient = WebViewClient()
        mainBinding.webView.loadUrl("http://www.csdn.net")
    }
}

最后,在AndroidManifest.xml文件中申请权限
在这里插入图片描述
其中
红色箭头所指的语句用于申请访问网络的权限声明。
紫色箭头所指的语句用于改正由于接口问题产生的错误。(参考)
Android9.0系统程序默认只能使用HTTPS类型的网络请求,要使用HTTP类型的网络请求需配置

运行后就能看到展示的浏览器
在这里插入图片描述

使用HTTP访问网络

简述HTTP的工作原理:客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理。
下面尝试手动发送HTTP请求

使用HttpURLConnection

首先在xml文件中添加一个Button和一个ScrollView控件

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NetworkActivity">

    <Button
        android:id="@+id/sendRequestBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="send request"
        app:layout_constraintTop_toTopOf="parent"/>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50dp"
        app:layout_constraintTop_toBottomOf="@id/sendRequestBtn">

        <TextView
            android:id="@+id/responseText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </ScrollView>
    
</androidx.constraintlayout.widget.ConstraintLayout>

然后在Avtivity中设置发送请求和读取操作

class NetworkActivity : AppCompatActivity() {

    private lateinit var networkBinding: ActivityNetworkBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        networkBinding = ActivityNetworkBinding.inflate(layoutInflater)
        setContentView(networkBinding.root)

        networkBinding.sendRequestBtn.setOnClickListener {
            sendRequestWithHttpURLConnection()
        }
    }

    private fun sendRequestWithHttpURLConnection(){
        //开启线程发起网络请求
        thread {
            var connection : HttpURLConnection ? = null
            try {
                val response = StringBuilder()
                val url = URL("https://www.baidu.com") //创建URL
                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)
                    }
                }
                showResponse(response.toString())
            }catch (e:Exception){
                e.printStackTrace()
            }finally {
                connection?.disconnect() //关闭HTTP链接
            }
        }
    }

    private fun showResponse(response: String){
        runOnUiThread {
            //在这里进行UI操作,结果显示在界面上
            networkBinding.responseText.text = response
        }
    }
}

运行后点击按钮展示服务器返回的数据
在这里插入图片描述

使用OkHttp

OkHttp是众多开源的网络信息库中较为出色的一个
在使用之前需要先在OkHttp的项目主页查看新最新版本号添加库依赖
用OkHttp的方式实现刚刚的功能:

private fun sendRequestWithOkHttp(){
    thread { 
        try {
            val client = OkHttpClient() //创建OkHttpClient实例
            val request = Request.Builder() //创建Request对象
                .url("https://www.baidu.com") //设计目标的网络地址
                .build()
            //调用新建call对象的execute()方法发送请求并获取服务器返回数据
            val response = client.newCall(request).execute() 
            val responseData = response.body?.string() //得到返回数据
            if (responseData != null){
                showResponse(responseData)
            }
        }catch (e: Exception){
            e.printStackTrace()
        }
    }
}

在点击事件中调用这个函数就可以得到跟刚刚相同的结果

解析XML格式数据

安装配置apache

跳转参考文章 成功的界面如下
请添加图片描述

pull解析方式

从XML格式的数据中解析出我们想要得到的那部分内容。
先提前准备好一个用于测试的xml文件(文件放在网站根目录下)
在这里插入图片描述
然后在刚刚的NetworkActivity添加并修改一部分代码

private fun sendRequestWithOkHttp(){
    thread {
        try {
            val client = OkHttpClient() //创建OkHttpClient实例
            val request = Request.Builder() //创建Request对象
                .url("http://10.0.2.2:88/get_data.xml") //设计目标的网络地址
                .build()
            //调用新建call对象的execute()方法发送请求并获取服务器返回数据
            val response = client.newCall(request).execute()
            val responseData = response.body?.string() //得到返回数据
            if (responseData != null){
                parseXMLWithPull(responseData)
            }
        }catch (e: Exception){
            e.printStackTrace()
        }
    }
}

private fun parseXMLWithPull(xmlData: String){
    try{
    	//准备工作
        val factory = XmlPullParserFactory.newInstance() //创建XmlPullParserFactory实例
        val xmlPullParser = factory.newPullParser() //得到XmlPullParser对象
        xmlPullParser.setInput(StringReader(xmlData)) //放入从服务器返回的xml数据
        //开始解析
        var eventType = xmlPullParser.eventType //得到解析事件
        var id = ""
        var name = ""
        var version = ""
        while (eventType != XmlPullParser.END_DOCUMENT){ //不等于则解析工作还未完成
            val nodeName = xmlPullParser.name //获取节点名字
            when(eventType){
                //开始解析某节点
                XmlPullParser.START_TAG -> {
                    when(nodeName){ //nextText()获取节点具体内容
                        "id" -> id = xmlPullParser.nextText()
                        "name" -> name = xmlPullParser.nextText()
                        "version" -> version = xmlPullParser.nextText()
                    }
                }
                //完成解析某个节点
                XmlPullParser.END_TAG ->{
                    if ("app" == nodeName){
                        Log.d("NetworkActivity.out","id is $id")
                        Log.d("NetworkActivity.out","name is $name")
                        Log.d("NetworkActivity.out","version is $version")
                    }
                }
            }
            eventType = xmlPullParser.next() //获取下一个解析事件
        }
    }catch (e: Exception){
        e.printStackTrace()
    }
}

在点击button接收到XML格式的数据后会在parseXMLWithPull()方法中解析以日志的形式展示
在这里插入图片描述
10.0.2.2对于模拟器来说就是计算机本机的IP地址

SAX解析方式

SAX解析也是种常用的解析方式,虽然用法复杂一些但是语义方面会更清楚。
首先需要新建一个继承于DefaultHandler的类,并重写父类的五个方法

class ContentHandler() : DefaultHandler() {
    private val TAG = "ContentHandler.out"

    private var nodeName = ""
    private lateinit var id : StringBuilder
    private lateinit var name : StringBuilder
    private lateinit var version : StringBuilder

    /**
     * 开始XML解析时调用
     */
    override fun startDocument() {
        id = StringBuilder()
        name = StringBuilder()
        version = StringBuilder()
    }

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

    /**
     * 获取节点中的内容时调用
     */
    override fun characters(ch: CharArray, start: Int, length: Int) {
        //根据节点名判断将内容添加到哪一个对象中
        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){
            //trim()用于去除最后的回车或换行符
            Log.d(TAG,"id is ${id.toString().trim()}")
            Log.d(TAG,"name is ${name.toString().trim()}")
            Log.d(TAG,"version is ${version.toString().trim()}")
            //最后要将StringBuilder清空
            id.setLength(0)
            name.setLength(0)
            version.setLength(0)
        }
    }

    /**
     * 完成所有XML解析时调用
     */
    override fun endDocument() {

    }
}

然后在刚刚的NetworkActivity添加parseXMLWithSAX()方法并在sendRequestWithOkHttp()中调用它

private fun parseXMLWithSAX(xmlData: String){
    try{
        val factory = SAXParserFactory.newInstance()
        val xmlReader = factory.newSAXParser().xmlReader
        val handler = ContentHandler()
        //将ContentHandler实例设置到XMLReader中
        xmlReader.contentHandler = handler
        //开始解析
        xmlReader.parse(InputSource(StringReader(xmlData)))
    }catch (e: Exception){
        e.printStackTrace()
    }
}

就能得到打印解析的日志(略)

解析JSON格式数据

提前准备好JSON格式的数据。
在这里插入图片描述

JSONObject

还是在刚刚的NetworkActivity
修改sendRequdetWithOkHttp()方法中的HTTP请求和解析数据的方法
在这里插入图片描述
然后加入解析JSON数据的方法

private fun parseJSONWithJSONObject(jsonData: String) {
    try {
        val jsonArray = JSONArray(jsonData)
        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.d("NetworkActivity.out","id is $id")
            Log.d("NetworkActivity.out","name is $name")
            Log.d("NetworkActivity.out","version is $version")
        }
    }catch (e: Exception){
        e.printStackTrace()
    }
}

点击按钮后,数据被完整解析。
在这里插入图片描述

GSON

在使用前先添加库依赖

implementation 'com.google.code.gson:gson:2.8.5'

一段JSON数据

如:{"name":"Tom","age":20}
在解析时只需定义一个Person类,加入nameage这两个字段,调用如下代码就可以自动将JSON格式的字符串映射成一个Person类的对象

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

一段JSON数组

如:[{"name":"Tom","age":20},{"name":"Jack","age":25},{"name":"Amy.","age":18}]
就要借助TypeToken将期望解析成的数据类型传入fromJson()方法中

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

使用示例

先新建一个数据对应的类名为App

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

还是在刚刚的NetworkActivity
sendRequdetWithOkHttp()方法中使用新加入的parseJSONWithGSON()方法进行数据解析

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.d("NetworkActivity.out","id is ${app.id}")
        Log.d("NetworkActivity.out","name is ${app.name}")
        Log.d("NetworkActivity.out","version is ${app.version}")
    }
}

点击按钮后会得到跟刚刚一样的结果

网络请求回调的实现方式

在通常情况下,会将刚刚这些常用的网络操作提取到一个公共的类里,并提供一个通用方法,当想要发起网络请求时,只需调用这个方法即可。
比如:

HttpURLConnection

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 = "http://www.baidu.com"
val response = HttpUtil.sendHttpRequest(address)

注意:网络请求通常属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候主线程被阻塞。
如果在sendHttpRequest()方法中开启一个线程来发起HTTP请求,服务器响应的数据是无法进行返回的。这是由于所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了

这时就需要用到编程语言的回调机制了
新建一个接口

interface HttpCallbackListener {
    /**
     * 当服务器响应成功时调用
     * 参数代表服务器返回的数据
     */
    fun onFinish(response: String)

    /**
     * 网络操作出现错误时调用
     * 参数记录错误详细信息
     */
    fun onError(e: Exception)
}

然后再修改刚刚HttpUtil中的部分代码
在这里插入图片描述
这样再使用的就可以:

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

OkHttp

将上述HttpURLConnection的写法用OkHttp实现更为简洁
HttpUtil中加入一个sendOkHttpRequest()方法

fun sendOkHttpRequest(address: String,callback: okhttp3.Callback){
    val client = OkHttpClient()
    val request = Request.Builder()
        .url(address)
        .build()
    //OkHttp在enqueue()中已经开好了子线程
    //并将最终的请求结果调回到okhttp3.Callback中
    client.newCall(request).enqueue(callback)
}

在调用该方法时即可:

HttpUtil.sendOkHttpRequest(address, object : Callback{
    override fun onResponse(call: Call, response: Response) {
        //得到服务器返回的具体内容
        val responseData = response.body?.string()
    }

    override fun onFailure(call: Call, e: IOException) {
        //对异常情况进行处理
    }
})

Retrofit - 最好用的网络库

基本用法

新建一个AppService项目

在使用前要先添加依赖库

implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'

无需再手动添加OkHttp库和GSON库

首先,Retrofit会借用GSON将JSON数据转换成对象,所以此处还需要一个App类

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

接下来,根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中定义对应具体服务器接口的方法。此处只需要一个获取JSON数据的接口

// Retrofit的接口文件名一般以具体的功能种类名开头,并以Service结尾
interface AppService {
    /**
     * @GET 表示在调用getAppData()方法时会发起一条GET请求
     * 请求的地址就是在注解中传入的具体参数
     * 注意 此处只需传入请求地址的相对路径
     * 
     * getAppData()方法的返回值类型必须声明成Retrofit中内置的Call类型
     * 并通过泛型来指定服务器相应数据应该转换成什么对象
     */
    @GET("get_data.json")
    fun getAppData(): Call<List<App>>
}

activity_main.xml中添加一个idgetAppDataBtn的按钮便于控制。
然后在MainActivity中添加按钮的点击事件用于处理具体的网络请求逻辑

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity.out"

    private lateinit var mainBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        mainBinding.getAppDataBtn.setOnClickListener {
            val retrofit = Retrofit.Builder()
                .baseUrl("http://10.0.2.2:88/") //指定所有Retrofit请求的根路径
                .addConverterFactory(GsonConverterFactory.create()) //指定Retrofit解析数据时使用的转换库
                .build() //构建Retrofit对象
            val appService = retrofit.create(AppService::class.java)
            appService.getAppData().enqueue(object : Callback<List<App>>{
                override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
                    val list = response.body() //得到解析后的对象
                    if (list != null){
                        for (app in list){
                            Log.d(TAG,"id is ${app.id}")
                            Log.d(TAG,"name is ${app.name}")
                            Log.d(TAG,"version is ${app.version}")
                        }
                    }
                }

                override fun onFailure(call: Call<List<App>>, t: Throwable) {
                    t.printStackTrace()
                }
            })
        }
    }
}

点击按钮后日志正常打印如下:
在这里插入图片描述

处理复杂的接口地址类型

以下列Data类所示为例

class Data(val id: String, val content: String){}

静态的

如果接口位置为静态的:GET http://example.com/get_data.json
那么对应的写法为:

interface ExampleService {
    @GET("get_data.json")
    fun getData(): Call<Data>
}

动态的

但是在许多场景下,接口位置为动态的:GET http://example.com/<page>/get_data.json
<page>代表页数,传入的页数不同,服务器返回的数据也会不同
那么对应的写法为:

interface ExampleService {
	//{page}为占位符
	//@Path("page") 注解用于声明参数
    @GET("{page}/get_data.json")
    fun getData(@Path("page") page: Int): Call<Data>
}

当发起请求时,参数值将自动替换到占位符的位置。

传参的

当服务器接口要求传入参数时:GET http://example.com/get_data.json?u=<user>&t=<token>
那么对应的写法为:

interface ExampleService {
    @GET("get_data.json")
    fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}

请求类型

请求名称用途
@GET从服务器获取数据
@POST向服务器提交数据
@PUT@PATCH修改服务器上的数据
@DELETE删除服务器上的数据

发出请求

比如服务器提供了如下接口地址:DELDTE http://example.com/data/<id>
要根据id删除一条数据
那么对应的写法为:

interface ExampleService {
    @DELETE("data/{id}")
    fun deleteData(@Path("id") id: String): Call<Data>
}

提交数据

比如服务器提供了如下接口地址:

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<ResponseBody>
}

在handler中指定参数

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<Data>
}
动态的

当发起网络请求的时候,Retrofit就会自动将参数中传入的值设置到User-AgentCache-Control这两个header当中,从而实现了动态指定header值的功能。

interface ExampleService {
	 @GET("get_data.json")
	 fun getData(@Header("User-Agent") userAgent: String,
	 	@Header("Cache-Control") cacheControl: String): Call<Data>
}

Retrofit构建器的最佳写法

想要得到AppService的动态代理对象,需要先使用Retrofit.Builder构建出⼀个Retrofit对象,然后再调用Retrofit对象的create()方法创建动态代理对象。
事实上,没有每次都写一遍的必要,因为构建出的Retrofit对象是全局通用的,只需要在调用create()方法时针对不同的Service接口传入相应的Class类型即可。因此可以将通用的这部分功能封装起来,从而简化获取Service接口动态代理对象的过程。
新建一个ServiceCreator单例类:

object ServiceCreateor {
    //用于指定根路径
    private const val BASE_URL = "http://10.0.2.2:88/"

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun <T> create(serviceClass: Class<T>): T =
        retrofit.create(serviceClass)
}

经过这样的封装之后,Retrofit的用法将会变得简单许多
比如我们想获取一个AppService接口的动态代理对象,只需要使用如下写法即可:

val appService = ServiceCreator.create(AppService::class.java)
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值