第十一章:看看精彩的世界,使用网络技术
注意,使用网络需要在权限中声明
目录
WebView
当我们需要在应用程序里显示一些网页时就可以使用这个控件。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView.settings.javaScriptEnabled = true
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
}
使用HTTP访问网络
原理:客户端向服务器发出一条HTTP请求,服务器收到请求之后返回一些数据给客户端,客户端再将这些数据进行解析和处理。
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
//设置HTTP请求所使用的方法,GET表示从服务器获取,POST表示向服务器提交
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 {
//关闭HTTP连接
connection?.disconnect()
}
}
}
private fun showResponse(response: String) {
//该方法就是对异步消息处理机制进行了一层封装,背后原理是一样的
runOnUiThread {
responseText.text = response
}
}
如果想要提交数据:
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
ouyput.writeBytes("username=admin&password=123456")
OkHttp
private fun sendRequestWithOkHttp() {
thread {
try {
//获取OkHttpClient实例
val client = OkHttpClient()
//创建Request对象
val request = Request.Builder()
.url("http://10.0.2.2/get_data.json")
.build()
//调用OkHttpClient的newCall方法来创建一个Call对象,并调用其execute方法发送请求并获取服务器返回的数据
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData!=null){
parseJSONWithGSON(responseData)
}
}catch (e : Exception){
e.printStackTrace()
}
}
}
如果是发起一条POST请求,需要构建一个Request Body对象来存放待提交的参数:
val requestBody = FormBody.Builder()
.add("username","admin")
.add("password","123456")
.build()
解析XML数据
Pull
private fun parseXMLWithPull(xmlData: String) {
try {
//创建XmlPullParserFactory实例,并借其得到xmlPullParser对象
val factory = XmlPullParserFactory.newInstance()
val xmlPullParser = factory.newPullParser()
//将服务器返回的XML数据setInput里
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){
Log.d("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
}
}
eventType = xmlPullParser.next()
}
}catch (e : Exception){
e.printStackTrace()
}
}
SAX
SAX用法比Pull复杂一些,但语义方面会更加清楚。
private fun parseXMLWithSAX(xmlData: String) {
try {
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader
val handler = ContentHandler()
xmlReader.contentHandler = handler
xmlReader.parse(InputSource(StringReader(xmlData)))
}catch (e:Exception){
e.printStackTrace()
}
}
class ContentHandler : DefaultHandler() {
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("ContentHandler", "uri is $uri")
Log.d("ContentHandler", "localName is $localName")
Log.d("ContentHandler", "qName is $qName")
Log.d("ContentHandler", "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){
Log.d("ContentHandler", "id is ${id.toString().trim()}")
Log.d("ContentHandler", "name is ${name.toString().trim()}")
Log.d("ContentHandler", "version is ${version.toString().trim()}")
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
//完成XML解析时调用
override fun endDocument() {
super.endDocument()
}
}
解析JSON数据
JSONObject
//我们在服务器中定义了一个数组,传入其中
private fun parseJSONWithJSONObject(jsonData: String) {
try {
//将数据传入JSONArray
val jsonArray = JSONArray(jsonData)
//对jsonArray中的每一个元素依次取出我们需要的数据
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("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
}catch (e:Exception){
e.printStackTrace()
}
}
GSON
GSON的强大之处在于可以将一段JSON的字符串自动映射成一个对象。
private fun parseJSONWithGSON(jsonData: String) {
val gson = Gson()
//获取数组中对象的类型
val typeOf = object : TypeToken<List<App>>(){}.type
//根据解析出的类型将JSON数据自动解析成对象
val appList = gson.fromJson<List<App>>(jsonData, typeOf)
for (app in appList){
Log.d("MainActivity", "id is ${app.id}")
Log.d("MainActivity", "name is ${app.name}")
Log.d("MainActivity", "version is ${app.version}")
}
}
网络请求回调的实现方式
一般我们应该将通用的网络操作提取到一个公共的类里,并提供一个公共的方法,每当发起网络请求时,只需要简单地调用一下这个方法即可。
如何在使用线程的提升返回响应的数据?
利用回调机制。
在HTTPURLConnection中,传入接口
//发送请求时同时传入该接口,使得
intertface HttpCallbackListener{
//服务器成功响应时调用该方法
fun onFinish(response : String)
//网络操作出现错误时调用该方法
fun onError(e : Exception)
}
在OKHTTP中,有自带的回调接口okhttp3.Callback。将execute方法改为enqueue方法,并把okhttp3.Callback参数传入即可。
注意,回调接口是在子线程中运行的,切忌在其中进行UI操作。
Android 9.0的HTTP适配问题
从Android 9.0开始,应用程序默认只允许使用HTTPS类型的网络请求,HTTP类型的网络请求被认为因为有安全隐患默认不再被支持。
新建一个xml,并配置进application中。
<?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>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_config">
...
</application>
Retrofit
Retrofit是在OkHttp的基础上进一步开放出来的应用层网络通信库,它的定位与OkHttp不同,前者侧重上册接口的封装,后者侧重底层通信的实现。
设计思想
Retrofit的设计基于以下几个事实
1.同一款应用程序中发起的网络请求绝大多数指向的是同一个服务器域名
Retrofit可以设置根路径,然后再指定服务器接口时只需使用相对路径即可
2.服务器提供的接口通常是可以根据功能来归类的
Retrofit允许我们对服务器接口进行分类,将功能同属于一类的服务器接口定义到同一个接口文件中,从而让代码架构变得更加合理。
3.开发者更习惯于“调用一个接口,获得它的返回值”这样的编码方式
我们不用关心网络通信的细节,只需要在接口文件中声明一系列方法和返回值 ,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们调用该方法是,Retrofit会自动向对应的服务器接口发起请求,并将相应的数据解析成返回值声明的类型。这就使得我们可以用更加面向对象的思维来进行网络操作。
用法
//具体功能种类名开头,Service结尾
interface AppService {
//GET注解表示会发对该路径(相对路径)发起GET请求
@GET("get_data.json")
//这里的返回值必须声明为Retrofit内置的Call类型,并通过泛型来指定服务器响应的数据应该转换成什么对象
fun getAppData() : Call<List<App>>
}
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
//使用Retrofit.Builder构建Retrofit对象
val retrofit = Retrofit.Builder()
//指定根路径
.baseUrl(BASE_URL)
//指定Retrofit解析数据用的转换库
.addConverterFactory(GsonConverterFactory.create())
.build()
//利用泛型实化获取泛型的类型信息
inline fun <reified T> create() : T = retrofit.create(T::class.java)
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val appService = ServiceCreator.create<AppService>()
//Retrofit在发起请求时自动开启子线程,数据回调到Callback后又会自动切换回主线程
appService.getAppData().enqueue(object :Callback<List<App>>{
override fun onFailure(call: Call<List<App>>, t: Throwable) {
t.printStackTrace()
}
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
val list = response.body()
if (list!=null){
for (app in list){
Log.d("MainActivity", "id is ${app.id}")
Log.d("MainActivity", "name is ${app.name}")
Log.d("MainActivity", "version is ${app.version}")
}
}
}
})
}
}
}
Retrofit中的注解
处理复杂的接口地址类型
灵活运用@Path,@Query注解
指定HTTP请求的header
静态声明
@Headers注解
动态指定
@Header注解
HTTP请求类型
GET | POST | PUT | PATCH | DELETE |
---|---|---|---|---|
从服务器获取数据 | 向服务器提交数据 | 修改服务器上的数据(全部更新) | 修改服务器上的数据(局部更新) | 删除服务器上的数据 |