在 Android 运用开发中,网络恳求必不可少,怎样去封装才能使自己的恳求代码显得更加简练高雅,更加方便于以后的开发呢?这儿运用 Kotlin 的函数式编程和 Retrofit 来从零开端封装一个网络恳求框架,下面就一起来瞧瞧吧!
首要,引入一些必要的依赖。
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
界说拦截器
咱们能够先自界说一些拦截器,对一些公共提交的字段做封装,比方 token。在服务器注册成功或许登录成功之后获取 token,过期之后便无法正常恳求接口,所以需求在恳求接口时判别 token 是否过期,因为接口众多,不可能每个接口都进行判别,所以需求全局设置一个拦截器判别 token。
class TokenInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// 当时拦截器中收到的恳求目标
val request = chain.request()
// 履行恳求
var response = chain.proceed(request)
if (response.body == null) {
return response
}
val mediaType = response.body!!.contentType() ?: return response
val type = mediaType.toString()
if (!type.contains("application/json")) {
return response
}
val result = response.body!!.string()
var code = ""
try {
val jsonObject = JSONObject(result)
code = jsonObject.getString("code")
} catch (e: Exception) {
e.printStackTrace()
}
// 从头构建 response
response = response.newBuilder().body(result.toResponseBody(null)).code(200).build()
if (isTokenExpired(code)) {
// token 过期,需求获取新的 token
val newToken = getNewToken() ?: return response
// 从头构建新的 token 恳求
val builder = request.url.newBuilder().setEncodedQueryParameter("token", newToken)
val newRequest = request.newBuilder().method(request.method, request.body)
.url(builder.build()).build()
return chain.proceed(newRequest)
}
return response
}
// 判别 token 是否过期
private fun isTokenExpired(code: String) =
TextUtils.equals(code, "401") || TextUtils.equals(code, "402")
// 刷新 token
private fun getNewToken() = ServiceManager.instance.refreshToken()
}
这儿是 token 过期之后直接从头恳求接口获取新的 token,这需求依据详细业务需求来,有些可能是过期之后跳转到登录页面,让用户从头登录等等。
咱们还能够再界说一个拦截器,全局增加 token。
class TokenHeaderInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val headers = request.headers
var token = headers["token"]
if (TextUtils.isEmpty(token)) {
token = ServiceManager.instance.getToken()
request = request.newBuilder().addHeader("token", token).build()
}
return chain.proceed(request)
}
}
创立 retrofit
class RetrofitUtil {
companion object {
private const val TIME_OUT = 20L
private fun createRetrofit(): Retrofit {
// OkHttp 提供的一个拦截器,用于记载和检查网络恳求和响应的日志信息。
val interceptor = HttpLoggingInterceptor()
// 打印恳求和响应的所有内容,响应状况码和履行时间等等。
interceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient().newBuilder().apply {
addInterceptor(interceptor)
addInterceptor(TokenInterceptor())
addInterceptor(TokenHeaderInterceptor())
retryOnConnectionFailure(true)
connectTimeout(TIME_OUT, TimeUnit.SECONDS)
writeTimeout(TIME_OUT, TimeUnit.SECONDS)
readTimeout(TIME_OUT, TimeUnit.SECONDS)
}.build()
return Retrofit.Builder().apply {
addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())
addConverterFactory(GsonConverterFactory.create())
baseUrl(ServiceManager.instance.baseHttpUrl)
client(okHttpClient)
}.build()
}
fun <T> getAPI(clazz: Class<T>): T {
return createRetrofit().create(clazz)
}
}
}
网络恳求封装
界说通用根底恳求回来的数据结构
private const val SERVER_SUCCESS = "200"
data class BaseResp<T>(val code: String, val message: String, val data: T)
fun <T> BaseResp<T>?.isSuccess() = this?.code == SERVER_SUCCESS
恳求状况流程封装,能够依据详细业务流程完成方法。
class RequestAction<T> {
// 开端恳求
var start: (() -> Unit)? = null
private set
// 发起恳求
var request: (suspend () -> BaseResp<T>)? = null
private set
// 恳求成功
var success: ((T?) -> Unit)? = null
private set
// 恳求失利
var error: ((String) -> Unit)? = null
private set
// 恳求完毕
var finish: (() -> Unit)? = null
private set
fun request(block: suspend () -> BaseResp<T>) {
request = block
}
fun start(block: () -> Unit) {
start = block
}
fun success(block: (T?) -> Unit) {
success = block
}
fun error(block: (String) -> Unit) {
error = block
}
fun finish(block: () -> Unit) {
finish = block
}
}
因为网络恳求都是在 ViewModel 中进行的,咱们能够界说一个 ViewModel 的扩展函数,用来处理网络恳求。
fun <T> ViewModel.netRequest(block: RequestAction<T>.() -> Unit) {
val action = RequestAction<T>().apply(block)
viewModelScope.launch {
try {
action.start?.invoke()
val result = action.request?.invoke()
if (result.isSuccess()) {
action.success?.invoke(result!!.data)
} else {
action.error?.invoke(result!!.message)
}
} catch (ex: Exception) {
// 能够做一些定制化的回来错误提示
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}
}
private const val SERVER_ERROR = "HTTP 500 Internal Server Error"
private const val HTTP_ERROR_TIP = "服务器或许网络连接错误"
fun getErrorTipContent(ex: Throwable) = if (ex is ConnectException || ex is UnknownHostException
|| ex is SocketTimeoutException || SERVER_ERROR == ex.message.toString()
) HTTP_ERROR_TIP else ex.message.toString()
运用案例
界说网络恳求接口
interface HttpApi {
@GET("/exampleA/exampleP/exampleI/exampleApi/getNetData")
suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<NetDataBean>
@GET("/exampleA/exampleP/exampleI/exampleApi/getTestData")
suspend fun getTestData(
@Query("param1") param1: String,
@Query("param2") param2: String
): BaseResp<NetDataBean>
@GET("/exampleA/exampleP/exampleI/exampleApi/{id}")
fun getNetTask(
@Path("id") id: String,
@QueryMap params: HashMap<String, String>,
): Call<BaseResp<TaskBean>>
@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/confirm")
suspend fun confirm(@Field("id") id: String, @Field("token") token: String): BaseResp<String>
@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/upload")
suspend fun upload(@FieldMap params: Map<String, String>): BaseResp<String>
}
咱们能够写一个网络恳求协助类,用于恳求的创立。
class RequestHelper {
private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)
companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}
suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)
suspend fun getTestData(branchCode: String, token: String) =
httpApi.getTestData(branchCode, token)
suspend fun getNetTask(id: String, params: HashMap<String, String>) =
httpApi.getNetTask(id, params)
suspend fun confirm(id: String, token: String) = httpApi.confirm(id, token)
suspend fun upload(params: HashMap<String, String>) = httpApi.upload(params)
}
界说用户的目的和 UI 状况
// 界说用户目的
sealed class MainIntent {
object FetchData : MainIntent()
}
// 界说 UI 状况
sealed class MainUIState {
object Loading : MainUIState()
data class NetData(val data: NetDataBean?) : MainUIState()
data class Error(val error: String?) : MainUIState()
}
ViewModel 中做目的的处理和 UI 状况的变更,依据网络恳求结果传递不同的状况,运用界说的扩展方法去履行网络恳求,封装往后的网络恳求就很简练方便了,下面演示下详细运用。
class MainViewModel : ViewModel() {
val mainIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _mainUIState = MutableStateFlow<MainUIState>(MainUIState.Loading)
val mainUIState: StateFlow<MainUIState>
get() = _mainUIState
init {
viewModelScope.launch {
mainIntent.consumeAsFlow().collect {
if (it is MainIntent.FetchData) {
getNetDataResult()
}
}
}
}
// 运用
private fun getNetDataResult() = netRequest {
start { _mainUIState.value = MainUIState.Loading }
request {
val paramMap = hashMapOf<String, String>()
paramMap["param1"] = "param1"
paramMap["param2"] = "param2"
RequestHelper.instance.getNetData(paramMap)
}
success { _mainUIState.value = MainUIState.NetData(it) }
error { _mainUIState.value = MainUIState.Error(it) }
}
}
这样是不是看起来很简练呢?接下来,Activity 负责发送目的和接收 UI 状况进行相关的处理就行啦!
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainViewModel>()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
initData()
observeViewModel()
}
private fun initData() {
lifecycleScope.launch {
// 发送目的
viewModel.mainIntent.send(MainIntent.FetchData)
}
}
private fun observeViewModel() {
lifecycleScope.launch {
viewModel.mainUIState.collect {
when (it) {
is MainUIState.Loading -> showLoading()
// 这儿拿到网络恳求回来的数据,依据业务自行操作,这儿只做简略的显现。
is MainUIState.NetData -> showText(it.data.toString())
is MainUIState.Error -> showText(it.error)
}
}
}
}
private fun showLoading() {
binding.progressBar.visibility = View.VISIBLE
binding.netText.visibility = View.GONE
}
private fun showText(result: String?) {
binding.progressBar.visibility = View.GONE
binding.netText.visibility = View.VISIBLE
binding.netText.text = result
}
}
文件的上传与下载
如果是文件的上传和下载呢?其实文件还不太一样,这涉及到上传进展,文件的处理等方面,所以,为了方便开发运用,咱们能够针对文件独自再做一下封装。
界说文件上传目标
data class UpLoadFileBean(val file: File, val fileKey: String)
自界说 RequestBody,从中获取上传进展。
class ProgressRequestBody(
private var requestBody: RequestBody,
var onProgress: ((Int) -> Unit)?,
) : RequestBody() {
private var bufferedSink: BufferedSink? = null
override fun contentType(): MediaType? = requestBody.contentType()
override fun contentLength(): Long {
return requestBody.contentLength()
}
override fun writeTo(sink: BufferedSink) {
if (bufferedSink == null) bufferedSink = createSink(sink).buffer()
bufferedSink?.let {
requestBody.writeTo(it)
it.flush()
}
}
private fun createSink(sink: Sink): Sink = object : ForwardingSink(sink) {
// 当时写入字节数
var bytesWritten = 0L
// 总字节长度
var contentLength = 0L
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
if (contentLength == 0L) {
contentLength = contentLength()
}
// 增加当时写入的字节数
bytesWritten += byteCount
CoroutineScope(Dispatchers.Main).launch {
// 进展回调
onProgress?.invoke((bytesWritten * 100 / contentLength).toInt())
}
}
}
}
创立 MultipartBody.Part
fun <T> createPartList(action: UpLoadFileAction<T>): List<MultipartBody.Part> =
MultipartBody.Builder().apply {
// 公共参数 token
addFormDataPart("token", ServiceManager.instance.getToken())
// 其他基本参数
action.params?.forEach {
if (it.key.isNotBlank() && it.value.isNotBlank()) {
addFormDataPart(it.key, it.value)
}
}
// 文件校验
action.fileData?.let {
addFormDataPart(
it.fileKey, it.file.name, ProgressRequestBody(
requestBody = it.file
.asRequestBody("application/octet-stream".toMediaTypeOrNull()),
onProgress = action.progress
)
)
}
}.build().parts
界说文件上传行为
class UpLoadFileAction<T> {
// 恳求体
lateinit var request: (suspend () -> BaseResp<T>)
private set
lateinit var parts: List<MultipartBody.Part>
// 其他一般参数
var params: HashMap<String, String>? = null
private set
// 文件参数
var fileData: UpLoadFileBean? = null
private set
// 初始化参数
fun init(params: HashMap<String, String>?, fileData: UpLoadFileBean?) {
this.params = params
this.fileData = fileData
parts = createPartList(this)
}
var start: (() -> Unit)? = null
private set
var success: (() -> Unit)? = null
private set
var error: ((String) -> Unit)? = null
private set
var progress: ((Int) -> Unit)? = null
private set
var finish: (() -> Unit)? = null
private set
fun start(block: () -> Unit) {
start = block
}
fun success(block: () -> Unit) {
success = block
}
fun error(block: (String) -> Unit) {
error = block
}
fun progress(block: (Int) -> Unit) {
progress = block
}
fun finish(block: () -> Unit) {
finish = block
}
fun request(block: suspend () -> BaseResp<T>) {
request = block
}
}
同样,界说 ViewModel 的扩展函数,用来履行文件上传。
fun <T> ViewModel.upLoadFile(
block: UpLoadFileAction<T>.() -> Unit,
params: HashMap<String, String>?,
fileData: UpLoadFileBean?,
) = viewModelScope.launch {
val action = UpLoadFileAction<T>().apply(block)
try {
action.init(params, fileData)
action.start?.invoke()
val result = action.request.invoke()
if (result.isSuccess()) {
action.success?.invoke()
} else {
action.error?.invoke(result.message)
}
} catch (ex: Exception) {
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}
界说文件上传接口
interface HttpApi {
//...
@Multipart
@POST("/exampleA/exampleP/exampleI/exampleApi/uploadFile")
suspend fun uploadFile(@Part partLis: List<MultipartBody.Part>): BaseResp<String>
}
在 RequestHelper 中界说上传文件方法
class RequestHelper {
private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)
companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}
//...
suspend fun uploadFile(partList: List<MultipartBody.Part>) = httpApi.uploadFile(partList)
}
封装往后的文件上传就很简练方便了,下面演示下详细运用。
private fun uploadMyFile() = upLoadFile(
params = hashMapOf("param1" to "param1", "param2" to "param2"),
fileData = UpLoadFileBean(File(absoluteFilePath), "file"),
) {
start {
// TODO: 开端上传,此处能够显现加载动画
}
request { RequestHelper.instance.uploadFile(parts) }
success {
// TODO: 上传成功
}
error {
// TODO: 上传失利
}
finish {
// TODO: 上传完毕,此处能够关闭加载动画
}
}
已然上传文件都有了,那怎样少得了下载呢?其实,下载比上传更简略,下面就来写一下,同样运用了 kotlin 的函数式编程,咱们增加 ViewModel 的扩展函数,需求注意的是,因为这边是直接运用 OkHttp 的同步恳求,所以把这部分代码放在了 IO 线程中。
fun ViewModel.downLoadFile(
downLoadUrl: String,
dirPath: String,
fileName: String,
progress: ((Int) -> Unit)?,
success: (File) -> Unit,
failed: (String) -> Unit,
) = viewModelScope.launch(Dispatchers.IO) {
try {
val fileDir = File(dirPath)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val downLoadFile = File(fileDir, fileName)
val request = Request.Builder().url(downLoadUrl).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {
response.body?.let {
val totalLength = it.contentLength().toDouble()
val stream = it.byteStream()
stream.copyTo(downLoadFile.outputStream()) { currentLength ->
// 当时下载进展
val process = currentLength / totalLength * 100
progress?.invoke(process.toInt())
}
success.invoke(downLoadFile)
} ?: failed.invoke("response body is null")
} else failed.invoke("download failed:$response")
} catch (ex: Exception) {
failed.invoke("download failed:${getErrorTipContent(ex)}")
}
}
// InputStream 增加扩展函数,完成字节复制。
private fun InputStream.copyTo(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
progress: (Long) -> Unit,
): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
progress(bytesCopied)
}
return bytesCopied
}
然后,运用就会变得很简练了,如下所示:
fun downloadMyFile(downLoadUrl: String, dirPath: String, fileName: String) =
downLoadFile(
downLoadUrl = downLoadUrl,
dirPath = dirPath,
fileName = fileName,
progress = {
// TODO: 这儿能够拿到进展
},
success = {
// TODO: 下载成功,拿到下载的文件目标 File
},
failed = {
// TODO: 下载失利,回来原因
}
)