提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。
Android网络技术是指Android应用程序中使用网络技术进行数据传输和处理的技术。
WebView是Android中的一个组件,它允许我们在自己的应用程序内部嵌入一个浏览器。借助WebView,我们可以很轻松的显示HTML、CSS和JavaScript内容,就像在浏览器中一样。如果你想使用一个WebView,你需要先在布局文件中添加一个WebView控件。
新建一个WebViewTest项目,在activity_main.xml文件中添加一个WebView控件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--WebView组件-->
<WebView
android:id="@+id/myWebView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
接下来,我们需要通过ViewBinding获取WebView控件,然后设置WebView的属性。在MainActivity.kt中修改以下代码:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 通过ViewBinding获取WebView控件
binding.myWebView.let {
// 支持JavaScript脚本功能开启
it.settings.javaScriptEnabled = true
// 发生网页跳转时 仍在WebView中显示新页面
it.webViewClient = WebViewClient()
// 加载指定的网页URL
it.loadUrl("https://www.baidu.com")
}
}
}
最后,别忘了在清单文件中申请访问网络的权限。在AndroidManifest.xml文件中添加以下权限:
<uses-permission android:name="android.permission.INTERNET" />
运行程序,可以看到我们的应用自动打开了一个网页。
还可以在网页内通过点击链接来跳转到其他页面:
首先,了解一下HTTP的工作原理:发送请求(客户端)——>响应并返回数据(服务器)——>解析服务器返回的数据(客户端)——>将解析的数据显示出来(客户端)。
由于WebView已经在后台帮我们处理好了:发送HTTP请求、接收服务器响应、解析返回的数据以及显示页面这几步工作。使得我们没办法直观的看出HTTP是怎么工作的,下面我们就通过手动发送HTTP请求来更加深入的理解这个过程。
Android上发送HTTP请求主要是通过HttpURLConnection进行的 ,若想使用HttpURLConnection,有以下几个步骤:
首先创建一个URL对象并传入目标地址,然后调用URL对象的openConnection()方法获取HttpURLConnection对象。
val url = URL(""https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection
HTTP请求常用的方法有两种:GET和POST。GET主要用于从服务器获取数据,POST主要用于向服务器发送数据。
// 设置请求HTTP的方式(GET or POST)
connection.requestMethod = "GET"
实际上,GET请求也可以用于向服务器传送数据,只不过在实际应用中,我们通常使用POST请求来提交数据,因为这样做可以更好地保护数据的安全性和稳定性。由于GET请求的数据会显示在URL中,存在安全风险。相反,POST请求的数据在请求体中,因此相对更安全。
我们可以对HTTP请求进行定制,例如设置连接超时、读取超时的毫秒数、消息头等。
// 连接服务器超时时间为8s(如果8s内无法建立到服务器的连接 抛出异常)
connection.connectTimeout = 8000
// 读取数据的超时时间为8s(如果8s内服务器没有返回数据 抛出异常)
connection.readTimeout = 8000
当你从服务器获取到数据时,你需要调用Connection对象的getInputStream()方法获取输入流InputStream,然后通过输入流InputStream来读取服务器返回的数据:
// 获取输入流
val inputStream = connection.inputStream
使用HttpURLConnection完成HTTP网络请求后,必须调用Connection对象的disconnect()方法断开HTTP连接:
// 断开HTTP连接(释放系统资源)
connection.disconnect()
下面我们新建一个NetWork项目来体验一下如何使用HttpURLConnection。修改activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/sendRequestBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="发送请求" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/responseTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="服务器返回的响应" />
</ScrollView>
</LinearLayout>
我们在主界面放置了一个ScrollView控件,ScrollView是一种滚动视图控件,主要用于实现垂直方向上的滚动效果。ScrollView只能包含一个子View,当子View的内容超出了ScrollView的可见区域时,用户可以通过滚动视图来查看超出的内容。同时,ScrollView的高度是自适应的,它会根据自身和子View的内容自动调整高度。
接着修改MainActivity.kt中的代码,实现HttpURLConnection发送网络请求,并将服务器返回的数据显示在ScrollView中:
class MainActivity : AppCompatActivity() {
private lateinit var myBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(myBinding.root)
myBinding.sendRequestBtn.setOnClickListener {
// 通过HttpURLConnection发送Http请求
sendRequestWithHttpURLConnection()
}
}
/**
* 通过HttpURLConnection发送Http请求
*/
private fun sendRequestWithHttpURLConnection() {
// 在子线程中发起网络请求(所有网络请求必须放在子线程中进行)
thread {
// HttpURLConnection实例(未初始化)
var connection: HttpURLConnection? = null
try {
val response = StringBuilder() // 服务器返回的数据
val url = URL("https://www.baidu.com") // 要访问的网络地址
connection = url.openConnection() as HttpURLConnection // 初始化HttpURLConnection实例
// 配置HTTP网络请求
connection?.let {
it.connectTimeout = 8000
it.readTimeout = 8000
}
val inputStream = connection.inputStream // 获取服务器响应数据的输入流
val reader = BufferedReader(InputStreamReader(inputStream)) // 从输入流中读取数据
// 使用use函数自动关闭流
reader.use {
reader.forEachLine {
response.append(it)
}
}
showResponse(response.toString()) // 将服务器返回的数据显示在TextView中
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 不论请求是否成功 最终都要断开Http连接
connection?.disconnect()
}
}
}
/**
* 将服务器返回的数据显示在TextView中
*/
private fun showResponse(response: String) {
// 在UI线程(主线程)中执行任务(更新UI)
runOnUiThread {
myBinding.responseTextView.text = response // 将数据显示在TextView中
}
}
}
Android系统要求,所有的网络请求必须在子线程中进行,否则会抛出NetworkOnMainThreadException异常,这样做的目的是为了避免由于网络问题导致主线程阻塞。
下图是没有将sendRequestWithHttpURLConnection()方法中的代码放在thread{ }子线程中运行产生的报错:
在showResponse()方法中,我们需要将服务器返回的数据显示到ScrollView上。因此,我们通过runOnUiThread { }结构在主线程(UI线程)中执行UI操作 。
private fun showResponse(response: String) {
// 在UI线程(主线程)中执行任务(更新UI)
runOnUiThread {
myBinding.responseTextView.text = response // 将数据显示在TextView中
}
}
最后,记得在AndroidManifest.xml文件中申请网络权限:
<uses-permission android:name="android.permission.INTERNET" />
运行程序,点击发送请求按钮后的效果:
可以看到,这一大段字符就是服务器返回给我们的HTML格式的数据,只是通常情况下浏览器会帮我们将这些HTML数据解析成网页后再显示出来。
如果我们想提交数据给服务器,只需要将HTTP请求的方式改为POST,然后在获取输入流之前把要提交的数据写入即可。每条数据都需要以键值对的形式存在,数据与数据之前需要使用“&”符号进行隔开。例如我们想向服务器提交用户名和密码,就可以这样写:
connection.requestMethod = "POST"
val outputStream = DataOutputStream(connection.outputStream)
outputStream.writeBytes("username=admin&password=123456")
OkHttp是一个高效的HTTP请求框架,通过该框架可以简化客户端的网络请求并提升效率。
在使用OkHttp之前,我们需要在项目中添加OkHttp的依赖。在build.gradle.kts(:app)文件中添加以下内容:
dependencies {
· · ·
implementation("com.squareup.okhttp3:okhttp:4.9.0")
}
那么如何使用OkHttp呢?首先需要创建一个OkHttpClient对象:
val client = OkHttpClient()
①如果发送的是一条GET请求,需要构建一个Request对象。然后在调用build()方法前,通过连缀多个方法来配置这个Request对象。例如,设置网络请求的目标地址就可以调用url()方法:
val request = Request.Builder()
.url("https://www.baidu.com") // 设置网络请求地址
.build()
①如果发送的是一条POST请求,则会比GET请求稍微复杂一些。我们需要先构建一个RequestBody对象用来存放将要发给服务器的参数:
// 发送给服务器的参数
val requestBody = FormBody.Builder()
.add("username", "admin") // 用户名
.add("password", "123456") // 密码
.build()
②然后就和发送GET请求一样,构建一个Request对象并调用post()方法将上面的RequestBody对象作为参数传入:
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody) // 配置发送给服务器的参数
.build()
之后调用OkHttpClient的newCall()方法创建一个新的HTTP请求,并在此基础上调用execute()方法来执行发送操作。服务器收到请求后会返回一个Response对象,这个对象包含了服务器返回的所有数据:
// 通过的client和request对象创建一个新的HTTP请求
// 调用execute()方法发送HTTP请求 response对象就是服务器返回的数据
val response = client.newCall(request).execute()
response对象就是服务器返回的数据了,我们可以通过以下写法来取出服务器返回的数据:
val responseData = response.body?.string()
那么response和responseData有什么区别呢?准确地说response包含了服务器返回来的所有数据,包括状态码、响应头、响应体等信息。而responseData只是从中提取的数据内容,不包含状态码和响应头等信息。通俗地说,response就像是一个完整的信封,里面包含了信件、信封和邮戳等所有信息。而responseData只是信件的内容,不包含信封和邮戳等附加信息。
现在,我们尝试在NetWorkTest项目中通过OkHttp来发送Http网络请求吧!首先我们在主界面新增一个按钮sendOkHttpRequestBtn,然后修改MainActivity.kt中的方法:
class MainActivity : AppCompatActivity() {
private lateinit var myBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
· · ·
myBinding.sendOkHttpRequestBtn.setOnClickListener {
// 通过OkHttp发送Http请求
sendRequestWithOkHttp()
}
}
/**
* 通过OkHttp发送Http请求
*/
private fun sendRequestWithOkHttp() {
// 在子线程发起网络请求(网络请求必须放在子线程中进行)
thread {
try {
// 创建OkHttpClient对象
val client = OkHttpClient()
// 创建Request对象(GET请求)
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
// response服务器返回的数据
val response = client.newCall(request).execute()
// 取出服务器返回的数据并转为String
val responseData = response.body?.string()
if (responseData != null) {
// 将服务器返回的数据显示在TextView中
showResponse(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// 通过HttpURLConnection发送Http请求
private fun sendRequestWithHttpURLConnection() {
· · ·
}
// 将服务器返回的数据显示在TextView中
private fun showResponse(response: String) {
// 在UI线程中执行任务
runOnUiThread {
// 将数据显示在TextView中
myBinding.responseTextView.text = response
}
}
}
可以看到,我们只是新增了一个sendOkHttpRequestBtn按钮和一个sendRequestWithOkHttp()方法。由于所有网络请求必须放在子线程中进行,所以我们仍然将相关逻辑放到一个子线程去执行。现在我们运行程序,点击sendOkHttpRequestBtn按钮,效果如下图所示:
可以看到效果和使用HttpURLConnection请求网络的效果一样,只不过这次我们是通过OkHttp框架发送的网络请求。
为了便于你更好的理解,附上使用OkHttp的基本流程:
在网络传输数据时最常用的格式有两种:XML和JSON,下面我们先来学习如何解析XML格式数据。在Android开发中,解析XML的方式主要有三种:SAX、DOM和Pull。
前置工作需要先在本地安装Apache服务器,可以参考【27】应用开发——如何在Ubuntu系统中安装并配置Apache Http Server这篇文章。
在完成了前置操作后,我们需要在Apache服务器的目录下创建一个名为get_data.xml的文件:
<apps>
<app>
<id>1id>
<name>Google Mapsname>
<version>1.0version>
app>
<app>
<id>2id>
<name>Chromename>
<version>2.1version>
app>
<app>
<id>3id>
<name>Goole Playname>
<version>2.3version>
app>
apps>
在浏览器中输入以下网址并访问:
这里就说明我们的Apache服务器已经配置成功,并且可以成功访问到目标XML文件了!
接下来,让我们在应用程序中获取并解析这个XML文件。继续在NetworkTest项目的基础上进行开发,修改MainActivity中的代码,增加一个通过Pull解析XML数据的方法::
class MainActivity : AppCompatActivity() {
· · ·
// 通过OkHttp发送Http请求
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://10.0.2.2/get_data.xml") //指定服务器地址为本地计算机
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseXMLWithPull(responseData) // 通过Pull解析XML数据
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
· · ·
/**
* 通过Pull解析XML数据
*/
private fun parseXMLWithPull(xmlData: String) {
try {
// 创建XmlPullParserFactory工厂对象(用于创建XmlPullParser实例)
val factory = XmlPullParserFactory.newInstance()
// 通过工厂对象创建XmlPullParser实例(用于解析XML文档)
val xmlPullParser = factory.newPullParser()
// 设置解析器的数据输入源(Reader类型)
xmlPullParser.setInput(StringReader(xmlData))
// getEventType()获取当前的解析事件
var eventType = xmlPullParser.eventType
var id = ""
var name = ""
var version = ""
// 如果当前的解析事件不是结束事件END_DOCUMENT 则说明解析工作还没完成
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("MainActivity", "id is :$id")
Log.d("MainActivity", "name is :$name")
Log.d("MainActivity", "version is :$version")
}
}
} // when
//获取下一个解析事件(第二个标签中的内容)
eventType = xmlPullParser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
在Android 9.0版本开始,应用程序默认不再允许使用HTTP类型的网络请求,因为HTTP类型的网络请求存在安全隐患。很不幸,我们的Apache服务器使用的就是HTP协议。
为了让应用程序能够支持HTTP,我们还需要进行相应配置:res目录 —> xml目录 —> 创建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>
这段配置文件将允许我们的应用程序以明文的方式在网络上传输数据,而HTTP使用的就是明文传输的方式。最后,修改AndroidManifest.xml中的代码,让应用程序启用我们刚才创建的network_config.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
· · ·
<application
· · ·
android:networkSecurityConfig="@xml/network_config">
· · ·
</application>
</manifest>
至此,在虚拟机中运行我们的应用程序,点击“通过OkHttp发送请求”按钮:
可以看到我们已经成功将XML文件中的指定内容解析出来了:
下面,我们尝试使用SAX解析的方式实现对XML文件的解析。如果想使用SAX解析,我们通常需要新建一个类继承自DefaultHandler(),然后重写父类的5个方法。新建一个ContentHandler类:
class ContentHandler : DefaultHandler() {
private var nodeName = ""
private lateinit var id: StringBuilder
private lateinit var name: StringBuilder
private lateinit var version: StringBuilder
private val tag: String = "ContentHandler"
//开始解析XML文件时调用
override fun startDocument() {
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
//开始解析XML文件中的某个节点时调用
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) {
// 根据当前节点名判断将解析出的内容添加到哪一个StringBuilder对象中
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) {
// 若app节点已经解析完成
if ("app"== localName) {
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() {
super.endDocument()
}
}
然后我们修改MainActivity.kt文件,增加一个通过SAX解析XML数据的方法:
class MainActivity : AppCompatActivity() {
· · ·
// 通过OkHttp发送Http请求
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://10.0.2.2/get_data.xml") //指定服务器地址为本地计算机
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseXMLWithSAX(responseData) // 通过SAX解析XML数据
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
· · ·
/**
* 通过SAX解析XML数据
*/
private fun parseXMLWithSAX(xmlData: String) {
try {
// SAXParserFactory工厂对象
val factory = SAXParserFactory.newInstance()
// 通过工厂对象创建SAX解析器 然后通过解析器获取XMLReader对象
val xmlReader = factory.newSAXParser().xmlReader
val mHandler = ContentHandler()
// 将ContentHandler的实例设置到XMLReader上
xmlReader.contentHandler = mHandler
// 开始执行解析
xmlReader.parse(InputSource(StringReader(xmlData)))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
重新运行程序,重复之前使用Pull方式解析XML文件的步骤,你会看到我们已经成功将XML文件解析出来了!
不同于XML这种基于标签的复杂格式,JSON是一种基于键值对的格式,结构相对简单。相比XML,JSON的体积更小,在网络传输中更省流量。
这次我们在Apache服务器的目录下创建一个名为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": "Lash Royale"}]
在浏览器中键入以下地址并访问:
这里就说明我们的Apache服务器已经配置成功,并且可以成功访问到目标JSON文件了!
JSONObject是官方提供的用于处理JSON格式文件的库,而GSON则是由Google提供的库。我们先来学习一下如何使用JSONObject对JSON文件进行解析。首先修改MainActivity.kt文件,增加一个通过JSONObject解析JSON数据的方法:
class MainActivity : AppCompatActivity() {
· · ·
// 通过OkHttp发送Http请求
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://10.0.2.2/get_data.json") // 本地计算机中的JSON文件
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseJSONWithJSONObject(responseData) // 通过JSONObject解析JSON数据
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
· · ·
/**
* 通过JSONObject解析JSON数据
*/
private fun parseJSONWithJSONObject(jsonData: String) {
try {
// 将服务器返回的数据解析成JSON数组
val jsonArray = JSONArray(jsonData)
// 遍历JSON数组
for(i in 0 until jsonArray.length()){
// 当前元素(JSONObject)
val jsonObject = jsonArray.getJSONObject(i)
//从JSON对象中获取相应的值
val id = jsonObject.getString("id")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d(tag, "id is : $id")
Log.d(tag, "name is : $name")
Log.d(tag, "version is : $version")
}
}catch (e:Exception){
e.printStackTrace()
}
}
}
使用JSONObject解析JSON文件的流程:将服务器返回的数据转为JSON数组 —> 遍历JSON数组 —> 获取当前的元素(JSONObject) —> 从元素中取出相应字段的值。
在虚拟机中运行我们的应用程序,点击“通过OkHttp发送请求”按钮。可以看到,我们已经成功将JSON文件中的数据解析出来了!
使用JSONObject解析JSON非常简单,但是Google提供的GSON库会让解析JSON数据变得更加容易!GSON的强大之处在于它可以将一段JSON数据自动映射成一个对象,不需要我们手动编写代码进行解析。
例如我们要解析一段如下所示的JSON数据:
{"name":"Tom","age":20}
那么我们就可以定义一个Person类,并创建name和age这两个字段。然后只需要简单调用如下代码就可以将上面这条JSON数据自动解析成一个Person对象了:
// Gson对象
val gson = Gson()
// 将JSON数据转化为Person类的实例
val person = gson.fromJson(jsonData, Person::class.java)
如果我们要解析一段如下所示的JSON数组:
[{ "name":"Tom", "age":20 }, { "name":"Jack", "age":25 }, { "name":"Lily", "age":22 }]
我们需要借助TypeToken将期望解析成的数据类型传入fromJson()方法中,如下所示:
// typeOf用于告诉Gson我们要将JSON数据解析成哪种类型(这里指的是List类型)
val typeOf = object : TypeToken<List<Person>>() {}.type
// 将服务器返回的JSON数据转为List对象
val people = gson.fromJson(List<Person>(jsonData, typeOf))
以上就是GSON的基本用法了,下面就让我们在项目中尝试使用GSON解析JSON数据。首先需要①添加GSON库的依赖,编辑build.gradle.kts文件:
dependencies {
· · ·
implementation("com.google.code.gson:gson:2.8.5")
}
②新增一个App类,并在其中声明id、name和version这三个字段:
class App(val id: String, val name: String, val version: String) {
}
修改MainActivity.kt文件,③增加一个通过GSON解析JSON数据的方法:
class MainActivity : AppCompatActivity() {
· · ·
// 通过OkHttp发送Http请求
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://10.0.2.2/get_data.json") // 本地计算机中的JSON文件
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseJSONWithGSON(responseData) // 通过GSON解析JSON数据
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
· · ·
/**
* 通过GSON解析JSON数据
*/
private fun parseJSONWithGSON(jsonData: String) {
val gson = Gson()
// typeOf用于告诉Gson我们要将JSON数据解析成哪种类型(这里指的是List类型)
val typeOf = object : TypeToken<List<App>>() {}.type
// 将服务器返回的JSON数据转为List对象
val appList = gson.fromJson<List<App>>(jsonData, typeOf)
for (app in appList) {
Log.d(tag, "id is : ${app.id}")
Log.d(tag, "name is : ${app.name}")
Log.d(tag, "version is : ${app.version}")
}
}
}
现在重新运行程序,重复前面验证JSONObject解析JSON数据的步骤,你会在日志中得到相同的结果。
前面的内容我们学习了如何使用HttpURLConnection和OkHttp来发送HTTP请求,然后学习了如何解析服务器返回的XML和JSON格式数据。但是,在一个应用程序中存在很多需要使用网络功能的地方,而发送HTTP网络请求的代码基本是相同的。如果每次都编写一次发送HTTP请求的代码,无疑会使我们的代码变得十分臃肿。
我们可以通过将这些通用的网络操作封装到一个工具类中(例如:HttpUtil),并提供一个通用的方法(例如:sendHttpRequest())。这样当需要发起网络请求的时候,只需要调用这个网络工具类中的方法就可以了。例如下面的代码:
String address = "http://www.baidu.com"
String response = HttpUtil.sendHttpRequest(address)
由于所有的网络请求都需要放在子线程中去执行,所以我们必须在HttpUtil.sendHttpRequest()方法中开启一个子线程,然后将网络请求操作都放到子线程中去执行。但这里存在一个问题,当我们在主线程中调用HttpUtil.sendHttpRequest()方法发送网络请求时,实际上相关操作在子线程中执行的。但主线程并不会等待子线程中HTTP请求操作完成,而是会继续执行主线程中的其他代码,所以也就无法获取到服务器返回的响应数据。为了解决这个问题,我们需要使用编程语言的回调机制。
为了实现网络请求的回调,我们需要先定义一个接口。这里新建一个HttpCallbackListener接口:
interface HttpCallbackListener {
// 服务器成功响应网络请求时调用(参数表示服务器返回的数据)
fun onFinish(response: String)
// 网络操作出现错误时调用(参数表示错误信息)
fun onError(e: Exception)
}
然后我们通过object关键字创建一个单例的网络请求工具类HttpUtil.kt,这样我们就可以在代码中的任意位置通过类名 . 方法名的方式进行调用了:
// 单例类
object HttpUtil {
/**
* 通过HttpURLConnection发送网络请求并实现结果的回调
*/ 网络请求地址参数 回调接口参数
fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.let {
it.connectTimeout = 8000
it.readTimeout = 8000
}
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
reader.use {
reader.forEachLine {
response.append(it)
}
}
// 回调HttpCallbackListener的onFinish()方法 传入服务器返回的数据
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
// 回调HttpCallbackListener的onError()方法
listener.onError(e)
} finally {
// 不论是否请求成功 HttpURLConnection最终都要断开Http连接
connection?.disconnect()
}
}
}
}
需要注意,在子线程中不能直接通过return语句返回数据到主线程中。这是因为子线程和主线程拥有不同的执行路径和堆栈,它们不能直接共享返回值。
但我们需要通过回调的方式,在子线程中将服务器响应的数据返回给主线程。我们给sendHttpRequest()方法添加了一个HttpCallbackListener回调接口参数,然后在方法内开启一个子线程,并在子线程中执行具体的网络操作。
现在HttpUtil.sendHttpRequest()方法接收两个参数:①网络请求地址address和②HttpCallbackListener接口实例。如果我们想要调用sendHttpRequest(),就可以按照下面的方式:
网络请求url HttpCallbackListener接口实例
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
TODO("response是网络请求成功后服务器返回数据")
}
override fun onError(e: Exception) {
TODO("网络请求异常处理")
}
})
可以看到,我们定义了一个匿名内部类,实现了HttpCallbackListener接口并重写了onFinish()和onError()方法。这样,当服务器成功响应时,我们就可以在onFinish()方法中对服务器返回的数据进行处理。同理,若网络请求出现异常,可以在onError()方法中进行处理。这样,我们就利用回调机制,成功将服务器响应的数据(子线程)返回给调用方(主线程中)了。
通过HttpURLConnection请求网络,并通过回调机制将数据返回给调用方的这种方式,写起来还是比较复杂的。下面我们来学习一下如何在OkHttp中实现回调功能。
首先,我们需要在HttpUtil中增加一个sendOkHttpRequest()方法:
object HttpUtil {
· · ·
/**
* 通过OkHttp发送网络请求并实现结果的回调
*/ 网络请求url OkHttp自带的回调接口
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
// enqueue()方法会自动开启子线程执行HTTP请求 然后将服务器响应的结果返回到okhttp3.Callback中
client.newCall(request).enqueue(callback)
}
}
可以看到,我们先是使用client和request对象创建一个新的HTTP请求。
client.newCall(request)
然后调用enqueue()方法将新的HTTP请求加入到队列中,并使用前面传入的callback对象处理服务器的响应。
.enqueue(callback)
如果我们想要调用sendOkHttpRequest(),就可以按照下面的方式:
HttpUtil.sendOkHttpRequest("address", object : okhttp3.Callback {
override fun onResponse(call: Call, response: Response) {
// 取出服务器返回的数据
val responseData = response.body?.string()
}
override fun onFailure(call: Call, e: IOException) {
TODO("网络请求异常处理")
}
})
可以看到OkHttp对网络请求回调的实现及使用是十分简单的,不过有一点你需要注意。不论是HttpURLConnection还是OkHttp,最终的回调接口都还是在子线程中运行的。因此,我们不可以在回调接口的代码段中执行任何UI操作,除非你借助runOnUiThread()方法回到主线程(UI线程)中。
Retrofit是一个基于OkHttp的网络请求库,它是对OkHttp的进一步封装,使得网络请求变得更加简单和方便。OkHttp是底层网络请求的封装与优化库,而Retrofit是在OkHttp的基础上进一步开发出来的上层(应用层)网络通信库。
Retrofit的设计思想是基于以下几个事实:
基于以上几点设计思想,我们在使用Retrofit时可以:
新建一个RetrofitTest项目,在build.gradle.kts(:app)文件中添加Retrofit相关依赖:
dependencies {
· · ·
// Retrofit + Okttp + Okio
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// GSON + Retrofit转换库(借助GSON 将服务器返回的JSON数据自动解析成对象)
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
增加一个App类,声明id、name、version这三个字段:
class App(val id: String, val name: String, val version: String) {
}
由于我们的Apache服务器上只有一个获取JSON数据的接口,因此我们只需要定义一个接口文件,并包含一个方法即可。在实际项目中,你可以根据服务器接口的功能进行归类,创建不同种类的接口文件,并在其中定义具体的服务器接口的方法。这里我们就新建一个AppService接口:
// 注意 这里是Retrofit包中的Call
import retrofit2.Call
import retrofit2.http.GET
// 接口文件名规 范:以功能种类名开头 + Service结尾
interface AppService {
// 当调用getAppData()方法时会发起一条GET请求
@GET("get_data.json")
fun getAppData(): Call<List<App>>
}
在AppService接口中,我们定义了一个getAppData()方法,该方法会返回一个Call对象。这个Call对象可以用于执行网络请求(这里是GET)并获取服务器返回的结果,我们通过泛型指定服务器返回的数据应该被Retrofit转换成什么对象。由于我们的get_data.json数据是一个包含App数据的JSON数组,所以我们期望返回的类型是List< App >。
这里的@GET(“get_data.json”)注解语句表示我们在调用getAppData()方法时,会发送一条GET请求。请求的地址 = 根路径 + 注解参数路径,在@GET注解中我们只需要传入相对路径即可,根路径我们稍后会进行配置。
接下来我们在主界面中添加一个按钮getAppDataBtn,然后在MainActivity.kt中实现它的点击事件:
class MainActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityMainBinding
private val tag: String = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
//getAppDataBtn按钮点击事件
mBinding.getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder() // 构建Retrofit实例
.baseUrl("http://10.0.2.2/") // 所有通过Retrofit发起的网络请求的根路径(本地计算机)
.addConverterFactory(GsonConverterFactory.create()) // 使用什么转换库解析JSON数据
.build()
val appService = retrofit.create(AppService::class.java) // 创建网络服务接口实例
// 发起网络请求并处理响应
// Retrofit会在发起请求时自动在内部开启子线程 当数据回调后又会自动回到主线程
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()
}
})
}
}
}
然后别忘了在AndroidManifest.xml文件中添加网络权限,并且加载允许应用程序发送明文网络请求的配置文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--网络权限-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许应用程序以明文的方式在网络上传输数据-->
<application
· · ·
android:networkSecurityConfig="@xml/network_config">
· · ·
</application>
</manifest>
可以看到,我们使用Retrofit的大致流程是:①使用Retrofit.Builder()构建Retrofit实例—>②通过Retrofit实例中的create()方法创建网络服务接口实例—>③调用网络接口实例中的方法发起网络请求并调用enqueue()方法配置Callback回调—>④在Callback回调中重写onResponse()方法和onFailure()方法处理服务器响应。
运行程序,点击Get App Data按钮,观察日志输出台可以看到我们已经成功通过Retrofit框架发送网络请求,并将服务器返回的JSON数据解析出来了:
定义一个Data类,声明id和content这两个字段:
class Data(val id: String, val content: String) {
}
先来看一个较为简单的服务器接口地址:
GET http://example.com/get_data.json
这种情况的接口地址是静态的,并不会发生变化,对应Retrofit框架的写法如下:
interface ExampleService {
@GET("get_data.json")
fun getData(): Call<Data>
}
这就是我们上节学习过的内容,可惜服务器不会总是给我们这么简单的静态类型接口。
在很多场景下,接口地址中的部分内容是会动态变化的,例如:
GET http://example.com/<page>/get_data.json
这个接口中的< page >部分代表页数,我们传入不同的页数,服务器返回的数据也是不同的。这种类型的接口地址对应Retrofit中应该这样写:
interface ExampleService {
路径参数
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call<Data> // 通过@Path注解将page声明为
}
这里GET注解的参数是"{page}/get_data.json",我们使用{page}结构来表示这是一个路径参数。在getData()方法的参数中通过@Path(“page”)注解声明这个路径参数,意味着getData()方法中参数page的值将来自GET注解参数路径中的{page}部分。
通过这种方式,当我们调用getData()方法发送网络请求时,就会自动将getData()方法参数中page的值替换到@GET注解的路径参数上,从而组成一个完整的请求地址。
很多时候,服务器接口还会要求我们传入一些参数:
GET http://example.com/get_data.json?u=<user>&t=<token>
上面这个例子是一个标准的带参数的GET请求格式,接口地址的最后使用问号连接参数部分。每个参数都是一个使用等号连接的建值对,多个参数之间使用“&”号进行分隔。
从上面的例子中我们可以得知:u和t是查询参数的名称也就是键,而< user >和< token >是这些键的值,二者组成一个建值对。也就是说,服务器期望从该请求中接收两个参数:一个名为u的参数其值为< user >;另一个名为t的参数其值为< token >。这种类型的接口地址对应Retrofit中应该这样写:
interface ExampleService {
//使用@Query注解来将user参数作为查询参数添加到URL中它的键为"u"
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}
在getData()方法中,我们使用@Query注解声明了user和token这两个参数。@Query注解会将它们作为查询参数添加到URL中,而@Query(“x”)注解中的x就是相应的键。当发起网络请求时,Retrofit会自动将这两个参数构建到请求地址当中。
当然HTTP并不是只有GET请求,还有POST、PUT、PATCH、DELETE请求。
比如服务器提供了如下接口:
DELETE http://example.com/data/<id>
这种接口通常意味着要根据id删除一条指定的数据。我们在Retrofit中想要发起这样的请求,就可以这样写:
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id") id: String): Call<ResponseBody>
}
这里我们通过@DELETE注解发出一条DELETE类型的请求,并在deleteData()方法中通过@Path注解来动态指定id的值。需要注意的是,我们将Call的泛型指定为了ResponseBody而不是Data。这是因为POST、PUT、PATCH、DELETE这几个类型的请求与GET请求不同,它们主要用于对服务器中的数据进行操作,而不是单纯的从服务器中获取数据。所以他们通常期望返回一个表示操作成功或失败的状态消息,也就是"响应体"(ResponseBody)。
如果我们需要向服务器提交数据该怎么写呢?例如下面的接口地址:
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 sendData(@Body data: Data): Call<ResponseBody>
}
我们在sendData()方法中使用@Body注解声明了一个Data类型的参数。当调用sendData()方法发送POST请求时,就会将Data对象中的数据转为JSON格式,然后放入HTTP请求的body中。服务器收到请求后只需要从body中将数据解析出来即可,这种写法也同样适用于PUT、PATCH、DELETE类型的请求提交数据。
有些服务器的接口还会要求我们在HTTP请求的header中指定参数,例如:
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 getDataWithHeaders(): Call<Data>
}
但是这种写法只能进行静态header声明,如果想要动态指定header的值,则需要使用@Header注解 (注意与@Headers不同):
interface ExampleService {
@GET("get_data.json")
fun getDataWithHeaders(
@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String
): Call<Data>
}
现在当发起网络请求时,Retrofit会自动将参数中传入的值设置到User-Agent和Cache-Control这两个header当中,从而实现了动态指定header值的功能。
在前面的内容中,我们获取Serice接口的动态代理对象是这么写的:
val retrofit = Retrofit.Builder() // 构建Retrofit实例
.baseUrl("http://10.0.2.2/") // 所有通过Retrofit发起的网络请求的根路径
.addConverterFactory(GsonConverterFactory.create()) // 使用什么转换库解析JSON数据
.build()
val appService = retrofit.create(AppService::class.java) // 创建网络服务接口实例
为了得到AppService的动态代理对象,我们需要先通过Retrofit.Builder()构建一个Retrofit对象,然后再调用Retrofit对象的create()方法创建动态代理对象。其实我们的Retrofit对象是全局通用的,因此我们只需要在调用create()方法时针对不同的Service接口传入相应的Class类型即可。我们可以将这部分通用功能封装起来,从而简化获取Service接口动态代理对象的过程。
新建一个ServiceCreator单例类:
object ServiceCreator {
// 根目录常量
private const val BASE_URL = "http://10.0.2.2/"
//Retrofit实例
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
// 通用的泛型参数create()方法
fun <T> create(serviceClass: Class<T>): T {
return retrofit.create(serviceClass)
}
}
我们将根目录常量BASE_URL和Retrofit实例都声明为private,相当于对外部而言它们都是不可见的。然后我们声明了一个暴露给外部的create()方法,并接收一个Class类型的参数。这样在外部调用create()方法时,实际上就调用了Retrofit实例的create()方法,从而茶ungjianchu相应Service接口的动态代理对象。
现在,如果我们想获取一个AppService接口的动态对象,只需要这样写:
val appService = ServiceCreator.create(AppService::class.java)
假如我们又创建了一个名为OrderService的接口,如果想获取它的动态对象只需要这样写:
val appService = ServiceCreator.create(OrderService::class.java)
这样我们就可以随意调用AppService、OrderService接口中定义的任何方法了。