11-Web

WebView

WebView 空间可以实现应用程序内浏览器的嵌入以及网页的展示

Quick Start

  1. Layout
1
2
3
4
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
  1. loadUrl: 其中 WebViewClient() 的设置是希望当从一个网页跳转到另一个网页时,希望目标网页仍然在当前 WebView 中显示,而不是打开系统浏览器
1
2
3
4
5
R.id.buttonLoadBaidu -> {
webView.settings.javaScriptEnabled = true
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
  1. Permission
<uses-permission android:name="android.permission.INTERNET" />

HTTP in Android

Quick Start

  1. 发送 HTTP 请求: 创建一个子线程完成请求的发送与响应的获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private fun sendRequestWithHttpURLConnection() {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL("https://www.baidu.com")
connection = url.openConnection() as HttpURLConnection

connection.requestMethod = "GET"
connection.connectTimeout = 8000 //ms
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()
}
}

// 子线程与 UI 交互的封装
private fun showResponse(response: String) {
runOnUiThread {
// UI 操作线程
responseText.setText(response)
}
}
}

Post Data

将 connection 对象的请求方法设置为 POST 后即可通过 outputStream 进行数据的发送

connection.requestMethod = "POST" // 可以提交数据
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=123456")

OkHTTP

超越原生 HttpURLConnection 的外部开源库

Quick Start

  1. app/build.gradle 中添加依赖
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
  1. client + request + response + responseData
1
2
3
4
5
6
7
8
9
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
showResponse(responseData)
}

Post data

1
2
3
4
5
6
7
8
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build()
val requestPost = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)kotlin
.build()

HTTP

HTTP 明文传输默认不支持, 需要在 res/xml 中手动写一个配置文件 network_config.xml 并应用

1
2
3
4
5
6
7
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

在 AndroidManifest 中应用

1
2
3
4
<application
...
android:networkSecurityConfig="@xml/network_config"
/>

XML Parser

在网络上传输数据时最常用的格式有两种:XML 和 JSON, 先来了解下 Android 关于 XML 解析的相关知识

Pull 解析

首先时请求获取 XML 相关数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun parseXMLTest() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://192.168.0.107/DVWA/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
val xmlData = parseXMLWithPull(responseData)
showResponse(xmlData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

然后对 XML 数据进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private fun parseXMLWithPull(xmlData: String): String {
val retXmlData = StringBuilder()
try {
// 创建工厂类实例, 获取解析对象: xmlPullParse
val factory = XmlPullParserFactory.newInstance()
val xmlPullParser = factory.newPullParser()
// 设置解析内容:
xmlPullParser.setInput(StringReader(xmlData))
// 获得初始的解析状态
var eventType = xmlPullParser.eventType
var id = ""
var name = ""
var version = ""
// 解析循环判停条件
while (eventType != XmlPullParser.END_DOCUMENT) {
// getName 获取当前解析节点的名称
val nodeName = xmlPullParser.name
when (eventType) {
// 根据开始节点的名称进行处理
XmlPullParser.START_TAG -> {
// 简单的根据开始节点名称获取节点中的值
when (nodeName) {
"id" -> id = xmlPullParser.nextText()
"name" -> name = xmlPullParser.nextText()
"version" -> version = xmlPullParser.nextText()
}
}
// 根据结束节点的名称进行处理
XmlPullParser.END_TAG -> {
// 结束节点为 app 时表示一个 app 的值获取完毕, 进行输出封装
if ("app" == nodeName) {
retXmlData.append("id is $id + name is $name + version is $version\n")
}
}
}
// 获取下一个解析事件类型
eventType = xmlPullParser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
return retXmlData.toString()
}

SAX 解析方式

用法比 Pull 更复杂, 但是语义更清楚

  1. 需要继承 DefaultHandler 类并实现相关处理方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class XMLContentHandler: DefaultHandler() {

private var nodeName = ""
private var retXMLData = StringBuilder()
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?
) {
if (localName == null) return
nodeName = localName
retXMLData.apply {
append("uri is $uri\n")
append("localname is $localName\n")
append("qName is $qName\n")
append("attributes is $attributes\n")
}
}

// 针对节点中内容的解析
override fun characters(ch: CharArray?, start: Int, length: Int) {
// 通过 startElement 中记录的节点名称, 将内容添加到相应容器中
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) {
retXMLData.append("id is ${id.toString().trim()} @ name is ${name.toString().trim()} @ version is ${version.toString().trim()}\n")
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}

// 完成整个 xml 解析时调用
override fun endDocument() {
super.endDocument()
}

fun getRetXMLData(): String {
return this.retXMLData.toString()
}

}
  1. 在 SAXParser 中应用 contentHandler 进行 parse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun parseXMLWithSAX(xmlData: String): String {
var retXMLData = ""
try {
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader
val handler = XMLContentHandler()
xmlReader.contentHandler = handler
xmlReader.parse(InputSource(StringReader(xmlData)))
retXMLData = handler.getRetXMLData()
} catch (e: Exception) {
e.printStackTrace()
}
return retXMLData
}

JSON Parser

JSON 体积更小,在网络上传输的时候更省流量,但语义性较差,不如 XML 直观

JSONObject 解析

解析的方式十分简单, 将 JSON 字符串转为 JSONArray 对象, 遍历按类型解析即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val retJsonData = StringBuilder()
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")
retJsonData.append("id is $id | name is $name | version is $version\n")
}
} catch (e: Exception) {
e.printStackTrace()
}
return retJsonData.toString()

GSON 解析

  1. app\build.gradle 中添加依赖
implementation 'com.google.code.gson:gson:2.8.5'
  1. 为 JSON 数据建立一个 Bean 类
class Book(var id: Int, var name: String, var version: String) {}
  1. 使用 GSON 的实例进行解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun parseJsonWithGSON(jsonData: String): String {
val retJsonData = StringBuilder()
val gson = Gson()
// 解析单个数据
// val book = gson.fromJson(jsonData, Book::class.java)
// 解析多个(数组)数据
// 防止 List<Book> 中 Book 类型被擦除
// TypeToken 是个类, 我们使用 object: TypeToken<List<Book>>() {} 定义了其匿名类, 然后获取了这个类的 type 属性
// 相当于用一个子类 SubList extends List<String> 将父类中的泛型给保存
val typeOf = object: TypeToken<List<Book>>() {}.type
val books = gson.fromJson<List<Book>>(jsonData, typeOf)
books.forEach {
retJsonData.append("id is ${it.id} @ name is ${it.name} @ version is ${it.version}\n")
}
return retJsonData.toString()
}

GSON 处理复杂数据

如果是时间等复杂对象的需要手动配置针对目标对象的 Serializer 与 Deseralizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
val todoGson = GsonBuilder()
.registerTypeAdapter(LocalDateTime::class.java, // 对 LocalDateTime 添加 Serializer
object : JsonSerializer<LocalDateTime> {
override fun serialize(
src: LocalDateTime?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonPrimitive(src?.toLong()) // toLong 是我对 LocalDateTime 添加的方法, 转为了 millsSecond
}

}
).registerTypeAdapter(LocalDateTime::class.java, // 对 LocalDateTime 添加 Deserializer
object : JsonDeserializer<LocalDateTime> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): LocalDateTime {
return json!!.asJsonPrimitive.asString.toLong().toLocalDateTime() // 获取 json 存储的 long 格式数据,转为 LocalDatetime
}
}).serializeNulls().create()

// to Json
val dbJson = todoGson.toJson(dbInJson)
// back to obj
val dbInJson = todoGson.fromJson(dbJson, DBInJson::class.java)

网络请求的封装

新建一个工具类对网络请求进行封装, 但是因为是多线程任务, 要再事件完成或失败时进行相应的回调处理, 因此需要定义相关的回调接口, 或实现相关库提供的回调接口

相关的回调接口

1
2
3
4
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e: Exception)
}

整体工具类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
object HttpUtil {

fun sendHttpRequest(address: String, callbackListener: HttpCallbackListener) {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
var 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)
}
}
callbackListener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
callbackListener.onError(e)
} finally {
connection?.disconnect()
}
}

}

fun sendOkHttpRequest(address: String, callbackListener: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
// OkHttp 在 enqueue() 方法的内部已经帮我们开好子线程了,然后会在子线程中执行 HTTP 请求,并将最终的请求结果回调到 okhttp3.Callback 当中
client.newCall(request).enqueue(callbackListener)
}
}

网络库 Retrofit

Quick Start

  1. app/build.gradle 中添加依赖
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
  1. 封装好要请求的对象
class Book(val id: String, val name: String, val version: String) {}
  1. 定义请求接口
1
2
3
4
5
6
interface BookService {

@GET("/DVWA/get_data.json")
// 返回值必须声明成 Retrofit 中内置的 Call 类型
fun getBookData(): Call<List<Book>>
}
  1. 触发服务器请求: 传入服务器根地址获取 Retrofit 对象 + 创建服务对象 + 调用服务获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
buttonGetBookData.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.0.107/")
// 用于指定 Retrofit 在解析数据时所使用的转换库,这里指定成 GsonConverterFactory - 获取的是 JSON 数据
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(BookService::class.java)
appService.getBookData().enqueue(object : Callback<List<Book>> {

override fun onResponse(call: Call<List<Book>>, response: Response<List<Book>>) {
// 自动的多线程操作
// 当发起请求的时候,Retrofit 会自动在内部开启子线程,当数据回调到 Callback 中之后,Retrofit 又会自动切换回主线
val list = response.body()
if (list != null) {
for (book in list) {
Log.d("MainActivity", "id is ${book.id}")
Log.d("MainActivity", "name is ${book.name}")
Log.d("MainActivity", "version is ${book.version}")
}
}
}

override fun onFailure(call: Call<List<Book>>, t: Throwable) {
t.printStackTrace()
}
})
}
  1. 按照之前步骤配置 HTTP 明文请求 + AndroidManifest 中配置网络权限
<uses-permission android:name="android.permission.INTERNET" />

复杂接口

通过变量匹配多个接口

  • Path 中应用变量
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call<Data>
  • GET 请求中的参数: http://example.com/get_data.json?u=<user>&t=<token>
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
  • POST 请求中的参数
@POST("data/create.json")
fun createData(@Body data: Data): Call<ResonseBody>
  • 静态配置请求头参数
@Headers("User-Agent: ...", "Cache-Control: ...")
@GET("get_data.json")
fun getData(): Call<Data>
  • 动态配置请求头参数
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String): Call<Data>

单例类封装

Service 的动态代理对象是能通用的, 因此依旧可以通过创建一个单例类来实现共享使用

1
2
3
4
5
6
7
8
9
10
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
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)
}