WebView WebView 空间可以实现应用程序内浏览器的嵌入以及网页的展示
Quick Start
Layout
1 2 3 4 <WebView android:id ="@+id/webView" android:layout_width ="match_parent" android:layout_height ="match_parent" />
loadUrl: 其中 WebViewClient() 的设置是希望当从一个网页跳转到另一个网页时,希望目标网页仍然在当前 WebView 中显示,而不是打开系统浏览器
1 2 3 4 5 R.id.buttonLoadBaidu -> { webView.settings.javaScriptEnabled = true webView.webViewClient = WebViewClient() webView.loadUrl("https://www.baidu.com" ) }
Permission
<uses-permission android:name ="android.permission.INTERNET" />
HTTP in Android Quick Start
发送 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 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() } } private fun showResponse (response: String ) { runOnUiThread { 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
app/build.gradle
中添加依赖
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
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 { 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) { 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 -> { 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 更复杂, 但是语义更清楚
需要继承 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 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 ) { 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 ) } } override fun endDocument () { super .endDocument() } fun getRetXMLData () : String { return this .retXMLData.toString() } }
在 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 解析
app\build.gradle
中添加依赖
implementation 'com.google.code.gson:gson:2.8.5'
为 JSON 数据建立一个 Bean 类
class Book (var id: Int , var name: String, var version: String) {}
使用 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 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, object : JsonSerializer<LocalDateTime> { override fun serialize ( src: LocalDateTime ?, typeOfSrc: Type ?, context: JsonSerializationContext ? ) : JsonElement { return JsonPrimitive(src?.toLong()) } } ).registerTypeAdapter(LocalDateTime::class .java, object : JsonDeserializer<LocalDateTime> { override fun deserialize ( json: JsonElement ?, typeOfT: Type ?, context: JsonDeserializationContext ? ) : LocalDateTime { return json!!.asJsonPrimitive.asString.toLong().toLocalDateTime() } }).serializeNulls().create() val dbJson = todoGson.toJson(dbInJson)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() client.newCall(request).enqueue(callbackListener) } }
网络库 Retrofit Quick Start
app/build.gradle
中添加依赖
implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
封装好要请求的对象
class Book (val id: String, val name: String, val version: String) {}
定义请求接口
1 2 3 4 5 6 interface BookService { @GET("/DVWA/get_data.json" ) fun getBookData () : Call<List<Book>> }
触发服务器请求: 传入服务器根地址获取 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/" ) .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 >>) { 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() } }) }
按照之前步骤配置 HTTP 明文请求 + AndroidManifest 中配置网络权限
<uses-permission android:name ="android.permission.INTERNET" />
复杂接口 通过变量匹配多个接口
@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("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) }