好的设计是成功的一半,好的设计思想为后面扩展带来极大的方便
一、前言
网络请求在开发中是必不可少的一个功能,如何设计一套好的网络请求框架,可以为后面扩展及改版带来极大的方便,特别是一些长期维护的项目。作为一个深耕Android开发十几载的大龄码农,深深的体会到。
网络框架的发展:
1. 从最早的HttpClient
到 HttpURLConnection
,那时候需要自己用线程池封装异步,Handler切换到UI线程,要想从网络层就返回接收实体对象,也需要自己去实现封装
2. 后来,谷歌的 Volley
, 三方的 Afinal
再到 XUtils
都是基于上面1中的网络层再次封装实现
3. 再到后来,OkHttp
问世,Retrofit
空降,从那以后基本上网络请求应用层框架就是 OkHttp
和 Retrofit
两套组合拳,基本打遍天下无敌手,最多的变化也就是在这两套组合拳里面秀出各种变化,但是思想实质上还是这两招。
我们试想:从当初的大概2010年,2011年,2012年开始,就启动一个App项目,就网络这一层的封装而言,随着时代的潮流,技术的演进,我们势必会经历上面三个阶段,这一层的封装就得重构三次。
现在是2024年,往后面发展,随着http3.0的逐渐成熟,一定会出现更好的网络请求框架
我们怎么封装一套更容易扩展的框架,而不必每次重构这一层时,改动得那么困难。
本文下面就示例这一思路如何封装,涉及到的知识,jetpack
中的手术刀: Hilt
成员来帮助我们实现。
二 、示例项目
-
上图截图圈出的就是本文重点介绍的内容:
怎么快速封装一套可以切换网络框架的项目
及相关Jetpack中的 Hilt
用法 -
其他的1,2,3,4是之前我写的:花式封装:Kotlin+协程+Flow+Retrofit+OkHttp +Repository,倾囊相授,彻底减少模版代码进阶之路,大家可以参考,也可以在它的基础上,再结合本文再次封装,可以作为
花式玩法五
三、网络层代码设计
1. 设计请求接口,包含请求地址 Url
,请求头,请求参数,返回解析成的对象Class
:
interface INetApi {
/**
* Get请求
* @param url:请求地址
* @param clazzR:返回对象类型
* @param header:请求头
* @param map:请求参数
*/
suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>? = null, map: MutableMap<String, Any>? = null): R
/**
* Get请求
* @param url:请求地址
* @param clazzR:返回对象类型
* @param header:请求头
* @param map:请求参数
* @param body:请求body
*/
suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>? = null, body: String? = null): R
}
2. 先用早期 HttpURLConnection
对网络请求进行实现:
class HttpUrlConnectionImpl constructor() : INetApi {
private val gson by lazy { Gson() }
override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
//这里HttpUrlConnectionRequest内部是HttpURLConnection的Get请求真正的实现
val json = HttpUrlConnectionRequest.getResult(BuildParamUtils.buildParamUrl(url, map), header)
android.util.Log.e("OkhttpImpl", "HttpUrlConnection 请求:${json}")
return gson.fromJson<R>(json, clazzR)
}
override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
////这里HttpUrlConnectionRequest内部是HttpURLConnection的Post请求真正的实现
val json = HttpUrlConnectionRequest.postData(url, header, body)
return gson.fromJson<R>(json, clazzR)
}
}
3. 整个项目 build.gradle
下配置 Hilt插件
buildscript {
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
}
}
4. 工程app的 build.gradle
下引入:
先配置:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'dagger.hilt.android.plugin'//Hilt使用
id 'kotlin-kapt'//
}
里面的 android
下面添加:
kapt {
generateStubs = true
}
在 dependencies
里面引入 Hilt
使用
//hilt
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-android-compiler:2.42"
kapt 'androidx.hilt:hilt-compiler:1.0.0'
5. 使用 Hilt
5.1 在Application上添加注解 @HiltAndroidApp
:
@HiltAndroidApp
class MyApp : Application() {
}
5.2 在使用的Activity上面添加注解 @AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : BaseViewModelActivity<MainViewModel>(R.layout.activity_main), View.OnClickListener {
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn1 -> {
viewModel.getHomeList()
}
else -> {}
}
}
}
5.3 在使用的ViewModel上面添加注解 @HiltViewModel
和 @Inject
:
@HiltViewModel
class MainViewModel @Inject constructor(private val repository: NetRepository) : BaseViewModel() {
fun getHomeList() {
flowAsyncWorkOnViewModelScopeLaunch {
repository.getHomeList().onEach {
val title = it.datas!![0].title
android.util.Log.e("MainViewModel", "one 111 ${title}")
errorMsgLiveData.postValue(title)
}
}
}
}
5.4 在 HttpUrlConnectionImpl
构造方法上添加注解 @Inject
如下:
class HttpUrlConnectionImpl @Inject constructor() : INetApi {
private val gson by lazy { Gson() }
override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
val json = HttpUrlConnectionRequest.getResult(BuildParamUtils.buildParamUrl(url, map), header)
android.util.Log.e("OkhttpImpl", "HttpUrlConnection 请求:${json}")
return gson.fromJson<R>(json, clazzR)
}
override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
val json = HttpUrlConnectionRequest.postData(url, header, body)
return gson.fromJson<R>(json, clazzR)
}
}
5.5 新建一个 annotation
: BindHttpUrlConnection
如下:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
annotation class BindHttpUrlConnection()
5.6 再建一个绑定网络请求的 abstract
修饰的类 AbstractHttp
如下:让 @BindHttpUrlConnection
和 HttpUrlConnectionImpl
在如下方法中通过注解绑定
@InstallIn(SingletonComponent::class)
@Module
abstract class AbstractHttp {
@BindHttpUrlConnection
@Singleton
@Binds
abstract fun bindHttpUrlConnection(h: HttpUrlConnectionImpl): INetApi
}
5.7 在viewModel持有的仓库类 NetRepository
的构造方法中添加 注解 @Inject
,并且申明 INetApi
,并且绑定注解 @BindHttpUrlConnection
如下: 然后即就可以开始调用 INetApi
的方法
class NetRepository @Inject constructor(@BindHttpUrlConnection val netHttp: INetApi) {
suspend fun getHomeList(): Flow<WanAndroidHome> {
return flow {
netHttp.getApi("https://www.wanandroid.com/article/list/0/json", HomeData::class.java).data?.let { emit(it) }
}
}
}
到此:Hilt使用就配置完成了,那边调用 网络请求就直接执行到 网络实现 类 HttpUrlConnectionImpl
里面去了。
运行结果看到代码执行打印:
5.8 我们现在切换到 Okhttp
来实现网络请求:
新建 OkhttpImpl
实现 INetApi
并在其构造方法上添加 @Inject
如下:
class OkhttpImpl @Inject constructor() : INetApi {
private val okHttpClient by lazy { OkHttpClient() }
private val gson by lazy { Gson() }
override suspend fun <R> getApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, map: MutableMap<String, Any>?): R {
try {
val request = Request.Builder().url(buildParamUrl(url, map))
header?.forEach {
request.addHeader(it.key, it.value)
}
val response = okHttpClient.newCall(request.build()).execute()
if (response.isSuccessful) {
val json = response.body?.string()
android.util.Log.e("OkhttpImpl","okhttp 请求:${json}")
return gson.fromJson<R>(json, clazzR)
} else {
throw RuntimeException("response fail")
}
} catch (e: Exception) {
throw e
}
}
override suspend fun <R> postApi(url: String, clazzR: Class<R>, header: MutableMap<String, String>?, body: String?): R {
try {
val request = Request.Builder().url(url)
header?.forEach {
request.addHeader(it.key, it.value)
}
body?.let {
request.post(RequestBodyCreate.toBody(it))
}
val response = okHttpClient.newCall(request.build()).execute()
if (response.isSuccessful) {
return gson.fromJson<R>(response.body.toString(), clazzR)
} else {
throw RuntimeException("response fail")
}
} catch (e: Exception) {
throw e
}
}
}
5.9 再建一个注解 annotation
类型的 BindOkhttp
如下:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
annotation class BindOkhttp()
5.10 在 AbstractHttp
类中添加 @BindOkhttp
绑定到 OkhttpImpl
,如下:
@InstallIn(SingletonComponent::class)
@Module
abstract class AbstractHttp {
@BindOkhttp
@Singleton
@Binds
abstract fun bindOkhttp(h: OkhttpImpl): INetApi
@BindHttpUrlConnection
@Singleton
@Binds
abstract fun bindHttpUrlConnection(h: HttpUrlConnectionImpl): INetApi
}
5.11 现在只需要在 NetRepository
中持有的 INetApi
修改其绑定的 注解 @BindHttpUrlConnection
改成 @BindOkhttp
便可以将项目网络请求全部改成由 Okhttp
来实现了,如下:
//class NetRepository @Inject constructor(@BindHttpUrlConnection val netHttp: INetApi) {
class NetRepository @Inject constructor(@BindOkhttp val netHttp: INetApi) {
suspend fun getHomeList(): Flow<WanAndroidHome> {
return flow {
netHttp.getApi("https://www.wanandroid.com/article/list/0/json", HomeData::class.java).data?.let { emit(it) }
}
}
}
运行执行结果截图可见:
到此:网络框架切换就这样简单的完成了。
四、总结
- 本文重点介绍了,怎么对网络框架扩展型封装:即怎么可以封装成快速从一套网络请求框架,切换到另一套网络请求上去
- 借助于
Jetpack中成员 Hilt
对其整个持有链路进行切割,简单切换绑定网络实现框架1,框架2,框架xxx等。