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
类,加入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":"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
中添加一个id
为getAppDataBtn
的按钮便于控制。
然后在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-Agent
和Cache-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)