Kotlin第五章:网络编程
我正在参加「启航方案」
1. Android网络编程
- OkHttp
OkHttp是一个高效的HTTP客户端,它的横空出世,让其他的网络恳求结构都变得黯然失色。
- Retrofit
Retrofit是一个根据OkHttp的RESTful网络恳求结构,功用强壮、简洁易用及高可拓展性。Retrofit说起来适当简略,简略到源码只要37个文件,其中22个文件是注解,还都和HTTP有关,真实露出给用户的类并不多。
- 封装
Retrofit其实就是一个根据OKHttp的网络恳求结构的封装。使恳求接口和数据解析愈加简洁明了。为什么需求封装呢?说白了,就是为了解耦,为了便利日后切换到不同结构完成,而无需到处修改调用的当地。
比方咱们项目傍边常常会用到一些之前比较盛行的的网络结构,后期这个结构中止维护或许功用无法满足业务需求,咱们想切换到新的结构,或许调用的当地会非常多,假如不做封装,直接切换的话,改动量将非常非常大,并且还很有或许会有遗失,风险度非常高。
OkHttp是一个HTTP骑牛引擎 ,担任任何底层网络操作,缓存,恳求和呼应操作等
Retrofit是在OkHttp之上构建的高档REST笼统。使恳求接口和数据解析愈加简洁明了
2. OkHttp
1. 呈现背景
在okhttp呈现以前,android上建议网络恳求要么运用系统自带的HttpClient
、HttpURLConnection
、要么运用google开源的Volley
、要么运用第三方开源的AsyncHttpClient
, 随着互联网的发展,APP的业务发展也越来越复杂,APP的网络恳求数量急剧增加,但是上述的网络恳求结构均存在难以功能和并发数量的约束
OkHttp
盛行得益于它的良好的架构设计,强壮的拦截器(intercepts)
使得操纵网络非常便利;OkHttp现在现已得到Google官方认可,大量的app都选用OkHttp做网络恳求,其源码详见OkHttp Github。
也得益于强壮的生态,大量的盛行库都以OkHttp
作为底层网络结构或供给支撑,比方Retrofit
、Glide
、Fresco
、Moshi
、Picasso
等。
当OKhttp问世之后,瞬间成为各个公司的开发者的新宠,终年霸占github star榜单,okhttp能够说是为高效而生,投合了互联网高速发展的需求
2. 特色
1.一起支撑HTTP1.1与支撑HTTP2.0;
2.一起支撑同步与异步恳求;
3.一起具备HTTP与WebSocket功用;
4.具有主动维护的socket衔接池,削减握手次数;
5.具有行列线程池,轻松写并发;
6.具有Interceptors(拦截器),轻松处理恳求与呼应额定需求(例:恳求失利重试、呼应内容重定向等等);
3. 运用
1. 增加网络拜访权限
在AndroidManifest.xml 中增加网络拜访权限
<uses-permission android:name="android.permission.INTERNET" />
2. 增加相关的依赖
在
app/build.gradle
的dependencies
下增加依赖
implementation("com.squareup.okhttp3:okhttp:4.9.0")
// 网络恳求日志打印
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
3. Get恳求
1. 同步Get恳求
创立一个
OkHttpDemoTest.kt
文件,选用object
关键字使本类在整个程序运转期间只要一个示例,适当所以单例模式,然后安卓规定网络恳求不能在主线程,所以咱们的get恳求需求新起一个线程运转
- 创立kt文件
package com.example.myapplication.http
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
// 程序运转的时分只要一份,适当所以单例模式
// 能够直接运用 OkHttpDemoTest1.get()调用
object OkHttpDemoTest1{
private val client = OkHttpClient.Builder()
.connectTimeout(10,TimeUnit.SECONDS) // 衔接超时时刻
.readTimeout(10,TimeUnit.SECONDS) //读取超时
.writeTimeout(10,TimeUnit.SECONDS) // 恳求超时
.build()
fun get(url: String){
Thread(Runnable {
val request = Request.Builder()
.url(url)
.build()
// 结构恳求目标
val call = client.newCall(request)
// 建议同步恳求
val response = call.execute()
// 获取恳求的回来信息
val body = response.body?.string()
// Log.e是安卓自带的一个打印日志的办法,日志信息会打印到logcat里
Log.e("OkHttp $this","get response: $body")
}).start()
}
}
- 在
MainActivity
中调用
package com.example.myapplication
import android.os.Bundle
import com.example.myapplication.databinding.ActivityMainBinding
import com.example.myapplication.http.OkHttpDemoTest1
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 中心代码省略
val url = "http://123.56.232.18:8080/serverdemo/user/query?userId=1"
OkHttpDemoTest1.get(url)
}
}
- 假如创立运用的时分是高版别的安卓的话是不支撑http恳求的,有必要要运用https,在
AndroidManifest.xml
的application
标签中增加相应开关
<application
android:usesCleartextTraffic="true">
</application>
2. 异步Get恳求
异步恳求是没有回来值的,需求完成callback办法来操作
- 创立办法
// 异步恳求
fun getAsync(url: String){
val request = Request.Builder()
.url(url)
.build()
// 结构恳求目标
val call = client.newCall(request)
// 运用enqueue 建议异步恳求
val response = call.enqueue(object: Callback{
// 失利的话会回调此办法
override fun onFailure(call: Call, e: IOException) {
Log.e("OkHttp Get Async $this","")
}
// 接口调用成功之后调用此办法
override fun onResponse(call: Call, response: Response) {
// 获取恳求的回来信息
val body = response.body?.string()
Log.e("OkHttp Get Async $this","get response: $body")
}
})
}
- 在
MainActivity
的onViewCreated
中测验
val url = "http://123.56.232.18:8080/serverdemo/user/query?userId=1"
//OkHttpDemoTest1.get(url)
OkHttpDemoTest1.getAsync(url)
- 当然也能够将获取call目标的办法封装一下
fun getClientCall(url: String): Call{
val request = Request.Builder()
.url(url)
.build()
// 结构恳求目标
return client.newCall(request)
}
3. Get恳求总结
异步恳求的过程和同步恳求相似,仅仅调用了
Call
的enqueue
办法异步恳求,结果通过回调Callback
的onResponse
办法及onFailure
办法处理。看了两种不同的Get恳求,基本流程都是先创立一个
OkHttpClient
目标,然后通过Request.Builder()
创立一个Request
目标,OkHttpClient
目标调用newCall()
并传入Request
目标就能取得一个Call
目标。而同步和异步不同的当地在于
execute()
和enqueue()
办法的调用,调用
execute()
为同步恳求并回来Response
目标;调用
enqueue()
办法测验通过callback的方式回来Response
目标。留意:无论是同步仍是异步恳求,接收到
Response
目标时均在子线程中,onFailure
,onResponse
的回调是在子线程中的,咱们需求切换到主线程才干操作UI控件
4. Post恳求
POST恳求与GET恳求不同的当地在于
Request.Builder
的post()
办法,post()
办法需求一个RequestBody
的目标作为参数
1. 同步Post恳求
- 书写恳求
fun post(url: String): Unit{
val body = FormBody.Builder()
.add("userId","123")
.add("key","value")
.add("tagId","71")
.build()
val request = Request.Builder()
.url(url)
.post(body)
.build()
val call = client.newCall(request)
// 由于传入的是一个函数,直接简略写法写到大括号里完事
Thread {
val response = call.execute()
Log.e("OkHttp Post formData ${body.toString()}","")
Log.e("OkHttp Post response","response $response")
}.start()
}
-
MainActivity
调用
val baseUrl = "http://123.56.232.18:8080/serverdemo"
OkHttpDemoTest1.post("$baseUrl/tag/toggleTagFollow")
2. 异步Post表单提交
与Get恳求相同,只需求将execute()换成enqueue()即可
- 书写办法
fun postAsyncForm(url: String){
val body = FormBody.Builder()
.add("userId","123")
.add("key","value")
.add("tagId","71")
.build()
val request = Request.Builder()
.url(url)
.post(body)
.build()
val call = client.newCall(request)
call.enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
Log.e("Post异步提交表单数据失利")
}
override fun onResponse(call: Call, response: Response) {
Log.i("Post异步提交表单成功","response $response")
}
})
}
- 调用
val baseUrl = "http://123.56.232.18:8080/serverdemo"
OkHttpDemoTest1.postAsyncForm("$baseUrl/tag/toggleTagFollow")
3. Post恳求文件上传
读取存储卡的文件需求在清单文件中声明权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
/**
* 异步post上传文件
* 在android6.0今后,读取外部存储卡的文件需求动态恳求权限
* 即时声明了权限也需求动态授权的
*/
fun postAsyncMultipart(context: Context,url: String){
val file = File(Environment.getExternalStorageDirectory(),"1.png")
if(!file.exists()){
Toast.makeText(context, "文件不存在", Toast.LENGTH_SHORT).show()
return
}
val multipartBody = MultipartBody.Builder()
.addFormDataPart("key", "value")
.addFormDataPart(
"file", "file.png",
RequestBody.create("application/octet-stream".toMediaType(), file)
)
.build()
val request = Request.Builder().url(url)
.post(multipartBody)
.build()
val call = client.newCall(request)
call.enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
Log.e("异步post恳求上传文件失利","$e")
}
override fun onResponse(call: Call, response: Response) {
Log.e("异步post恳求上传文件成功","response $response")
}
})
}
4. Post提交字符串
fun postAsyncString(url: String){
val jsonObject = JSONObject()
jsonObject.put("'key1","value1")
jsonObject.put("'key2","value2")
// 这儿假如想要提交纯文本的话需求指定的恳求头为 text/plain;charset=utf-8
val body = RequestBody.create(
"application/json;charset=utf-8".toMediaType(),
jsonObject.toString()
)
val request = Request.Builder().url(url)
.post(body)
.build()
val call = client.newCall(request)
call.enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
Log.e("OkHttp Post发送json参数失利","错误信息 $e")
}
override fun onResponse(call: Call, response: Response) {
Log.e("OkHttp Post发送json参数成功","回来结果 $response")
}
})
}
5. 拦截器
拦截器是OkHttp傍边的一个比较强壮的机制,能够监视,重写和重试调用恳求.
本次比方书写一个拦截恳求记载并日志信息输出的拦截器
- 创立拦截器
package com.example.myapplication.interceptor
import android.util.Log
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody
import okio.Buffer
// 自界说拦截器需求完成 okhttp中的接口
class LogInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// 获取恳求执行时的时刻戳
val timeStart = System.nanoTime()
// 获取调用链中的request目标
val request = chain.request()
var buffer = Buffer()
request.body?.writeTo(buffer)
val requestBodyStr = buffer.readUtf8()
Log.e("OkHttp",
String.format("Sending request %s with params 5s",request.url,requestBodyStr))
val response = chain.proceed(request)
// response 只能读取一次,后续再次读取body的时分就会报错
val responseData = response.body?.string()?: "response body null"
// 构建新的responseBody
val responseBody = ResponseBody.create(response.body?.contentType(), responseData)
val endTime = System.nanoTime()
Log.e("OkHttp","接口恳求地址为 ${request.url},接口回来的数据是 $responseData,用时${(endTime - timeStart) / 1e6}ms ")
// 回来新的 response
return response.newBuilder().body(responseBody).build()
}
}
- 运用拦截器
在上边创立client的时分增加上拦截器即可
private val client = OkHttpClient.Builder()
.connectTimeout(10,TimeUnit.SECONDS) // 衔接超时时刻
.readTimeout(10,TimeUnit.SECONDS) //读取超时
.writeTimeout(10,TimeUnit.SECONDS) // 恳求超时
.addInterceptor(LogInterceptor())
.build()
- 优化日志输出
能够看到,上边的比方输出的日志中心有许多的无用信息,所以能够优化一下日志输出,将interceptor的级别设置为Body,这样输出的日志就会美观一点
// 将client目标提取到外边
private val client : OkHttpClient
// 初始化类的时分加载这个办法
init {
// 运用okhttp自带的拦截器
val httpLoggingInterceptor = HttpLoggingInterceptor()
// 设置拦截器级别为 Body
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
// 给client增加 interceptor
client = OkHttpClient.Builder()
.connectTimeout(10,TimeUnit.SECONDS) // 衔接超时时刻
.readTimeout(10,TimeUnit.SECONDS) //读取超时
.writeTimeout(10,TimeUnit.SECONDS) // 恳求超时
.addInterceptor(httpLoggingInterceptor)
.build()
}
3. 运用Gson
1. 增加依赖
增加在
app/build.gradle
中
dependencies{
implementation 'com.google.code.gson:gson:2.8.6'
}
2. 解析json到目标
这儿类目标需求用
Account::class.java
而不是Account.class
package com.example.myapplication.http
import com.google.gson.Gson
class Account {
var uid: String = ""
var userName: String = "Freeman"
var password: String = "pwd"
var phone: String = "17663333333"
override fun toString(): String {
return "Account(uid='$uid', userName='$userName', password='$password', phone='$phone')"
}
}
fun main() {
val jsonStr = """
{
"uid": "123",
"userName": "test",
"password": "pwd",
"phone": "16666666666"
}
""".trimIndent()
var gson = Gson()
var fromJson = gson.fromJson<Account>(jsonStr, Account::class.java)
println(fromJson.toString())
}
3. 目标转Json
var toJson = gson.toJson(fromJson)
println(toJson)
4. 集合转json
val jsonArrayStr = """
[{
"uid": "123",
"userName": "test",
"password": "pwd",
"phone": "16666666666"
}]
""".trimIndent()
var fromJsonArray: List<Account> =
gson.fromJson(jsonArrayStr, object : TypeToken<List<Account>>(){}.type)
println(fromJsonArray)
5. 优化实体类
运用class 界说类的时分,类里边的字段需求有初始值,不是很便利运用,所以这个时分能够用
data class
,并且还不用写toString()办法
data class Account2(
val uid: String = "111",
val userName: String,
val password: String,
val phone: String
)
fun main(){
var fromJson2 = gson.fromJson(jsonStr, Account2::class.java)
println(fromJson2)
}
6. JsonToKotlinClass插件
File –> plugins–> JsonToKotlinClass插件下载,快捷键是 alt + K,或许右键找到 generate然后选择
kotlinm data class from JSON
{
"status": 200,
"message": "成功",
"data": {
"data": {
"id": 3117,
"userId": 160093269,
"name": "qvelychubby",
"avatar": "",
"description": "更多android进阶课程请在慕课授索lovelychubby",
"likeCount": 985,
"topCommentCount": 200,
"followCount": 100,
"followerCount": 10,
"qqOpenId": "A8747C32A5D614281E65DA5B473D1F31",
"expires_time": 1640266383000,
"score": 1000,
"historyCount": 10,
"commentCount": 3,
"favoriteCount": 0,
"feedCount": 10,
"hasFollow": false
}
}
}
- 生成实体类
// 主动生成的是没有泛型的,这儿能够直接增加上泛型
// 后续直接改泛型就行了,就不需求改实体类了
data class Result<T>(
val data: Data<T>,
val message: String,
val status: Int
)
data class Data<T>(
val data: T
)
data class UserInfo(
val avatar: String,
val commentCount: Int,
val description: String,
val expires_time: Long,
val favoriteCount: Int,
val feedCount: Int,
val followCount: Int,
val followerCount: Int,
val qqOpenId: String,
val hasFollow: Boolean,
val historyCount: Int,
val id: Int,
val likeCount: Int,
val name: String,
val score: Int,
val topCommentCount: Int,
val userId: Int
)
fun main(){
val responseJson = """ 上边的json """
var result = gson.fromJson<Result<UserInfo>>(responseStr, Result::class.java)
println(result)
}
4. Retrofit
Retrofit
是一个高质量高效率的HTTP恳求库,是一个restful的恳求库,和OkHttp
同样出自Square公司。Retrofit内部依赖于OkHttp,它将OKHttp底层的代码和细节都封装了起来,功用上做了更多的扩展,比方回来结果的主动解析,网络引擎的切换,拦截器……有了Retrofit之后对于一些恳求咱们就只需求一行代码或许一个注解、大大简化了网络恳求的代码量。
1. 注解
etrofit注解驱动型上层网络恳求结构,运用注解来简化恳求,大体分为以下几类:
- 用于标注网络恳求方式的注解
- 标记网络恳求参数的注解
- 用于标记网络恳求和呼应格局的注解
interface ApiService{
@GET("user/query")
Call<User> queryUser(@Query("userId") String userId);
}
val mApi = retrofit.create(ApiService.class);
val response = mApi.queryUser("100086").execute()
1. 恳求办法注解
序号 | 注解 | 阐明 |
---|---|---|
1 | @GET | get恳求 |
2 | @POST | post恳求 |
3 | @PUT | put恳求 |
4 | @DELETE | delete恳求 |
5 | @PATCH | patch恳求,该恳求是对put恳求的补充,用于更新部分资源 |
6 | @HEAD | head恳求 |
7 | @OPTIONS | option恳求 |
8 | @HTTP | 通用注解,能够替换以上一切的注解,其具有三个属性:method,path,hasBody |
2. 恳求头注解
注解 | 阐明 |
---|---|
@Headers | 用于增加固定恳求头,能够一起增加多个。通过该注解增加的恳求头不会相互掩盖,而是共同存在 |
@Header | 作为办法的参数传入,用于增加不固定值的Header,该注解会更新已有的恳求头 |
3. 恳求参数注解
称号 | 阐明 |
---|---|
@Body | 多用于post恳求发送非表单数据,比方想要以post方式传递json格局数据 |
@Filed | 多用于post恳求中表单字段,Filed和FieldMap需求FormUrlEncoded结合运用 |
@FiledMap | 和@Filed作用共同,用于不确定表单参数 |
@Part | 用于表单字段,Part和PartMap与Multipart注解结合运用,合适文件上传的情况 |
@PartMap | 用于表单字段,默许承受的类型是Map,可用于完成多文件上传 |
@Path | 用于url中的占位符 |
@Query | 用于Get中指定参数 |
@QueryMap | 和Query运用相似 |
@Url | 指定恳求途径 |
4. 恳求和呼应格局注解
称号 | 阐明 |
---|---|
@FormUrlEncoded | 表示恳求发送编码表单数据,每个键值对需求运用@Field注解 |
@Multipart | 表示恳求发送multipart数据,需求合作运用@Part |
@Streaming | 表示呼运用字节流的方式回来.假如没运用该注解,默许会把数据全部载入到内存中.该注解在在下载大文件的特别有用 |
2. 运用
1. 引进依赖
在
app/build.gradle
中增加
// 引进 retrofit结构
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
2. 初始化
- 创立东西类
baseUrl有必要以 / 结束,不然会报错
package com.example.myapplication.http
import com.example.myapplication.interceptor.LogInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitUtil {
// 创立 okhttp的client,能够在这儿增加拦截器等
private val client = OkHttpClient.Builder()
.connectTimeout(60,TimeUnit.SECONDS)
.readTimeout(60,TimeUnit.SECONDS)
.writeTimeout(60,TimeUnit.SECONDS)
.addInterceptor(LogInterceptor())
.build()
// 运用okhttp自带的日志输出
private var clientWithHttpLoggingInterceptor = OkHttpClient()
init {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
clientWithHttpLoggingInterceptor = OkHttpClient.Builder()
.connectTimeout(10,TimeUnit.SECONDS) // 衔接超时时刻
.readTimeout(10,TimeUnit.SECONDS) //读取超时
.writeTimeout(10,TimeUnit.SECONDS) // 恳求超时
.addInterceptor(httpLoggingInterceptor)
.build()
}
// 创立出来 retrofit的目标
private var retrofit = Retrofit.Builder()
// 这儿能够运用自己界说的client
.client(clientWithHttpLoggingInterceptor)
// 留意,这儿有必要以 / 结束,不然会报错
.baseUrl("http://123.56.232.18:8080/serverdemo/")
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(clazz: Class<T>): T{
return retrofit.create(clazz)
}
}
- 创立接口
package com.example.myapplication.http
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiServiceKotlin {
@GET(value = "user/query")
fun queryUser(
@Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}
- 在
MainActivity
中运用
retrofit结构现已帮助咱们完成了线程切换,在这儿能够直接操作主线程了
var serviceKotlin: ApiServiceKotlin = RetrofitUtil.create(ApiServiceKotlin::class.java)
serviceKotlin.queryUser("123456")
.enqueue(object: Callback<Result<UserInfo>> {
override fun onResponse(
call: retrofit2.Call<Result<UserInfo>>,
response: retrofit2.Response<Result<UserInfo>>
) {
Log.e("Retrofit Success","'$response")
}
override fun onFailure(call: retrofit2.Call<Result<UserInfo>>, t: Throwable) {
Log.e("Retrofit",t.message?: "unknown reason")
}
})
3. 测验用例
- java格局
public interface ApiService {
@GET("user/query")
Call<User> queryUser(@Query("userId") String userId);
//运用@Headers增加多个恳求头
@Headers({"User-Agent:android", "apikey:123456789", })
@GET("user/query")
Call<User> queryUser(@Query("userId") String userId);
// 多个参数的情况下能够运用@QueryMap,但只能用在GET恳求上
@GET("user/query"")
Call<User> queryUser(@QueryMap Map<String, String> params);
/**
* 许多情况下,咱们需求上传json格局的数据。当咱们注册新用户的时分,由于用户注册时的数据相对较多
* 并或许今后会变化,这时分,服务端或许要求咱们上传json格局的数据。此时就要@Body注解来完成。
* 直接传入实体,它会自行转化成Json, @Body只能用在POST恳求上
*
* 字符串提交
*/
@POST("user/update")
Call<User> update(@Body News post);
/**
* 表单提交(键值对提交)
*/
@POST()
@FormUrlEncoded
Call<User> executePost(@FieldMap Map<String, Object> maps);
/**
* 表单上传文件(键值对提交、一起上传文件)
*/
@Multipart
@POST("upload/upload")
Call<> register(@Field("openId") String openId, @PartMap Map<String, MultipartBody.Part> map);
}
- kotlin 版别
interface ApiServiceKotlin {
@GET(value = "user/query")
fun queryUser(
@Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}
tring> params);
/**
* 许多情况下,咱们需求上传json格局的数据。当咱们注册新用户的时分,由于用户注册时的数据相对较多
* 并或许今后会变化,这时分,服务端或许要求咱们上传json格局的数据。此时就要@Body注解来完成。
* 直接传入实体,它会自行转化成Json, @Body只能用在POST恳求上
*
* 字符串提交
*/
@POST("user/update")
Call<User> update(@Body News post);
/**
* 表单提交(键值对提交)
*/
@POST()
@FormUrlEncoded
Call<User> executePost(@FieldMap Map<String, Object> maps);
/**
* 表单上传文件(键值对提交、一起上传文件)
*/
@Multipart
@POST("upload/upload")
Call<> register(@Field("openId") String openId, @PartMap Map<String, MultipartBody.Part> map);
}
- kotlin 版别
```kotlin
interface ApiServiceKotlin {
@GET(value = "user/query")
fun queryUser(
@Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}