Android用kotlin搭建MVVM框架(二)
继上篇文章我们继续来搭建我们的MVVM框架
我们上篇文章封装了我们的Base本章我们来封装一下我们的网络框架,网络请求是每一个项目必不可缺的东西,好的网络请求会让我们在项目开发中事半功倍,在封装之前我们先来看一下效果
这个就是网络请求成功后返回的数据,我们用的请求接口是来自鸿洋大佬的玩Android 的接口
https://www.wanandroid.com/blog/show/2
现在我们正式开始封装我们的网络请求
kotlin+协程+retrofit+rxjava2
创建一个module来放置我们的网络请求命名为NetworkModule
首先我们要创建一个RetrofitClient然后初始化我们的Retrofit的一些操作
package com.kt.network.net
import android.annotation.SuppressLint
import android.content.Context
import com.kt.NetworkModel.net.interceptor.Level
import com.kt.NetworkModel.net.interceptor.LoggingInterceptor
import com.kt.ktmvvm.lib.BuildConfig
import com.kt.ktmvvm.net.event.OkHttpEventListener
import com.kt.network.net.dns.OkHttpDNS
import com.kt.network.net.interceptor.HTTPDNSInterceptor
import com.kt.network.net.interceptor.NoNetworkInterceptor
import okhttp3.Cache
import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
import okhttp3.internal.platform.Platform
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class RetrofitClient
/**
* retrofit 初始化build
*/(var context: Context?) {
companion object {
@SuppressLint("StaticFieldLeak")
private var retrofitClient: RetrofitClient? = null
private const val DEFAULT_TIME_OUT = 15
private val sRetrofitManager: MutableMap<Int, Retrofit> = HashMap()
fun getInstance(context: Context?): RetrofitClient {
if (retrofitClient == null) {
synchronized(RetrofitClient::class.java) {
retrofitClient = RetrofitClient(context)
return retrofitClient as RetrofitClient
}
}
return retrofitClient as RetrofitClient
}
}
/**
* 创建连接客户端
*/
private fun createOkHttpClient(optimization: Boolean): OkHttpClient {
//根据需求添加不同的拦截器
if (optimization) {
//DNS 优化以及 开启缓存、无网拦截
return OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.readTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.connectionPool(ConnectionPool(8, 10, TimeUnit.SECONDS)) //添加这两行代码
.sslSocketFactory(TrustAllCerts.createSSLSocketFactory()!!, TrustAllCerts())
.hostnameVerifier(TrustAllCerts.TrustAllHostnameVerifier())
// .protocols(Collections.unmodifiableList(listOf(Protocol.HTTP_1_1)))
//alibaba dns优化
.dns(OkHttpDNS.get(context))
.addInterceptor(HTTPDNSInterceptor(context)) //不建议用这种方式,因为大型APP 域名会比较多,假设HTTPS 的话,证书会认证失败
.cache(context?.cacheDir?.let { Cache(it, 50 * 1024 * 1024L) })//缓存目录
.addInterceptor(NoNetworkInterceptor(context))//无网拦截器
// .addInterceptor(httpLoggingInterceptor)
.addNetworkInterceptor(LoggingInterceptor().apply {
isDebug = BuildConfig.DEBUG
level = Level.BASIC
type = Platform.INFO
requestTag = "Request"
requestTag = "Response"
})
// .eventListenerFactory(OkHttpEventListener.FACTORY)
.build()
} else {
//无优化版本
return OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.readTimeout(DEFAULT_TIME_OUT.toLong(), TimeUnit.SECONDS)
.connectionPool(ConnectionPool(8, 10, TimeUnit.SECONDS)) //添加这两行代码
.sslSocketFactory(TrustAllCerts.createSSLSocketFactory()!!, TrustAllCerts())
.hostnameVerifier(TrustAllCerts.TrustAllHostnameVerifier())
// .addInterceptor(httpLoggingInterceptor)
.addNetworkInterceptor(LoggingInterceptor().apply {
isDebug = BuildConfig.DEBUG
level = Level.BASIC
type = Platform.INFO
requestTag = "Request"
requestTag = "Response"
})
// .eventListenerFactory(OkHttpEventListener.FACTORY)
.build()
}
}
/**
* 根据host 类型判断是否需要重新创建Client,因为一个app 有不同的BaseUrl,切换BaseUrl 就需要重新创建Client
* 所以,就根据类型来从map中取出对应的client
*/
fun <T> getDefault(interfaceServer: Class<T>?, hostType: Int): T {
val retrofitManager = sRetrofitManager[hostType]
return if (retrofitManager == null) {
create(interfaceServer, hostType)
} else retrofitManager.create(interfaceServer!!)
}
/**
*
*/
private fun <T> create(interfaceServer: Class<T>?, hostType: Int): T {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BaseUrlConstants.getHost(hostType))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(createOkHttpClient(true))
.build()
sRetrofitManager[hostType] = retrofit
if (interfaceServer == null) {
throw RuntimeException("The Api InterfaceServer is null!")
}
return retrofit.create(interfaceServer)
}
}
然后我们的文件报错,但是先别着急,我们需在继续创建一个URL的一个管理类,方便我们在同一个项目中调用多个不用的域名或者是ip地址的url,所有,我们需要再创建一个BaseUrlConstants
package com.kt.network.net
class BaseUrlConstants {
companion object {
private const val wanandroid: String = "https://www.wanandroid.com"
fun getHost(host: Int): String {
when (host) {
1 -> return wanandroid
}
return wanandroid;
}
}
}
然后我们继续进行封装,接下来我们需要封装一个DNS 优化
package com.kt.network.net.dns
import android.content.Context
import android.util.Log
import com.alibaba.sdk.android.httpdns.HttpDns
import com.alibaba.sdk.android.httpdns.HttpDnsService
import okhttp3.Dns
import java.net.InetAddress
/**
* DNS 优化
*/
class OkHttpDNS(context: Context?) : Dns {
private val SYSTEM = Dns.SYSTEM
private var httpDns: HttpDnsService? = null
init {
httpDns = HttpDns.getService(context)
}
companion object {
private var instance: OkHttpDNS? = null
fun get(context: Context?): OkHttpDNS {
if (instance == null) {
synchronized(OkHttpDNS::class.java) {
if (instance == null) {
instance = OkHttpDNS(context)
}
}
}
return instance!!
}
}
override fun lookup(hostname: String): MutableList<InetAddress> {
//通过异步解析接⼝获取ip
val ip = httpDns?.getIpByHostAsync(hostname)
ip?.let {
val inetAddresses = listOf(InetAddress.getAllByName(ip)) as MutableList<InetAddress>
Log.e("OkHttpDns", "inetAddresses:$inetAddresses")
return inetAddresses
} ?: let {
return Dns.SYSTEM.lookup(hostname)
}
}
}
网络请求肯定是少不了拦截器的,这里我们自己封装一个日志拦截器和请求头拦截器
package com.kt.network.net.interceptor
import android.content.Context
import com.alibaba.sdk.android.httpdns.HttpDns
import okhttp3.Interceptor
import okhttp3.Response
/**
* 拦截请求头
*/
class HTTPDNSInterceptor(private var context: Context?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originRequest = chain.request()
val httpUrl = originRequest.url()
val url = httpUrl.toString()
val host = httpUrl.host()
val service = HttpDns.getService(context)
val hostIP = service.getIpByHostAsync(host)
val builder = originRequest.newBuilder()
if (hostIP != null) {
builder.url(url.replaceFirst(url, hostIP))
builder.header("host", hostIP)
}
val newRequest = builder.build()
return chain.proceed(newRequest)
}
}
封装日志拦截器需要以下几步
首先我们需要创建一个LoggingInterceptor
package com.kt.NetworkModel.net.interceptor
import com.blankj.utilcode.util.JsonUtils
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.INFO
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* @author 浩楠
*
* @date 2023/5/12-13:35.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 创建一个LoggingInterceptor观察、修改并可能短路发出的请求和返回的相应响应。通常,拦截器会在请求或响应中添加、删除或转换报头。
*/
class LoggingInterceptor : Interceptor {
private var tag: String = "HttpLogging"
var isDebug: Boolean = false
var type = INFO
var requestTag: String = tag
var responseTag: String = tag
var level = Level.BASIC
private val headers = Headers.Builder()
var logger: Logger? = null
interface Logger {
fun log(level: Int, tag: String, msg: String)
companion object {
val DEFAULT: Logger = object : Logger {
override fun log(level: Int, tag: String, msg: String) {
Platform.get().log(level, msg, null)
}
}
}
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (getHeaders().size() > 0) {
val headers = request.headers()
val names = headers.names()
val iterator = names.iterator()
val requestBuilder = request.newBuilder()
requestBuilder.headers(getHeaders())
while (iterator.hasNext()) {
val name = iterator.next()
requestBuilder.addHeader(name, headers.get(name)!!)
}
request = requestBuilder.build()
}
if (!isDebug || level == Level.NONE) {
return chain.proceed(request)
}
val requestBody = request.body()
var rContentType: MediaType? = null
if (requestBody != null) {
rContentType = request.body()!!.contentType()
}
var rSubtype: String? = null
if (rContentType != null) {
rSubtype = rContentType.subtype()
}
if (rSubtype != null && (rSubtype.contains("json")
|| rSubtype.contains("xml")
|| rSubtype.contains("plain")
|| rSubtype.contains("html"))
) {
Printer.printJsonRequest(this, request)
} else {
Printer.printFileRequest(this, request)
}
val st = System.nanoTime()
val response = chain.proceed(request)
val segmentList = request.url().encodedPathSegments()
val chainMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - st)
val header = response.headers().toString()
val code = response.code()
val isSuccessful = response.isSuccessful
val responseBody = response.body()
val contentType = responseBody!!.contentType()
var subtype: String? = null
val body: ResponseBody
if (contentType != null) {
subtype = contentType.subtype()
}
if (subtype != null && (subtype.contains("json")
|| subtype.contains("xml")
|| subtype.contains("plain")
|| subtype.contains("html"))
) {
val bodyString = responseBody.string()
val bodyJson = JsonUtils.formatJson(bodyString)
Printer.printJsonResponse(
this,
chainMs,
isSuccessful,
code,
header,
bodyJson,
segmentList
)
body = ResponseBody.create(contentType, bodyString)
} else {
Printer.printFileResponse(this, chainMs, isSuccessful, code, header, segmentList)
return response
}
return response.newBuilder().body(body).build()
}
private fun getHeaders(): Headers = headers.build()
}
然后需要创建一个Level
package com.kt.NetworkModel.net.interceptor
/**
* @author 浩楠
*
* @date 2023/5/12-13:36.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 创建一个枚举类,用来表示几种不同形式的日志类型
*/
enum class Level {
/**
* No logs.
*/
NONE,
/**
*
* Example:
* <pre>`- URL
* - Method
* - Headers
* - Body
`</pre> *
*/
BASIC,
/**
*
* Example:
* <pre>`- URL
* - Method
* - Headers
`</pre> *
*/
HEADERS,
/**
*
* Example:
* <pre>`- URL
* - Method
* - Body
`</pre> *
*/
BODY
}
最后一步就是Printer
package com.kt.NetworkModel.net.interceptor
import com.blankj.utilcode.util.JsonUtils
import okhttp3.FormBody
import okhttp3.Request
import okio.Buffer
import java.io.IOException
/**
* @author 浩楠
*
* @date 2023/5/12-13:37.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 对日志拦截器的格式进行处理
*/
object Printer {
private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n"
private val DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR
private val OMITTED_RESPONSE = arrayOf(LINE_SEPARATOR, "Omitted response body")
private val OMITTED_REQUEST = arrayOf(LINE_SEPARATOR, "Omitted request body")
private const val N = "\n"
private const val T = "\t"
private const val REQUEST_UP_LINE =
"┌────── Request ────────────────────────────────────────────────────────────────────────"
private const val END_LINE =
"└───────────────────────────────────────────────────────────────────────────────────────"
private const val RESPONSE_UP_LINE =
"┌────── Response ───────────────────────────────────────────────────────────────────────"
private const val BODY_TAG = "Body:"
private const val URL_TAG = "URL: "
private const val METHOD_TAG = "Method: @"
private const val HEADERS_TAG = "Headers:"
private const val STATUS_CODE_TAG = "Status Code: "
private const val RECEIVED_TAG = "Received in: "
private const val CORNER_UP = "┌ "
private const val CORNER_BOTTOM = "└ "
private const val CENTER_LINE = "├ "
private const val DEFAULT_LINE = "│ "
private fun isEmpty(line: String) =
line.isEmpty() || N == line || T == line || line.trim().isEmpty()
internal fun printJsonRequest(builder: LoggingInterceptor, request: Request) {
val requestBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(request)
val tag = builder.requestTag
if (builder.logger == null)
log(builder.type, tag, REQUEST_UP_LINE)
logLines(builder.type, tag, arrayOf(URL_TAG + request.url()), builder.logger, false)
logLines(builder.type, tag, getRequest(request, builder.level), builder.logger, true)
if (request.body() is FormBody) {
val formBody = StringBuilder()
val body = request.body() as FormBody?
if (body != null && body.size() != 0) {
for (i in 0 until body.size()) {
formBody.append(body.encodedName(i) + "=" + body.encodedValue(i) + "&")
}
formBody.delete(formBody.length - 1, formBody.length)
logLines(builder.type, tag, arrayOf(formBody.toString()), builder.logger, true)
}
}
if (builder.level == Level.BASIC || builder.level == Level.BODY) {
logLines(
builder.type,
tag,
requestBody.split(LINE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray(),
builder.logger,
true
)
}
if (builder.logger == null)
log(builder.type, tag, END_LINE)
}
internal fun printJsonResponse(
builder: LoggingInterceptor, chainMs: Long, isSuccessful: Boolean,
code: Int, headers: String, bodyString: String, segments: List<String>
) {
val responseBody =
LINE_SEPARATOR!! + BODY_TAG + LINE_SEPARATOR + JsonUtils.formatJson(bodyString)
val tag = builder.responseTag
if (builder.logger == null)
log(builder.type, tag, RESPONSE_UP_LINE)
logLines(
builder.type, tag, getResponse(
headers, chainMs, code, isSuccessful,
builder.level, segments
), builder.logger, true
)
if (builder.level == Level.BASIC || builder.level == Level.BODY) {
logLines(
builder.type,
tag,
responseBody.split(LINE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray(),
builder.logger,
true
)
}
if (builder.logger == null)
log(builder.type, tag, END_LINE)
}
internal fun printFileRequest(builder: LoggingInterceptor, request: Request) {
val tag = builder.responseTag
if (builder.logger == null)
log(builder.type, tag, REQUEST_UP_LINE)
logLines(builder.type, tag, arrayOf(URL_TAG + request.url()), builder.logger, false)
logLines(builder.type, tag, getRequest(request, builder.level), builder.logger, true)
if (request.body() is FormBody) {
val formBody = StringBuilder()
val body = request.body() as FormBody?
if (body != null && body.size() != 0) {
for (i in 0 until body.size()) {
formBody.append(body.encodedName(i) + "=" + body.encodedValue(i) + "&")
}
formBody.delete(formBody.length - 1, formBody.length)
logLines(builder.type, tag, arrayOf(formBody.toString()), builder.logger, true)
}
}
if (builder.level == Level.BASIC || builder.level == Level.BODY) {
logLines(builder.type, tag, OMITTED_REQUEST, builder.logger, true)
}
if (builder.logger == null)
log(builder.type, tag, END_LINE)
}
internal fun printFileResponse(
builder: LoggingInterceptor, chainMs: Long, isSuccessful: Boolean,
code: Int, headers: String, segments: List<String>
) {
val tag = builder.responseTag
if (builder.logger == null)
log(builder.type, tag, RESPONSE_UP_LINE)
logLines(
builder.type, tag, getResponse(
headers, chainMs, code, isSuccessful,
builder.level, segments
), builder.logger, true
)
logLines(builder.type, tag, OMITTED_RESPONSE, builder.logger, true)
if (builder.logger == null)
log(builder.type, tag, END_LINE)
}
private fun getRequest(request: Request, level: Level): Array<String> {
val message: String
val header = request.headers().toString()
val loggableHeader = level == Level.HEADERS || level == Level.BASIC
message = METHOD_TAG + request.method() + DOUBLE_SEPARATOR +
when {
loggableHeader -> "${HEADERS_TAG}${LINE_SEPARATOR}${dotHeaders(header)}"
else -> ""
}
return message.split(LINE_SEPARATOR!!.toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
}
private fun getResponse(
header: String, tookMs: Long, code: Int, isSuccessful: Boolean,
level: Level, segments: List<String>
): Array<String> {
val message: String
val loggableHeader = level == Level.HEADERS || level == Level.BASIC
val segmentString = slashSegments(segments)
message =
"${if (segmentString.isNotEmpty()) "$segmentString - " else ""}is success : $isSuccessful - $RECEIVED_TAG$tookMs ms $DOUBLE_SEPARATOR $STATUS_CODE_TAG " +
"$code $DOUBLE_SEPARATOR ${if (loggableHeader) HEADERS_TAG + LINE_SEPARATOR + dotHeaders(
header
) else ""}"
return message.split(LINE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
}
private fun slashSegments(segments: List<String>): String {
val segmentString = StringBuilder()
for (segment in segments) {
segmentString.append("/").append(segment)
}
return segmentString.toString()
}
private fun dotHeaders(header: String): String {
if (isEmpty(header)) return ""
val headers =
header.split(LINE_SEPARATOR!!.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val builder = StringBuilder()
var tag = "─ "
if (headers.size > 1) {
for (i in headers.indices) {
tag = when (i) {
0 -> CORNER_UP
headers.size - 1 -> CORNER_BOTTOM
else -> CENTER_LINE
}
builder.append(tag).append(headers[i]).append("\n")
}
} else {
for (item in headers) {
builder.append(tag).append(item).append("\n")
}
}
return builder.toString()
}
private fun logLines(
type: Int,
tag: String,
lines: Array<String>,
logger: LoggingInterceptor.Logger?,
withLineSize: Boolean
) {
for (line in lines) {
val lineLength = line.length
val maxSize = if (withLineSize) 110 else lineLength
for (i in 0..lineLength / maxSize) {
val start = i * maxSize
var end = (i + 1) * maxSize
end = if (end > line.length) line.length else end
if (logger == null) {
log(type, tag, DEFAULT_LINE + line.substring(start, end))
} else {
logger.log(type, tag, line.substring(start, end))
}
}
}
}
private fun bodyToString(request: Request): String {
try {
val copy = request.newBuilder().build()
val buffer = Buffer()
if (copy.body() == null)
return ""
copy.body()!!.writeTo(buffer)
return JsonUtils.formatJson(buffer.readUtf8())
} catch (e: IOException) {
return "{\"err\": \"${e.message}\"}"
}
}
private fun log(type: Int, tag: String, msg: String) {
LoggingInterceptor.Logger.DEFAULT.log(type, tag, msg)
}
}
这样我们一个自定义的日志拦截器就制作完成了
接下来我们就该对网络的状态进行一个判断
package com.kt.network.net.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.Response
/**
* 网络状态判断
*/
class NoNetworkInterceptor(private var context: Context?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newBuilder = request.newBuilder()
if (getNetworkStatus(context) == -1) {
//无网时,只从缓存中取
newBuilder.cacheControl(CacheControl.FORCE_CACHE)
} else {
//有网时,只从服务器取
newBuilder.cacheControl(CacheControl.FORCE_NETWORK)
}
return chain.proceed(newBuilder.build())
}
companion object {
@SuppressLint("MissingPermission")
fun getNetworkStatus(context: Context?): Int {
try {
if (context == null) {
return -1
}
val conMan = context.getSystemService(
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
val info = conMan.activeNetworkInfo
if (null != info && info.isConnected) {
if (info.type == ConnectivityManager.TYPE_MOBILE) {
return when (info.subtype) {
1, 2, 4 -> // 2G网络
2
else -> // 3G及其以上网络
3
}
} else if (info.type == ConnectivityManager.TYPE_WIFI) {
// wifi网络
return 1
}
}
} catch (e: Exception) {
//报错的就当有
return 1
}
// 无网络
return -1
}
}
}
还需要做一个对服务器x509Certificates的一个判断
package com.kt.network.net
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.*
/**
* 用来检查服务器中的x509Certificates
*/
class TrustAllCerts : X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, p1: String?) {
requireNotNull(chain) { " Check Server x509Certificates is null" }
}
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
companion object {
fun createSSLSocketFactory(): SSLSocketFactory? {
var ssfFactory: SSLSocketFactory? = null
try {
val sc = SSLContext.getInstance("TLS")
sc.init(
null, arrayOf(TrustAllCerts()), SecureRandom()
)
ssfFactory = sc.socketFactory
} catch (e: Exception) {
}
return ssfFactory
}
}
class TrustAllHostnameVerifier : HostnameVerifier {
override fun verify(hostname: String, session: SSLSession): Boolean {
return true
}
}
}
截至到这里我们的网络请求成功时候的都已经封装好了,但是为什么说是成功时候呢,因为网络请求有成功,就有失败,各种各样的error,所有还得继续封装异常,因为我们的网络请求是在ViewModel中调用,所以我们的异常也是在BaseViewModel中调用的。首先创建一个ExceptionHandle
package com.kt.network.net
import android.app.Application
import android.content.Context
import android.net.ParseException
import android.widget.TextView
import com.blankj.utilcode.util.ToastUtils
import com.google.gson.JsonParseException
import com.google.gson.stream.MalformedJsonException
import com.hjq.xtoast.XToast
import com.kt.NetworkModel.App
import com.kt.ktmvvm.lib.R
import org.json.JSONException
import retrofit2.HttpException
import java.net.ConnectException
/**
* @author 浩楠
*
* @date 2023/3/10-10:56.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 根据不同的错误给出相应的提示
*/
object ExceptionHandle {
fun handleException(e: Throwable): ResponseThrowable {
var ex: ResponseThrowable
if (e is ResponseThrowable) {
ex = e
} else if (e is HttpException) {
ex = ResponseThrowable(ERROR.HTTP_ERROR, e)
when (e.code()) {
404 -> {
// ToastUtils.showShort(R.string.net_erro)
XToast<XToast<*>>(App.get()).apply {
setContentView(R.layout.layout_toast)
setDuration(3000)
findViewById<TextView>(R.id.txtToastMessage).text="网络地址错误。请稍后再试"
}.show()
ex = ResponseThrowable(ERROR.NOT_FOUND, e)
}
400 -> {
ex = ResponseThrowable(ERROR.TOKEN_EMPTY, e)
}
}
} else if (e is JsonParseException
|| e is JSONException
|| e is ParseException || e is MalformedJsonException
) {
ex = ResponseThrowable(ERROR.PARSE_ERROR, e)
} else if (e is ConnectException) {
// ToastUtils.showShort(R.string.net_erro)
XToast<XToast<*>>(App.get()).apply {
setContentView(R.layout.layout_toast)
setDuration(3000)
findViewById<TextView>(R.id.txtToastMessage).text="网络连接失败。请稍后再试"
}.show()
ex = ResponseThrowable(ERROR.NETWORD_ERROR, e)
} else if (e is javax.net.ssl.SSLException) {
ex = ResponseThrowable(ERROR.SSL_ERROR, e)
} else if (e is java.net.SocketTimeoutException) {
// ToastUtils.showShort(R.string.net_erro)
XToast<XToast<*>>(App.get()).apply {
setContentView(R.layout.layout_toast)
setDuration(3000)
findViewById<TextView>(R.id.txtToastMessage).text="网络连接失败。请稍后再试"
}.show()
ex = ResponseThrowable(ERROR.TIMEOUT_ERROR, e)
} else if (e is java.net.UnknownHostException) {
// ToastUtils.showShort(R.string.net_erro)
XToast<XToast<*>>(App.get()).apply {
setContentView(R.layout.layout_toast)
setDuration(3000)
findViewById<TextView>(R.id.txtToastMessage).text="网络连接失败。请稍后再试"
}.show()
ex = ResponseThrowable(ERROR.TIMEOUT_ERROR, e)
} else {
ex = if (!e.message.isNullOrEmpty()) ResponseThrowable(1000, e.message!!, e)
else ResponseThrowable(ERROR.UNKNOWN, e)
}
return ex
}
}
然后再创建一个错误的枚举类
package com.kt.network.net
/**
* @author 浩楠
*
* @date 2023/3/8 15:12.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 错误的枚举类
*/
enum class ERROR(private val code: Int, private val err: String) {
/**
* 未知错误
*/
UNKNOWN(1000, "未知错误"),
/**
* 解析错误
*/
PARSE_ERROR(1001, "解析错误"),
/**
* 网络错误
*/
NETWORD_ERROR(1002, "网络错误"),
/**
* 协议出错
*/
HTTP_ERROR(1003, "协议出错"),
TOKEN_EMPTY(-2, "No message available"),
/**
* 证书出错
*/
SSL_ERROR(1004, "证书出错"),
NOT_FOUND(404, "not found"),
/**
* 连接超时
*/
TIMEOUT_ERROR(1006, "连接超时");
fun getValue(): String {
return err
}
fun getKey(): Int {
return code
}
}
ExceptionHandle中会有一个XToast是报错的,先别急,需要我们再自定义一个Application
package com.kt.NetworkModel
import android.app.Application
import android.app.Person
import android.content.Context
import com.blankj.utilcode.util.ToastUtils
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import me.jessyan.autosize.AutoSize
import me.jessyan.autosize.AutoSizeConfig
import me.jessyan.autosize.unit.Subunits
/**
* @author 浩楠
*
* @date 2023/1/9 18:00.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* 描述:
*/
class App : Application() {
override fun onCreate() {
super.onCreate()
instance = this
//用于屏幕适配
/*AutoSize.initCompatMultiProcess(this);
AutoSize.checkAndInit(this);
AutoSizeConfig.getInstance().setCustomFragment(true).setExcludeFontScale(true)
.setPrivateFontScale(0.8f).setLog(false).setBaseOnWidth(true).setUseDeviceSize(true)
.getUnitsManager().setSupportDP(true).setDesignSize(2160F, 3840F).setSupportSP(true)
.setSupportSubunits(Subunits.MM);*/
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
instance = this
}
companion object {
private var instance: Application? = null
fun get(): App {
return instance as App
}
}
}
这里我们通过自定义Application后就可以是使用在ExceptionHandle中报错的XToast了
我们现在整体的异常已经封装完成了,将Exception进行封装就可以完成整体的异常的封装了
package com.kt.network.net
/**
* @author 浩楠
*
* @date 2023/3/8 15:11.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* 描述: TODO 构建一个异常的构造类
*/
class ResponseThrowable : Exception {
var code: Int
var errMsg: String
constructor(error: ERROR, e: Throwable? = null) : super(e) {
code = error.getKey()
errMsg = error.getValue()
}
constructor(code: Int, msg: String, e: Throwable? = null) : super(e) {
this.code = code
this.errMsg = msg
}
constructor(base: IBaseResponse<*>, e: Throwable? = null) : super(e) {
this.code = base.errorCode()
this.errMsg = base.errorMsg()
}
}
现在我们的整个网络请求就可以完成了。我们就可以进行调用了。
但是在调用之前我们还需要进行一个最后的封装,因为我们的网络请求后的JSON数据的格式都后端来定义的,这里我们就针对玩Android中的JSON格式来进行一个数据格式封装
首先我们需要创建一个IBaseResponse
package com.kt.network.net
/**
* @author 浩楠
*
* @date 2023/3/9 9:32.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO 定义一个JSON数据格式的接口类
*/
abstract class IBaseResponse<T> {
//后端返回的code
abstract fun errorCode():Int
//后端返回的信息
abstract fun errorMsg():String
abstract fun data():T?
//接口请求是否成功
abstract fun isSuccess():Boolean
}
然后再创建一个实体类data
package com.kt.network.bean
import com.kt.network.net.IBaseResponse
import java.util.ArrayList
/**
* @author 浩楠
*
* @date 2023/1/16 14:44.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* 描述: TODO 实体类data
*/
open class BaseResult<T> : IBaseResponse<T>() {
var success:Boolean = false
var errorCode:Int=0
var errorMsg:String =""
var data:T? = null
override fun errorCode(): Int {
return errorCode
}
override fun errorMsg(): String {
return errorMsg
}
override fun data():T? {
return data
}
override fun isSuccess(): Boolean {
return errorCode == 0
}
}
data class FontDataNew(
val curPage :Double,
val datas :ArrayList<Datas>,
val offset :Double,
val over :Boolean,
val pageCount:Double,
val size :Double,
val total :Double,
)
data class Datas(
val adminAdd :Boolean,
val apkLink :String,
val audit:Double,
val author:String,
val canEdit:Boolean,
val chapterId:Double,
val chapterName:String,
val collect :Boolean,
val courseId :Double,
val desc:String,
val descMd :String,
val envelopePic:String,
val fresh :Boolean,
val host :String,
val id :Double,
val isAdminAdd :Boolean,
val link :String,
val niceDate:String,
val niceShareDate :String,
val origin :String,
val prefix :String,
val projectLink :String,
val publishTime :Double,
val realSuperChapterId :Double,
val route :Boolean,
val selfVisible :Double,
val shareDate :Double,
val shareUser :String,
val superChapterId :Double,
val superChapterName:String,
val tags :Any,
val title :String,
val type :Double,
val userId :Double,
val visible:Double,
val zan :Double,
)
这样我们就可以把项目中的所有实体类都写到这一个data下了
接下来就是我们如何调用了
我们用的接口是这个:https://www.wanandroid.com/article/list/1/json
我们要创建一个ApiService来写我们的retrofit的网络请求的中的请求类型和请求接口,已经我们的请求参数
package com.kt.network.net
import com.kt.network.bean.BaseResult
import com.kt.network.bean.FontDataNew
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiService {
/**
* 获取导航数据
*/
@GET(ApiAddress.PROJECT)
suspend fun callback(): BaseResult<FontDataNew>
}
我们还需要创建一个ApiAddress,当然你也可以不创建,直接把ApiAddress.PROJECT改成"article/list/1/json"也是一样的
package com.kt.network.net
class ApiAddress {
companion object {
/**
* 导航
*/
const val PROJECT="article/list/1/json"
}
}
接下来就是在ViewModel中调用了
我们需要创建一个请求管理器来进行网络请求的一个管理
package com.ghn.cocknovel.net
import com.kt.NetworkModel.App
import com.kt.network.bean.BaseResult
import com.kt.network.bean.FontDataNew
import com.kt.network.net.ApiService
import com.kt.network.net.RetrofitClient
/**
* @author 浩楠
*
* @date 2023/1/9 17:08.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* 描述: TODO 请求管理器
*/
class DataService {
companion object{
/**
* 测试网络请求框架
*/
suspend fun callback(host: Int): BaseResult<FontDataNew> {
return RetrofitClient.getInstance(App.get()).getDefault(ApiService::class.java, host)
.callback()
}
}
}
然后再创建一个RecommendViewModel来进行网络请求
package com.ghn.cocknovel.viewmodel
import android.app.Application
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.example.basemodel.base.BaseViewModel
import com.ghn.cocknovel.net.DataService
import com.kt.network.bean.FontDataNew
/**
* @author 浩楠
*
* @date 2023/4/6-17:16.
*
* _ _ _ _ ____ _ _ _
* / \ _ __ __| |_ __ ___ (_) __| | / ___|| |_ _ _ __| (_) ___
* / _ \ | '_ \ / _` | '__/ _ \| |/ _` | \___ \| __| | | |/ _` | |/ _ \
* / ___ \| | | | (_| | | | (_) | | (_| | ___) | |_| |_| | (_| | | (_) |
* /_/ \_\_| |_|\__,_|_| \___/|_|\__,_| |____/ \__|\__,_|\__,_|_|\___/
* @Description: TODO
*/
open class RecommendViewModel(application: Application) : BaseViewModel(application) {
companion object {
val TAG: String? = BookStoreViewModel::class.simpleName
}
val loginStatus = MutableLiveData<String>()
open fun getwan(){
launchOnlyresult({
DataService.callback(1)
},{
loginStatus.value= it.toString()
})
}
}
最后在我们的activity或者是fragment中进行一个网络请求的调用就完成了
package com.ghn.cocknovel.ui.activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.basemodel.base.BaseActivity
import com.example.basemodel.base.BaseViewModel
import com.ghn.cocknovel.BR
import com.ghn.cocknovel.R
import com.ghn.cocknovel.databinding.ActivityHomeBinding
import com.ghn.cocknovel.viewmodel.RecommendViewModel
class HomeActivity : BaseActivity<ActivityHomeBinding, RecommendViewModel>(){
override fun initVariableId(): Int {
return BR.mode
}
override fun initContentView(savedInstanceState: Bundle?): Int {
return R.layout.activity_home
}
override fun initParam() {
viewModel?.getwan()
}
}
然后就是双向绑定
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="mode"
type="com.ghn.cocknovel.viewmodel.RecommendViewModel" />
</data>
<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"
android:fitsSystemWindows="true"
tools:context=".ui.activity.HomeActivity">
<TextView
android:id="@+id/HomeRecyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="@={mode.loginStatus}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
这样我们的整个MVVM的项目框架就搭建完成了
还是那句话
本章节到这里也就结束了,在封装的过程中,遇到问题的可以留言,我看到后会第一时间回复
如有大佬指点一二,小弟在此抱拳了