献给android原生应用层开发初学者技术架构选型和整合的方案思路(六)

续前篇《献给android原生应用层开发初学者技术架构选型和整合的方案思路(五)》,本篇着重于 ViewModel 与网络访问 http Restful API对象的封装等。

  1. 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)

     

  2. 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)"
        }
    )

     

  3. 一些别的辅助工具类,在 others 的 package 下面。如 Kotlin 的扩展ExtensionHelper,常量集合ConstantsCollection,ARouter 的路由表RouterPath等等。可按需自己修改。
  4. 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()
        }
    }

     

  5. 有关 ORM 的封装,因为在移动 app 中大多数方案都是针对底层的 sqlite 单机文件数据库的上层 ORM 封装,产品很多,如 greenDAO,Realm 等等。此处采用的谷歌自家的 Room 库,请看相关教程,本例中也完整实现了 Room 库的使用,也可进一步采用 room-rxjava 的 adapter 库封装成 RxJava 风格的 API。在业务 ViewModel 中一般如下调用:
    val ormService = TokenInfoOrmService(OrmDatabase.getInstance(viewModelContext.activity).getTokenInfoDao())

     

  6. 有关如何封装成 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解决方案等等。请慢慢爬坑。

  7. 有关数据对象 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){
     
    }

     

  8. 如果你的网络请求要通过 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的注意事项,沉浸式状态栏失效的解决方案等等。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值