APP架构的一些思考

作者: 折剑游侠 | 来源:发表于2021-11-05 15:45 被阅读0次

    先上代码MVVM

    aar/source

    一般来说组件化项目中都会做aar和源码切换,开发同学正在进行的业务module需依赖源码,其它的不相干的模块依赖远程aar。大概会先定义一个全局变量做aar/source切换的开关,然后在app中进行依赖。
    module.gradle

    ext {
        // source/aar
        isBusinessDev = false
        biz = [
                business: "com.xxx.xxx:xxx:1.0.0",
        ]
    }
    

    app下build.gradle

    dependencies {
        ...
        if (rootProject.ext.isBusinessDev) {
            implementation project(path: ':business')
        } else {
            implementation rootProject.ext.biz.business
        }
    }
    

    没啥毛病,问题是随着业务迭代module逐渐变多,需要不停的往app中添加这样if else的依赖控制代码,倒也不是if else不好,很多的if else就有点难受受了。思考一下项目中第三方依赖是怎么偷懒的。
    config.gradle中定义好依赖库版本号、依赖路径

    versions = [
                kotlin              : '1.5.20',
                coroutine           : '1.5.2',
                androidx_core       : '1.3.2',
                appcompat           : '1.2.0',
                lifecycle           : '2.3.1',
                work_manager        : '2.5.0',
                room                : '2.2.5',
                constraintlayout    : '2.0.4',
                recyclerview        : '1.1.0',
                material            : '1.3.0',
    ]
    
    common = [
                kotlin               : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin",
                coroutine            : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutine",
                androidx_core        : "androidx.core:core-ktx:$versions.androidx_core",
                appcompat            : "androidx.appcompat:appcompat:$versions.appcompat",
                viewmodel            : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle",
                livedata             : "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle",
                lifecycle            : "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle",
                constraintlayout     : "androidx.constraintlayout:constraintlayout:$versions.constraintlayout",
                recyclerview         : "androidx.recyclerview:recyclerview:$versions.recyclerview",
                material             : "com.google.android.material:material:$versions.material",
    ]
    

    然后在管理依赖的module中一个循环搞定

    dependencies {
        rootProject.ext.common.each { k, v -> api v }
    }
    

    arr/source切换也搞个循环好了

    ext {
        // source/aar
        biz = [
                business: [false, "com.xxx.xxx:xxx:1.0.0"],
        ]
    
        // module
        modules = [
                business: project(':business'),
        ]
    }
    

    app中修改依赖方式

    dependencies {
        ...
        biz.each { entry ->
            if (entry.value[0]) {
                implementation entry.value[1]
            } else {
                implementation modules.(entry.key)
            }
        }
    }
    

    application/library

    为了方便调试,很多时候我们希望业务module能单独run起来,让业务module独立运行。可以给业务module加一个开关,然后让业务module用这个开关控制application/library的切换。当然此种情况下,我们希望app也能独立运行,如此一来,业务module作为application独立运行时,app需剔除该业务module的依赖。

    修改module.gradle

    ext {
        // source/aar
        biz = [
                business: [false, "com.xxx.xxx:xxx:1.0.0"],
        ]
    
        // library/application
        isBusinessModule = true
    
        // module
        modules = [
                business: [isBusinessModule, project(':business')],
        ]
    }
    

    app下build.gradle再加个判断,module作为applicaiton时不进行依赖。

    dependencies {
        biz.each { entry ->
            if (entry.value[0]) {
                implementation entry.value[1]
            } else {
                if (modules.(entry.key)[0]) {
                    implementation modules.(entry.key)[1]
                }
            }
        }
    }
    

    业务moduel下的build.gradle用module.gradle中定义好的变量isBusinessModule控制application/library切换。

    def isModule = rootProject.ext.isBusinessModule
    if (isModule) {
        apply plugin: 'com.android.library'
    } else {
        apply plugin: 'com.android.application'
    }
    

    别忘了application需配置applicationId,manifest需指定启动项和Application。

    android {
        resourcePrefix "business_"
    
        defaultConfig {
            ...
            if (!isModule) {
                applicationId "com.xxx.xxx.xxx"
            }
        }
    
        sourceSets {
            main {
                if (isModule) {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
                }
            }
        }
    }
    

    反射初始化子类对象

    之前看有的同学搞了些骚操作,在基类初始化ViewBinding。想来也是在父类中拿到泛型类型,然后反射初始化。ViewBinding生成的类格式是固定的,直接匹配类名,反射实例化然后调用ViewBinding.inflate()方法返回ViewBinding实例。

    abstract class BaseSimpleActivity<VB : ViewBinding> : BaseActivity() {
        protected val binding by lazy {
            createViewBinding()
        }
    
        open fun createViewBinding() = reflectViewBinding() as VB
    
        @Suppress("UNCHECKED_CAST")
        private fun reflectViewBinding(): VB? {
            val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
            types.forEach {
                if (it.toString().endsWith("Binding") && it is Class<*>) {
                    val method = it.getDeclaredMethod("inflate", LayoutInflater::class.java)
                    return method.invoke(it, layoutInflater) as VB
                }
            }
            return null
        }
    }
    

    当然为了防止意外状况或者是性能问题,createViewBinding()方法默认实现为反射,加个open修饰让子类可以重写自己提供ViewBinding对象。

    嗯,这样很香啊~子类拿着binding直接用就好了。等等ViewModel是不是也可以这么搞呢,当然可以,搞一搞。

    abstract class BaseVMActivity<VM : BaseViewModel<R>, R : BaseRepository, VB : ViewBinding> :
        BaseSimpleActivity<VB>() {
        protected val viewModel: VM by lazy {
            createViewModel()
        }
    
        open fun createViewModel() = reflectViewModel()
    
        @Suppress("UNCHECKED_CAST")
        private fun reflectViewModel(): VM {
            val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
            return ViewModelProvider(this)[types[0] as Class<ViewModel>] as VM
        }
    }
    

    ViewModel的实现类命名可能并非以ViewModel结尾,这里直接取第一个泛型类型types[0]。同样的createViewModel()默认实现为反射,加个open让子类可以重写。

    网络请求绑定进度对话框

    在网络请求开始的时候弹一个菊花圈,结束/失败的时候关闭。因为用到协程viewModelScope,就把launch方法又封装了一下。

        internal typealias NetLaunch<T> = suspend CoroutineScope.() -> BaseResponse<T>
    
        val statusLiveData: MutableLiveData<CoroutineState> by lazy {
            MutableLiveData<CoroutineState>()
        }
    
        fun <T> start(
            refresh: Boolean = true,
            block: NetLaunch<T>,
        ): LaunchHandler<T> {
            val launchHandler = LaunchHandler<T>()
            viewModelScope.launch {
                try {
                    if (refresh) {
                        statusLiveData.value = CoroutineState.REFRESH
                    } else {
                        statusLiveData.value = CoroutineState.START
                    }
                    val result = block()
                    statusLiveData.value = CoroutineState.FINISH
                    launchHandler.successListener?.invoke(
                        LaunchResult.Success(result)
                    )
                } catch (e: Exception) {
                    statusLiveData.value = CoroutineState.ERROR
                    if (launchHandler.errorListener == null) {
                        errorLiveData.value = e
                    } else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
                }
            }
            return launchHandler
        }
    

    先忽略其它代码,主要看statusLiveData,将协程状态发送到Activity基类BaseVMActivity,在基类中进行处理。

    private fun initViewModelActions() {
            viewModel.statusLiveData.observe(this, { status ->
                status?.run {
                    when (this) {
                        CoroutineState.START -> {
                            //START
                        }
                        CoroutineState.REFRESH -> {
                            //REFRESH
                            ProgressDialog.showProgress(this@BaseVMActivity)
                        }
                        CoroutineState.FINISH -> {
                            //FINISH
                            ProgressDialog.dismissProgress()
                        }
                        CoroutineState.ERROR -> {
                            //ERROR
                            ProgressDialog.dismissProgress()
                        }
                    }
                }
            })
            //默认错误处理
            viewModel.errorLiveData.observe(this, {
                ToastUtils.showShort(it.message)
            })
        }
    

    不管用没用到协程,思路是一致的。网络请求入口包裹一层,将状态发送到页面基类,在基类统一处理。

    网络请求API设计

    现在都是MVVM了,那就在BaseViewModel里面统一。写kotlin还是要有kotlin的风格,搞一些lambda。
    BaseViewModel

        val statusLiveData: MutableLiveData<CoroutineState> by lazy {
            MutableLiveData<CoroutineState>()
        }
    
        val errorLiveData: MutableLiveData<Exception> by lazy {
            MutableLiveData<Exception>()
        }
    
        fun <T> start(
            refresh: Boolean = true,
            block: NetLaunch<T>,
        ): LaunchHandler<T> {
            val launchHandler = LaunchHandler<T>()
            viewModelScope.launch {
                try {
                    if (refresh) {
                        statusLiveData.value = CoroutineState.REFRESH
                    } else {
                        statusLiveData.value = CoroutineState.START
                    }
                    val result = block()
                    statusLiveData.value = CoroutineState.FINISH
                    launchHandler.successListener?.invoke(
                        LaunchResult.Success(result)
                    )
                } catch (e: Exception) {
                    statusLiveData.value = CoroutineState.ERROR
                    if (launchHandler.errorListener == null) {
                        errorLiveData.value = e
                    } else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
                }
            }
            return launchHandler
        }
    

    LaunchHandler

    class LaunchHandler<T> {
        var successListener: HandlerSuccess<T>? = null
        var errorListener: HandlerError? = null
    }
    
    infix fun <T> LaunchHandler<T>.resumeWithSuccess(handler: HandlerSuccess<T>) = this.apply {
        successListener = handler
    }
    
    infix fun <T> LaunchHandler<T>.resumeWithError(handler: HandlerError) = this.apply {
        errorListener = handler
    }
    

    不传错误回调函数的情况下,将错误状态errorLiveData发送给Activity基类Toast处理,当然这里也可以按照状态码做对应的Toast,最终调用处就很舒服了:MainViewModel

        val contentLiveData by lazy {
            MutableLiveData<String>()
        }
    
        fun getChapters() {
            start {
                repository.getChapters()
            } resumeWithSuccess {
                contentLiveData.value = GsonUtils.instance.toJson(it.response.data)
            } resumeWithError {
                ToastUtils.showShort(it.error.message)
            }
        }
    

    resumeWithSuccess、resumeWithError方法是中缀函数,调用时可以省略一个点。

    repository.getChapters()

    class MainRepository : BaseRepository() {
        private val service = ApiServiceUtil.getApiService<MainApiService>()
    
        suspend fun getChapters(): BaseResponse<List<Chapters>> {
            return withContext(Dispatchers.IO) {
                service.getChapters()
            }
        }
    }
    
    interface MainApiService : BaseService {
        @GET("/wxarticle/chapters/json")
        suspend fun getChapters(): BaseResponse<List<Chapters>>
    }
    

    没啥好说的,retrofit接口定义suspend方法,repository中withContext(Dispatchers.IO)切换到IO线程。

    相关文章

      网友评论

        本文标题:APP架构的一些思考

        本文链接:https://www.haomeiwen.com/subject/jvoialtx.html