续前篇《献给android原生应用层开发初学者技术架构选型和整合的方案思路(五)》,本篇着重于 ViewModel 与网络访问 http Restful API对象的封装等。
- http网络访问库 Retrofit2/OKHttp3的使用及与Rajva2/RxAndroid2的集成封装成,同时集成一些公用的访问拦截器。
在 general 包下面新建名为 network 的 package,主要用来放置全局网络访问对象代码和一些公用的 data class 包装类。
创建 ApiManager对象并通过 private constructort和 companion object伴生对象生成一个单例对象 instance。在 init初始代码块中初始化 okhttpClientBuilder 并且注册若干个网络拦截器链,其中CommonParamsInterceptor用来给 http 增加公用的请求参数。请求拦截观察器OkHttpProfilerInterceptor仅用在DEBUG 为 true 的时候加载,用来在 android studio 中查看请求和响应信息,以及根据json 生成 data class对象等等。最后实例化 retrofit对象并暴露出createProxyService方法给业务代码调用(一般在ViewModel 中封装成网络请求对象 API)。细节上面启用了 GSON 作为序列/反序列化工具,启用了 RxJava2的 Adapter 适配器以处理生成 RxJava 可处理的接口对象。有关代码大致如下:
class ApiManager private constructor() { private var retrofit: Retrofit /** * 创建 API 访问服务代理实例 */ fun <T> createProxyService(service: Class<T>): T { return this.retrofit.create(service) } companion object { val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ApiManager() } } init { val commonParamsInterceptor = CommonParamsInterceptor.Builder() .addHeaderParam(ConstantsCollection.clientTypeKey, ConstantsCollection.clientTypeValue) .addHeaderParam(ConstantsCollection.clientAppName, AppUtils.getAppName()) .addHeaderParam(ConstantsCollection.clientAppVersionName, AppUtils.getAppVersionName()) .addHeaderParam(ConstantsCollection.deviceSystemVersion, DeviceUtils.getSDKVersionName()) .addHeaderParam(ConstantsCollection.deviceSystemLanguage, SystemHelper.getSystemLanguage()) .addHeaderParam(ConstantsCollection.deviceBrand, SystemHelper.getDeviceBrand()) .addHeaderParam(ConstantsCollection.deviceModel, DeviceUtils.getModel()) .build() val okHttpClientBuilder = OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) //发起请求前检查网络连接可用性 okHttpClientBuilder.addInterceptor { if (!NetworkUtils.isConnected()) { throw Exception("网络未连接,请打开并连接可用网络") } val request = it.request() it.proceed(request) } okHttpClientBuilder.addInterceptor(commonParamsInterceptor) if (BuildConfig.DEBUG) { val okHttpProfilerInterceptor = OkHttpProfilerInterceptor() okHttpClientBuilder.addInterceptor(okHttpProfilerInterceptor) } val initRetrofit = Retrofit.Builder() .baseUrl(ConstantsCollection.baseUrl) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(okHttpClientBuilder.build()).build() retrofit = initRetrofit } }
调用createProxyService方法以在运行时创建封装给业务的retrofit2 http API接口代理实例对象。假设我们封装一个登录接口,调用示例代码如下,另外在后续的讲解中会再次提及如何使用:
interface AccountService { /** * @param accountUser AccountUser类的实例转 map * @see AccountUser */ @FormUrlEncoded @POST("api/accountUser/login.json") @JvmSuppressWildcards fun login(@FieldMap accountUser: Map<String, Any>): Observable<ResponseData<String>> } val service = ApiManager.instance.createProxyService(AccountService::class.java)
- network包中其他的类,比如静态类 object NetworkScheduler的 compose生成一个在新子线程中订阅处理请求,在 android 主线程中消费的ObservableTransformer,这个方法在异步请求方法中非常实用,在本github 的 demo 项目中你会看见此方法被高频率调用。NetWorkRequestException用来包装处理各种异常对象返回具体的 message。WsResultData文件中是 http server后端项目统一数据结构json对应类(适配 Gson自动处理),wsError 放后端反馈的错误对象,WsResult是后端数据结果,当后端反馈异常时 wsError 不为 null,否则就是当 wsError != null 时表示业务正常,但是设计上面 httpStatus 总是为 code 200(因为在 rxjava 的doOnError处理上面无法在 message 中得到后端反馈的错误信息)。ResponseDataHandleMap类用来自动处理errorMsg/errorCode与业务正常时的流程情况。ServiceException是一个异常包装类。PaginationResult是一个适配后端分页结果的类(有关后端服务器 http json/xml api的数据结构,各企业各不一样,本文只以此数据结构讲解)。大致数据结构如下:
data class WsError( var errCode: String? = null, var errMsg: String? = null ) data class WsResult<T>( var result: T? = null, var error: WsError? = null ) data class ResponseData<T>( @SerializedName("WSResult") var wsResult: WsResult<T>? = null ) data class PaginationResult<T>( var count: Long = -1, var data: T? = null ) class ServiceException(errorCode: String?, errorMsg: String?) : Exception( if (errorCode == null) (errorMsg ?: "") else (errorMsg ?: "").run { "$this($errorCode)" } )
- 一些别的辅助工具类,在 others 的 package 下面。如 Kotlin 的扩展ExtensionHelper,常量集合ConstantsCollection,ARouter 的路由表RouterPath等等。可按需自己修改。
- BaseViewModel 的封装。抽象类,比较简洁,取名CoreViewModel,用到泛型,所有的数据状态均继承自MvRxState。debugMode = BuildConfig.DEBUG表明在调试模式下才开启给调试附加的功能,另外也封装了一个executeActions传入函数闭包执行,注意调用了disposeOnClear()方法以解决内存泄露问题。 研究源码发现,CoreViewModel 继承自BaseMvRxViewModel,此类中利用 Kotlin extension功能给RxJava2的Observable扩展了一个模板方法execute,而此方法内部调用了disposeOnClear()统一管理解除订阅以消除内存泄露的问题。所以我们在 ViewModel 的业务 API 方法封装中要么需调用 execute,要么调用disposeOnClear(),以后的代码中你会发现,大量调用了 execute 这个模板方法。示例代码如下:
abstract class CoreViewModel<S : MvRxState>(initialState: S) : BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG) { /** * 在新线程上执行操作 */ protected fun executeActions(action: () -> Unit): Disposable = Completable .fromAction(action) .subscribeOn(Schedulers.io()) .subscribe().disposeOnClear() }
在具体的业务 ViewModel 文件中,我们需要定义一个 State 类用来承载数据状态供 Epoxy 的 controller 来根据 data state 驱动UI的展示,也要在 ViewModel把一些封装好的业务 service 作为它的成员变量,通常在他的 Factory 方法中实例化业务 service 并暴露为成员,采用 Kotlin 的语法让代码进一步简洁紧凑:
data class AccountListState( val request: Async<ResponseData<List<Account>>> = Uninitialized, val accountList: List<Account> = emptyList() ) : MvRxState class AccountListViewModel( initialState: AccountListState, private val accountService: AccountService, private val tokenOrmService: TokenInfoOrmService ) : CoreViewModel<AccountListState>(initialState) { companion object : MvRxViewModelFactory<AccountListViewModel, AccountListState> { @JvmStatic override fun create( viewModelContext: ViewModelContext, state: AccountListState ): AccountListViewModel { val service = ApiManager.instance.createProxyService(AccountService::class.java) val ormService = TokenInfoOrmService(OrmDatabase.getInstance(viewModelContext.activity).getTokenInfoDao()) return AccountListViewModel(state, service, ormService) } } init { logStateChanges() //初始化时自动加载最初的数据,一般建议在 viewModel 的调用者里调用此方法, // 当然得看生命周期,viewModel 的生命周期在 onCreate 创建在 onDestroy 才结束,不会在activity 别的 lifeCycle回调方法中多次触发 fetchAccountList() } }
- 有关 ORM 的封装,因为在移动 app 中大多数方案都是针对底层的 sqlite 单机文件数据库的上层 ORM 封装,产品很多,如 greenDAO,Realm 等等。此处采用的谷歌自家的 Room 库,请看相关教程,本例中也完整实现了 Room 库的使用,也可进一步采用 room-rxjava 的 adapter 库封装成 RxJava 风格的 API。在业务 ViewModel 中一般如下调用:
val ormService = TokenInfoOrmService(OrmDatabase.getInstance(viewModelContext.activity).getTokenInfoDao())
- 有关如何封装成 RxJava 风格的异步 Retrofit2网络请求 API,在模块 build.gradle 中采用了"com.squareup.retrofit2:adapter-rxjava2"实现,相关接口代码示例如下:
interface AccountService { /** * @param accountUser AccountUser类的实例转 map * @see AccountUser */ @FormUrlEncoded @POST("api/accountUser/login.json") @JvmSuppressWildcards fun login(@FieldMap accountUser: Map<String, Any>): Observable<ResponseData<String>> @GET("api/accountUser/accounts.json") fun fetchAccountList(@Header(ConstantsCollection.jwtToken) token: String?): Observable<ResponseData<AccountList>> }
通过诸如Observable<ResponseData<AccountList>>返回值类型实现适配。您会遇到几个典型的问题,如《关于Kotlin中使用Retrofit的坑——Any变?通配符的问题》,使用@JvmSuppressWildcards解决方案等等。请慢慢爬坑。
-
有关数据对象 data class 的编程约定。建立一个 package 叫 models,用来存放data entity class,这个对应于 Java server 中的实体类的定义。继承Parcelable使用android 高效的序列化/反序列化方案,给 constructor 使用注解@JvmOverloads,并加上@SuppressLint("ParcelCreator")@Parcelize很是方便(参见文章《Android Parcelable的介绍与使用》)。注解@JvmOverloads的知识文章参见《Kotlin学习笔记——注解》。示例代码如下:
@SuppressLint("ParcelCreator") @Parcelize data class AccountUser @JvmOverloads constructor( var id: String? = null, var userName: String? = null, var userPwd: String? = null ) : Parcelable typealias AccountUserList = List<AccountUser> fun AccountUserList.findById(id: String?) = firstOrNull { it.id == id }
您可以利用IDE 的OkHttpProfiler插件右击返回的 json 导出 java或者 kotlin data class,进一步解放您的劳动力。
上面代码还利用 Kotlin 的扩展功能加了findById 方法,请自行脑补 Kotlin 的编程知识。与此同时,继承Parcelable也是为了和 CoreFragment或者 CoreActivity 中封装的传递参数的方法呼应,举例代码如下:
protected fun popToNextFragment(targetRouter: String, params: Parcelable? = null){ }
-
如果你的网络请求要通过 http 明文访问,请在<application>标签下增加android:usesCleartextTraffic="true",否则为了安全性,请求默认只能走https即 SSL加密,代码如下:
<application android:name=".general.core.RootApplication" 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:usesCleartextTraffic="true"> </application>
续篇《献给android原生应用层开发初学者技术架构选型和整合的方案思路(七)》,终结篇,关注 UI的集成问题,如 QMUI 的启用和 swipeback的注意事项,沉浸式状态栏失效的解决方案等等。