文章

Android网络技术

一、准备工作

1.网络权限

在Android中使用网络功能需要在AndroidManifest.xml中声明权限:

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

2.明文HTTP(可选)

从Android9.0系统开始,应用程序默认只允许使用HTTPS类型的网络请求,HTTP因为有安全隐患默认不支持,为了让程序可以使用HTTP明文传输,需要进行如下配置:

  1. 右键res目录 -> New -> Directory,创建一个xml目录。
  2. 右键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>

修改AndroidManifest.xml

<application
    ...
    android:networkSecurityConfig="@xml/network_config">
    
</application>

2022/11/27更新:
现在可以直接在 AndroidManifest.xml 文件中加入如下字段即可:

<application
    ...
    android:usesCleartextTraffic="true">
    ...
</application>

二、WebView用法

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

// 让浏览器支持JavaScript脚本
webView.settings.javaScriptEnabled=true
// 当网页跳转时使目标网页仍在当前WebView中显示,而不是打开浏览器
webView.webViewClient = WebViewClient()
webView.loadUrl("http://www.baidu.com")

三、使用HttpURLConnection

private fun sendRequestWithHttpURLConnection() {
    // 开启线程发起网络请求
    thread {
        var connection: HttpURLConnection? = null
        try {
            val response = StringBuilder()
            val url = URL("https://www.baidu.com")
            // 获取HttpURLConnection实例
            connection = url.openConnection() as HttpURLConnection
            // 设置请求方式
            connection.requestMethod = "GET"
            // 设置超时毫秒树
            connection.connectTimeout = 8000
            connection.readTimeout = 8000
            // 获取服务器返回的输入流
            val input = connection.inputStream
            // 下面对获取到的输入流进行读取
            val reader = BufferedReader(InputStreamReader(input))
            reader.use {
                reader.forEachLine {
                    response.append(it)
                }
            }
            // 将读取到的字符串显示在TextView中
            showResponse(response.toString())
        } catch(e: Exception) {
            e.printStackTrace()
        } finally {
            // 关闭HTTP连接
            connection?.disconnect()
        }
        
    }
}

//将读取到的字符串显示在TextView中
private fun showResponse(response: String) {
    runOnUiThread {
        // 在这里可以执行UI操作,将结果显示在页面上
        textView.text = response
    }
}

1.使用POST提交数据

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

四、使用OkHttp

OkHttp是许多出色的网络通信库中做的最出色的一个,是由大名鼎鼎的Square公司开发的,出了OkHttp之外,还开发了Retrofit、Picasso等知名开源项目。
OKHttp不仅在接口封装上做的简单易用,就连在底层实现上也是自成一派,比起原生的HTTPURLConnection可以说是有过之而无不及。
现在已经成了广大Android开发者首选的网络通信库,OKHttp项目地址是:OKHttp

在使用OKHttp之前需要先在项目中添加OkHttp库的依赖,编辑app/build.gradle:

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

private fun sendRequestWithOkHttp() {
    thread {
        try {
            // 首先创建OkHttpClient实例
            val client = OkHttpClient()
            // 想要发起HTTP请求,就要创建一个Request对象
            val request = Request.Builder()
                .url("https://www.baidu.com")
                .build()
            // 创建Call对象,execute()发送请求
            val response = client.newCall(request).execute()
            // 获取返回具体内容
            val responseData = response.body?.string()
            if(responseData != null) {
                // 将返回结果字符串显示在TextView中
                showResponse(responseData)
            }
        } catch(e: Exception) {
            e.printStackTrace()
        }
    }
}

1.POST方法

val requestBody = FormBody.Builder()
    .add("username", "admin")
    .add("password", "123456")
    .build()
val request = Request.Builder()
    .url("https://www.baidu.com")
    // 放入Post数据
    .post(requestBody)
    .build()

五、网络请求回调的实现方式

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

因此,通常情况下我们应该将这些通用的网络操作提取到一个公共的类中,并提供一个方法。而且由于HTTP请求是耗时操作,如果没有在请求内部开启线程的话就很有可能导致ANR。

但是如果在请求内部直接开启一个线程来进行HTTP请求的话,服务器响应的数据是无法进行返回的,这是由于所有的耗时操作都是在子线程中进行的,子线程会在服务器还没有来得及响应的时候就结束了,所以我们还需要使用回调机制。

1.HttpURLConnection实现

1. 首先定义一个回调接口

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

2.将HTTP请求提取到公共类中并添加回调接口

object HttpUtil {
    fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
        thread {
            var connection: HttpURLConnection? = null
            try {
                val response = StringBuilder()
                val url = URL("https://www.baidu.com")
                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 {
                connection?.disconnect()
            }
        
        }
    }
}

3.调用HTTP请求方法并实现回调

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

2.OkHttp实现

1.将HTTP请求提取到公共类中

object HttpUtil {
    ...
    // okhttp3.Callback是OkHttp自带的回调接口
    fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
       val client = OkHttpClient()
       val request = Request.Builder()
           .url(address)
           .build() 
         // enqueue在内部开启子线程
        client.newCall(request).enqueue(callback)
    }
}


2.调用HTTP请求方法并实现回调

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

六、最好用的网络库——Retrofit

Retrofit也是Square公司开发的网络库,但是它和OkHttp定位完全不同,OkHttp侧重底层通信的实现,而Retrofit侧重上层接口的封装,事实上Retrofit就是在OkHttp的基础上进一步开发出来的应用层网络通信库。Github地址:Retrofit

Retrofit会借助GSON将JSON数据自动转换成POJO对象。

添加依赖库:

dependencies {
    // 自动导入:Retrofit、OkHttp、Okio
    implementation "com.squareup.retrofit2:retrofit:2.6.1"
    // Retrofit转换库,借助GSON解析数据,所以这里也会下载GSON
    // 除了GSON,Retrofit还支持Jackson、Moshi等
    implementation "com.squareup.retrofit2:converter-gson:2.6.1"
}

这里约定一个JSON文件待用(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"}]

同时还有一个对应的POJO类,包含id、name、version:

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

1.Retrofit的基本用法

我们根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中定义对应具体服务器接口的方法。下面定义一个获取上方约定的JSON文件的一个接口,创建AppService接口:

interface AppService {
    
    // 表示调用getAppData()时Retrofit发起一条GET请求
    // 请求地址就是传入的具体参数,只需填写相对地址
    @GET("get_data.json")
    // 返回值必须声明成Retrofit内置的Call
    fun getAppData(): Call<List<App>>    // 通过泛型来指定响应数据转换成什么对象
    
}

修改MainActivity代码:

fun onCreate(...) {
    ...
    btn.setOnClickListener {
        // 构建Retrofit对象
        val retrofit = Retrofit.Builder().run {
            // 指定所有Retrofit接口中请求的根路径
            baseUrl("http://10.0.2.2/")
            // Retrofit在解析数据时使用的转换库
            addConverterFactory(GsonConverterFactory.create())
            build()
        }
        // 传入具体Service接口的Class类型
        val appService = retrofit.create(AppService::class.java)
        // 调用AppService接口中的getAppData()方法,返回Call<List<App>>
        // 再调用Call的enqueue()方法发起请求,请求结果回调到Callback中
        // 请求时,会自动在内部开启子线程,数据回调后会自动切换到主线程
        appService.getAppData().enqueue(object : Callback<List<App>> {
            override fun onResponse(call: Call<List<App>>, 
                response: Response<List<App>>) {
                // 得到Retrofit解析后的对象(List<App>)
                val list = response.body()
                if (list != null) {
                    for(app in list) {
                        // 遍历list,并打印出App类中数据
                        Log.d("MainActivity", "id is ${app.id}")
                        Log.d("MainActivity", "name is ${app.name}")
                        Log.d("MainActivity", "version is ${app.version}")
                    }
                }
            }
           
            override fun onFailure(call: Call<List<App>>, t: Throwable) {
                t.printStackTrace()
            }
        })
     }
}

2.处理复杂的接口地址类型

为了方便举例,这里先定义一个Data类,包含id和content两个字段:

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

2.1.动态接口地址

在很多场景下,接口中地址的部分内容可能是会动态变化的,比如如下的接口地址:

GET http://example.com//get_data.json

这种情况下,对应到Retrofit接口类中可以如下编写:

interface ExampleService {
    
    // 添加{page}占位符
    @GET("{page}/get_data.json")
    // 使用@Path("page")来声明这个参数
    // 调用getData()时,自动将page参数替换到占位符
    fun getData(@Path("page") page: Int): Call<Data>
}

2.2.带参数接口

很多接口还会要求传入一系列参数,格式如下:

GET http://example.com/get_data.json?u=&t=

这里也可以使用上面的@Path注解来解决,但是会有些麻烦,专门的语法支持如下:

interface ExampleService {
    
    @GET("get_data.json")
    // @Query注解会在发起请求时自动将这两个参数构建到请求地址中
    fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
    
}

2.3.常用HTTP请求类型

Retrofit支持@GET、@POST、@PUT、@PATCH、@DELETE常用注解。比如提供了如下接口:

DELETE http://example.com/data/

上面接口意味着根据id删除一条指定数据,想发出这种请求可以这样写:

interface ExampleService {
    
    @DELETE("data/{id}")
    // ResponseBody表示对服务器响应数据不关心,可以接受任意类型数据
    // 并且不对响应数据进行解析
    fun deleteData(@Path("id") id: String): Call<ResponseBody>
}

2.4.向服务器提交数据POST

比如有有如下提交数据的接口:

POST http://example.com/data/creaet

{“id” : 1, “content” : “The description for this data.”}

使用POST来提交数据,需要将数据放到HTTP请求的body部分:

interface ExampleService {
    @POST("data/create")
    // 给Data类加上@Body注解,这样发出POST请求时
    // 会自动将Data中的数据转成JSON格式的文本
    // 这种写法同样适用于PUT、PATCH、DELETE
    fun createData(@Body data: Data): Call<ResponseBody>
}

2.5.设置请求Header

有些服务器接口可能要求在HTTP请求的header中指定参数,比如:

GET http://example.com/get_data.json

User-Agent: okhttp

Cache-Control: max-age=0
这些header就是一个个键值对,可以直接使用@Headers注解:

interface ExampleService {
    // 这种写法只能进行静态header声明
    @Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
    @GET("get_data.json")
    fun getData(): Call<Data>
}

也可以使用@Header注解进行动态指定header的值:

interface ExampleService {
    @GET("get_data.json")
    // 发起请求时会自动将两个参数的值设置到对应的header中
    fun getData(@Header("User-Agent") userAgent: String,
        @Header("Cache-Control") cacheControl: String): Call<Data>
}

3.Retrofit构建器的最佳写法

如果每一次使用Retrofit时都要写一遍上面的获取Service接口的动态代理对象,那实在是太麻烦了。Retrofit对象是全局通用的,只需要在调用create()方法时针对不同的Service接口传入相应的Class类即可,因此我们可以将通用的部分功能封装起来。新建一个ServiceCreator单例类:

object ServiceCreator {
    private const val BASE_URL = "http://10.0.2.2/"
    
    private val retrofit = Retrofit.Builder().run {
        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)
}

fun main() {
    // 获取AppService接口动态代理对象
    val appService = ServiceCreator.create<AppService>()
}
License:  CC BY 4.0