2024年Android开发架构推荐,防御性编程让同事再也看不懂我的代码 😏

Android项目开发模板开源与相关介绍

前言

这么惊世骇俗的标题,这是标题党吗?是,也不是,毕竟拆分结构优化代码的事怎么能算防御性编程呢?

当然如果项目拆分的过于细致,层级太多导致同事都看不懂代码了😏 这... 怪我咯 尝试阅读此文试试。

其实我们优化项目架构的真实目的是为了细致化的逻辑分层,还需要顾及到多个员工协作的开发效率,还要兼顾应用产品的多变性,不是炫技,不是为了分层而分层,最终目的还是单一职责,高内聚低耦合的思想。

本 Demo 基于 gradle 8.0+ 实现,compileSdk 为 34,targetSdk 为 33 ,使用 gradle.kts 做配置,用 Kotlin 封装,使用较为流行的组件化与路由方案,配合 Hilt 的依赖注入解耦各组件的依赖注入,页面基于 MVI + UserCase 的思路开发,UI 还是基于 XML 的布局,使用 ViewBinding 配合 MVI 做出布局响应。

Demo 的各种依赖可以说是相对较新的,如果你恰好是海外版应用开发者,那么是比较契合。当然国内的开发者也能用,不过貌似国内的应用开发版本都不会这么高。至于其他的小功能模块,例如 Log 框架,Json解析框架,图片加载框架,很多人的使用习惯不同,对于这些三方小插件我不做介绍,你可以自行替换你需要的对应框架即可。

其实通过上述介绍也可以看出本 Demo 其实都是一些流行和成熟的方案,只是做了一些整合与封装,如果对应的功能或逻辑你不是很了解,其实通过搜索引擎我相信你都能找到对应的资料。本文旨在对项目做简单的介绍,并没有深入某一块深入讲解,我默认你已经会了这块知识点,如果没有你可以参照对应的知识点在搜索引擎上搜索。当然文章末尾我会给出源码供大家参考。

话不多说,直接开始,Let's go

一、gradle.kts 管理依赖并封装常用依赖

关于项目的版本管理我两年前就出过相关文章,【Android开发依赖版本管理的几种方式】,在两年后的2024年再看来是有点落伍了。

为什么不继续用了呢?因为还是不够方便,不能点击查看,虽然可以仿继承实现,但是封装的细度不够,难免也会有一处改动多处修改的问题。而通过 buildSrc + gradle.kts 的方式会更加的方便。

通过 buildSrc 来统一管理依赖版本,这是之前就很流行的方案了,如何创建如何使用?如果你不了解我想你可能需要搜索引擎一下,我没必要复制粘贴不然篇幅太长了。

但是通过 Kotlin 的方式搭配 gradle.kts 的方案,通过扩展方法的使用、继承的使用可以更加便捷的封装 gradle 版本与依赖版本,可以更方便的管理依赖版本。通过使用函数式定义,可以快速的点击跳转到指定的依赖或依赖组。

gradle.kts 是什么?怎么用?这...

这不是本文的重点啊,我默认当做你已经会了,如果实在不了解可以先搜索引擎了解下,也不是什么高深的知识点。

接下来继续,在本 Demo 的 buildSrc 中有代码如下:

我们现在 buildSrc 中使用 Kotlin 类定义一些版本,其次我们定义一些扩展函数,再定义一些依赖组的快捷入口,然后定义了默认的 build.gradle 的基类,方便 gradle.kts 去依赖。

例如我们可以在 Kotlin 中定义项目的配置和签名文件等配置:

ini 复制代码
/**
 * @author Newki
 *
 * 项目编译配置与AppId配置
 */
object ProjectConfig {
    const val minSdk = 21
    const val compileSdk = 34
    const val targetSdk = 33

    const val versionCode = 100
    const val versionName = "1.0.0"

    const val applicationId = "com.newki.template"
    const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

}

//签名文件信息配置
object SigningConfigs {
    //密钥文件路径
    const val store_file = "key/newki.jks"

    //密钥密码
    const val store_password = "123456"

    //密钥别名
    const val key_alias = "newki"

    //别名密码
    const val key_password = "123456"
}

这样就可以在 build.gradle.kts 中直接引用,可以直接跳转到指定链接,是比较方便的,在这里修改这些配置是无需重新 Sync Project 的。

再例如我们可以在 Kotlin 的 单例类中定义一些依赖与版本:

kotlin 复制代码
object VersionAndroidX {

    //appcompat中默认引入了很多库,比如activity库、fragment库、core库、annotation库、drawerLayout库、appcompat-resources等
    const val appcompat = "androidx.appcompat:appcompat:1.6.1"

    //support兼容库
    const val supportV4 = "androidx.legacy:legacy-support-v4:1.0.0"

    //core包+ktx扩展函数
    const val coreKtx = "androidx.core:core-ktx:1.9.0"

    //activity+ktx扩展函数
    const val activityKtx = "androidx.activity:activity-ktx:1.8.0"

    //fragment+ktx扩展函数
    const val fragmentKtx = "androidx.fragment:fragment-ktx:1.5.1"

    //约束布局
    const val constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"

    //卡片控件
    const val cardView = "androidx.cardview:cardview:1.0.0"

    //recyclerView
    const val recyclerView = "androidx.recyclerview:recyclerview:1.2.1"

    //材料设计
    const val material = "com.google.android.material:material:1.11.0"

    //分包
    const val multidex = "androidx.multidex:multidex:2.0.1"

    ... 等
}

我们就可以把依赖按组分类,进行依赖组的管理,Dependencies.kt:

scss 复制代码
import org.gradle.api.artifacts.dsl.DependencyHandler

/**
 *  @author Newki
 *
 * 通过扩展函数的方式导入功能模块的全部依赖
 * 可以自行随意添加或更改
 */
fun DependencyHandler.appcompat() {
    api(VersionAndroidX.appcompat)
    api(VersionAndroidX.supportV4)
    api(VersionAndroidX.coreKtx)
    api(VersionAndroidX.activityKtx)
    api(VersionAndroidX.fragmentKtx)
    api(VersionAndroidX.multidex)
    api(VersionAndroidX.documentFile)
}

//生命周期监听
fun DependencyHandler.lifecycle() {
    api(VersionAndroidX.Lifecycle.livedata)
    api(VersionAndroidX.Lifecycle.liveDataKtx)
    api(VersionAndroidX.Lifecycle.runtime)
    api(VersionAndroidX.Lifecycle.runtimeKtx)

    api(VersionAndroidX.Lifecycle.viewModel)
    api(VersionAndroidX.Lifecycle.viewModelKtx)
    api(VersionAndroidX.Lifecycle.viewModelSavedState)

    kapt(VersionAndroidX.Lifecycle.compiler)
}

//Kotlin与协程
fun DependencyHandler.kotlin() {
    api(VersionKotlin.stdlib)
    api(VersionKotlin.reflect)
    api(VersionKotlin.stdlibJdk7)
    api(VersionKotlin.stdlibJdk8)

    api(VersionKotlin.Coroutines.android)
    api(VersionKotlin.Coroutines.core)
}

//依赖注入
fun DependencyHandler.hilt() {
    implementation(VersionAndroidX.Hilt.hiltAndroid)
    implementation(VersionAndroidX.Hilt.javapoet)
    implementation(VersionAndroidX.Hilt.javawriter)
    kapt(VersionAndroidX.Hilt.hiltCompiler)
}

//测试Test依赖
fun DependencyHandler.test() {
    testImplementation(VersionTesting.junit)
    androidTestImplementation(VersionTesting.androidJunit)
    androidTestImplementation(VersionTesting.espresso)
}

//常用的布局控件
fun DependencyHandler.widgetLayout() {
    api(VersionAndroidX.constraintlayout)
    api(VersionAndroidX.cardView)
    api(VersionAndroidX.recyclerView)
    api(VersionThirdPart.baseRecycleViewHelper)
    api(VersionAndroidX.material)
    api(VersionAndroidX.ViewPager.viewpager)
    api(VersionAndroidX.ViewPager.viewpager2)
}

//路由
fun DependencyHandler.router() {
    implementation(VersionThirdPart.ARouter.core)
    kapt(VersionThirdPart.ARouter.compiler)
}

//Work任务
fun DependencyHandler.work() {
    api(VersionAndroidX.Work.runtime)
    api(VersionAndroidX.Work.runtime_ktx)
}

//KV存储
fun DependencyHandler.dataStore() {
    implementation(VersionAndroidX.DataStore.preferences)
    implementation(VersionAndroidX.DataStore.core)
}

//网络请求
fun DependencyHandler.retrofit() {
    api(VersionThirdPart.Retrofit.core)
    implementation(VersionThirdPart.Retrofit.convertGson)
    api(VersionThirdPart.Retrofit.gson)
    api(VersionThirdPart.gsonFactory)
}

//图片加载
fun DependencyHandler.glide() {
    implementation(VersionThirdPart.Glide.core)
    implementation(VersionThirdPart.Glide.annotation)
    implementation(VersionThirdPart.Glide.integration)
    kapt(VersionThirdPart.Glide.compiler)
}

//多媒体相机相册
fun DependencyHandler.imageSelector() {
    implementation(VersionThirdPart.ImageSelector.core)
    implementation(VersionThirdPart.ImageSelector.compress)
    implementation(VersionThirdPart.ImageSelector.ucrop)
}

//弹窗
fun DependencyHandler.xpopup() {
    implementation(VersionThirdPart.XPopup.core)
    implementation(VersionThirdPart.XPopup.picker)
    implementation(VersionThirdPart.XPopup.easyAdapter)
}

//下拉刷新
fun DependencyHandler.refresh() {
    api(VersionThirdPart.SmartRefresh.core)
    api(VersionThirdPart.SmartRefresh.classicsHeader)
}


//fun DependencyHandler.compose() {
//    implementation(VersionAndroidX.Compose.composeUi)
//    implementation(VersionAndroidX.Compose.composeMaterial)
//    implementation(VersionAndroidX.Compose.composeRuntime)
//    implementation(VersionAndroidX.Compose.composeUiTooling)
//    implementation(VersionAndroidX.Compose.composeUiGraphics)
//    implementation(VersionAndroidX.Compose.composeUiToolingPreview)
//}

可以看到我们定义了很多依赖组,相对来说直接用依赖组会比较方便,统一管理之后如果有变动只需要改动依赖组中的依赖或版本即可。

当然关于 Log 框架,Json解析框架,图片加载框架,多媒体框架,权限框架,和一些弹窗吐司轮播等框架你需要按照你自己的使用习惯来。

那么我们如何使用这些依赖组呢?直接在 build.gradle.kts 中使用即可:

scss 复制代码
plugins {
    id("com.android.application")
}

android {
    //需要定义 namespace 和 applicationId 的信息
    namespace = "com.newki.template"
    defaultConfig {
        applicationId = ProjectConfig.applicationId
    }
}

dependencies {

    hilt()  //就可以依赖整个 Hilt 大礼包

}

我们的项目肯定是以组件化的方案开发的,那么每一个组件都需要写这些重复的配置吗?这岂不是很麻烦,万一有一些改动岂不是每一个组件都需要改动,太麻烦了,我能不能封装起来使用?

当然可以,本身 build.gradle.kts 就支持一些 Kotlin 的语法,我们直接把 Plugin 类作为基类去继承它,实现一些默认的配置不就行了吗?

比如每一个组件都需要的一些 compileSdk ,compileOptions,kotlinOptions,buildFeatures,dependencies 等信息都是一些固定的,我们就可以通过 Kotlin 的类来直接定义,然后在 build.gradle.kts 中直接依赖这个自定义的 Plugin 即可。

例如 DefaultGradlePlugin:

kotlin 复制代码
/**
 * @author Newki
 *
 * 默认的配置实现,支持 library 和 application 级别,根据子组件的类型自动判断
 */
open class DefaultGradlePlugin : Plugin<Project> {

    override fun apply(project: Project) {
        setProjectConfig(project)
        setConfigurations(project)
    }

    //项目配置
    private fun setProjectConfig(project: Project) {
        val isApplicationModule = project.plugins.hasPlugin("com.android.application")

        if (isApplicationModule) {
            // 处理 com.android.application 模块逻辑
            println("===> Handle Project Config by [com.android.application] Logic")
            setProjectConfigByApplication(project)
        } else {
            // 处理 com.android.library 模块逻辑
            println("===> Handle Project Config by [com.android.library] Logic")
            setProjectConfigByLibrary(project)
        }
    }

    private fun setConfigurations(project: Project) {
        //配置ARouter的Kapt配置
        project.configureKapt()
    }

    //设置 library 的相关配置
    private fun setProjectConfigByLibrary(project: Project) {
        //添加插件
        project.apply {
            plugin("kotlin-android")
            plugin("kotlin-kapt")
            plugin("org.jetbrains.kotlin.android")
            plugin("dagger.hilt.android.plugin")
        }

        project.library().apply {

            compileSdk = ProjectConfig.compileSdk

            defaultConfig {
                minSdk = ProjectConfig.minSdk
                testInstrumentationRunner = ProjectConfig.testInstrumentationRunner
                vectorDrawables {
                    useSupportLibrary = true
                }
                ndk {
                    //常用构建目标 'x86_64','armeabi-v7a','arm64-v8a'
                    abiFilters.addAll(arrayListOf("armeabi-v7a", "arm64-v8a"))
                }
                multiDexEnabled = true
            }

            compileOptions {
                sourceCompatibility = JavaVersion.VERSION_17
                targetCompatibility = JavaVersion.VERSION_17
            }

            kotlinOptions {
                jvmTarget = "17"
            }

            buildFeatures {
                buildConfig = true
                viewBinding = true
            }

            packaging {
                resources {
                    excludes += "/META-INF/{AL2.0,LGPL2.1}"
                }
            }

        }

        //默认 library 的依赖
        project.dependencies {
            hilt()
            router()
            test()
            appcompat()
            lifecycle()
            kotlin()
            widgetLayout()

            if (isLibraryNeedService()) {
                //依赖 Service 服务
                implementation(project(":cs-service"))
            }
        }

    }

    //设置 application 的相关配置
    private fun setProjectConfigByApplication(project: Project) {
        //添加插件
        project.apply {
            plugin("kotlin-android")
            plugin("kotlin-kapt")
            plugin("org.jetbrains.kotlin.android")
            plugin("dagger.hilt.android.plugin")
            plugin("com.alibaba.arouter")
        }

        project.application().apply {
            compileSdk = ProjectConfig.compileSdk

            defaultConfig {
                minSdk = ProjectConfig.minSdk
                targetSdk = ProjectConfig.targetSdk
                versionCode = ProjectConfig.versionCode
                versionName = ProjectConfig.versionName
                testInstrumentationRunner = ProjectConfig.testInstrumentationRunner
                vectorDrawables {
                    useSupportLibrary = true
                }
                ndk {
                    //常用构建目标 'x86_64','armeabi-v7a','arm64-v8a'
                    abiFilters.addAll(arrayListOf("armeabi-v7a", "arm64-v8a"))
                }
                multiDexEnabled = true
            }

            compileOptions {
                sourceCompatibility = JavaVersion.VERSION_17
                targetCompatibility = JavaVersion.VERSION_17
            }

            // 设置 Kotlin JVM 目标版本
            kotlinOptions {
                jvmTarget = "17"
            }

            buildFeatures {
                buildConfig = true
                viewBinding = true
            }

            packaging {
                resources {
                    excludes += "/META-INF/{AL2.0,LGPL2.1}"
                }
            }

            signingConfigs {
                create("release") {
                    keyAlias = SigningConfigs.key_alias
                    keyPassword = SigningConfigs.key_password
                    storeFile = project.rootDir.resolve(SigningConfigs.store_file)
                    storePassword = SigningConfigs.store_password
                    enableV1Signing = true
                    enableV2Signing = true
                    enableV3Signing = true
                    enableV4Signing = true
                }
            }

            buildTypes {
                release {
                    isDebuggable = false    //是否可调试
                    isMinifyEnabled = true  //是否启用混淆
                    isShrinkResources = true   //是否移除无用的resource文件
                    isJniDebuggable = false // 是否打开jniDebuggable开关

                    proguardFiles(
                        getDefaultProguardFile("proguard-android-optimize.txt"),
                        "proguard-rules.pro"
                    )
                    signingConfig = signingConfigs.findByName("release")
                }
                debug {
                    isDebuggable = true
                    isMinifyEnabled = false
                    isShrinkResources = false
                    isJniDebuggable = true
                }
            }

        }

        //默认 application 的依赖
        project.dependencies {
            hilt()
            router()
            test()
            appcompat()
            lifecycle()
            kotlin()
            widgetLayout()

            //依赖 Service 服务
            implementation(project(":cs-service"))
        }

    }

    //根据组件模块的类型给出不同的对象去配置
    private fun Project.library(): LibraryExtension {
        return extensions.getByType(LibraryExtension::class.java)
    }

    private fun Project.application(): BaseAppModuleExtension {
        return extensions.getByType(BaseAppModuleExtension::class.java)
    }

    // Application 级别 - 扩展函数来设置 KotlinOptions
    private fun BaseAppModuleExtension.kotlinOptions(action: KotlinJvmOptions.() -> Unit) {
        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure(
            "kotlinOptions",
            action
        )
    }

    // Library 级别 - 扩展函数来设置 KotlinOptions
    private fun LibraryExtension.kotlinOptions(action: KotlinJvmOptions.() -> Unit) {
        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure(
            "kotlinOptions",
            action
        )
    }

    //配置 Project 的 kapt
    private fun Project.configureKapt() {
        this.extensions.findByType(KaptExtension::class.java)?.apply {
            arguments {
                arg("AROUTER_MODULE_NAME", name)
            }
        }
    }

    //Library模块是否需要依赖底层 Service 服务,一般子 Module 模块或者 Module-api 模块会依赖到
    protected open fun isLibraryNeedService(): Boolean = false

}

需要注意的是 library 和 application 两种类型的配置依赖是不同的,其中 library 又分为普通 library 和 组件 library 其中又有一些依赖上的小差异,我们需要分别对两种类型做基本的配置。

那么我们在项目的 app 模块下的 build.gradle.kts 只需要这样就可以了:

scss 复制代码
plugins {
    id("com.android.application")
}

// 使用自定义插件
apply<DefaultGradlePlugin>()

android {
    //application 模块需要明确 namespace 和 applicationId 的信息
    namespace = "com.newki.template"
    defaultConfig {
        applicationId = ProjectConfig.applicationId
    }

    //如果要配置 JPush、GooglePlay等配置,直接接下去写即可
}

dependencies {

    //依赖子组件
    implementation(project(":cpt-auth"))
    implementation(project(":cpt-profile"))
}

比如在子组件 cpt-auth 中的 build.gradle.kts 中就只需要这样即可:

arduino 复制代码
plugins {
    id("com.android.library")
}

// 使用自定义插件
apply<ModuleGradlePlugin>()

android {
    namespace = "com.newki.auth"
}

这样library 和 application 模块就都能使用一套配置,封装起来再使用是不是很方便呢?如果需要修改只需要修改一处基类即可,如果该 library 有特殊的地方需要重写的地方也可以在对应的 build.gradle.kts 重写配置。

二、组件化与路由与独立运行配置

组件与路由是密不可分的整体,有组件必有路由,这里的组件化与组件化拆分也是基于路由来实现的。

2.1 组件拆分

组件化大家不是都会吗?我前两年也出过类似的文章【Android组件化,这可能是最完美的形态吧】

之前一直是按照这个思路开发的,但是随着项目的演变,组件越来越多,由于没有拆分组件,导致很多的重复数据仓库和冗余的公共服务模块,导致我们的开发者苦不堪言难以维护,所以在新的架构中我们一定要注意组件的拆分。

如何划分组件?一张图秒懂:

说了这么多,为什么要把一个组件拆分为主组件与Api组件?

主要是为了逻辑分离,路由分离,其他组件可能用到此组件的地方都在Api中定义,常见如数据仓库,接口,自定义对象等。

为什么会有重复数据仓库和冗余的公共服务模块呢?

例如上图中的 Auth 组件,它需要在用户登录完成之后,调用到 Profile 组件的用户详情接口,然后告诉 App 组件登录成功,那么此时我应该怎么写?

把 Profile 组件中的用户详情数据仓库复制一份? 如果每一个组件都这么搞,那么组件化就无意义,一旦要修改还得每一个组件都检查去修改,那么组件化的意义何在?起到了反作用。

告诉 App 组件登录成功,写入缓存,App 模块是我的上级,我如何能操作我的上级组件?大家常用的做法就是逻辑下沉,放入到公共的 Service 组件中去,这是一个办法,但是不够优雅,一旦有问题就下沉导致逻辑划分不清晰,公共模块臃肿,一旦产品逻辑变动会有大量冗余资源和代码。

怎么解决这些问题呢?就是上面说到的拆分组件,把一个组件分为主组件与Api组件,Auth 组件就只需要依赖对于的 Api 组件即可通过路由操作了。

auth - build.gradle.kts:

scss 复制代码
plugins {
    id("com.android.library")
}

// 使用自定义插件
apply<ModuleGradlePlugin>()

android {
    namespace = "com.newki.auth"
}

dependencies {
    //依赖到对应组件的Api模块
    implementation(project(":cpt-auth-api"))

    implementation(project(":cpt-profile-api"))
    implementation(project(":app-api"))
}

使用:

scss 复制代码
        mBinding.btnLogin.click {
            AuthServiceProvider.authService?.doUserLogin()
        }

        mBinding.btnGotoProfile.click {
            ARouter.getInstance().build(ARouterPath.PATH_PAGE_PROFILE).navigation()
        }

        mBinding.btnVersion.click {
            val version = AppServiceProvider.appService?.getAppVersion()
            toast("version:${version.toString()}")
        }

        mBinding.btnProfile.click {

            lifecycleScope.launch {
                showStateLoading()
                val start = System.currentTimeMillis()
                MyLogUtils.d("协程开始执行")

                val userProfile = withContext(Dispatchers.Default) {
                    ProfileServiceProvider.profileService?.fetchUserProfile()
                }
                val timeStamp = System.currentTimeMillis() - start
                showStateSuccess()

                toast("协程执行完毕,耗时:$timeStamp  UserProfile:${userProfile.toString()}")

            }

        }

通过路由就能完全解耦组件逻辑与资源了。

2.2 路由实现

可以看到我用的是 ARouter 这个路由来实现的页面跳转,服务实现。

ARouter 已经被大家玩透了,我就不献丑了,如何在项目中使用?来一点示例:

App模块定义路由:

kotlin 复制代码
interface IAppService : IProvider {
    fun getPushTokenId(): String
    fun getAppVersion(): AndroidVersion
}

App 组件定义Entiry:

kotlin 复制代码
data class AndroidVersion(val code: String, val url: String)

App 组件实现路由:

kotlin 复制代码
@Route(path = ARouterPath.PATH_SERVICE_APP, name = "App模块路由服务")
class AppComponentServiceImpl : IAppService {
    override fun getPushTokenId(): String {
        return "12345678ab"
    }
    override fun getAppVersion(): AndroidVersion {
        return AndroidVersion(code = "1.0.0", url = "http://www.baidu.com")
    }
    override fun init(context: Context?) {

    }
}

Profile 组件定义的接口:

kotlin 复制代码
interface IProfileService : IProvider {

    suspend fun fetchUserProfile(): UserProfile
}

Profile 组件定义的Entiry:

kotlin 复制代码
data class UserProfile(val userId: String, val userName: String, val gender: Int)

Profile 组件实现的路由:

kotlin 复制代码
@Route(path = ARouterPath.PATH_SERVICE_PROFILE, name = "Profile模块路由服务")
class ProfileServiceImpl : IProfileService {

    override suspend fun fetchUserProfile(): UserProfile {

        delay(2000)

        return UserProfile("12", "Newki", 1)
    }

    override fun init(context: Context?) {

    }
}

在 Auth 模块中的使用:

AuthLoginActivity 可以使用 App 模块和 Profile 模块的逻辑调用。

kotlin 复制代码
@Route(path = ARouterPath.PATH_PAGE_AUTH_LOGIN)
class AuthLoginActivity : BaseVMActivity<LoginViewModel>() {
    companion object {
        fun startInstance() {
            commContext().gotoActivity<AuthLoginActivity>()
        }
    }

    override fun getLayoutIdRes(): Int = R.layout.activity_auth_login

    override fun startObserve() {

    }

    override fun init(savedInstanceState: Bundle?) {

        findViewById<Button>(R.id.btn_login).click {
            AuthServiceProvider.authService?.doUserLogin()
        }

        findViewById<Button>(R.id.btn_goto_profile).click {
            ARouter.getInstance().build(ARouterPath.PATH_PAGE_PROFILE).navigation()
        }

        findViewById<Button>(R.id.btn_version).click {
            val version = AppServiceProvider.appService?.getAppVersion()
            toast("version:${version.toString()}")
        }

        findViewById<Button>(R.id.btn_profile).click {

            lifecycleScope.launch {
                showStateLoading()
                val start = System.currentTimeMillis()
                MyLogUtils.d("协程开始执行")

                val userProfile = withContext(Dispatchers.Default) {
                    ProfileServiceProvider.profileService?.fetchUserProfile()
                }
                val timeStamp = System.currentTimeMillis() - start
                showStateSuccess()

                toast("协程执行完毕,耗时:$timeStamp  UserProfile:${userProfile.toString()}")

            }

        }

    }


}

效果图:

2.3 组件独立运行

怎样让组件能单独运行与调试?你当然可以在 build.gradle.kts 中搞一个配置去切换,是否需要独立运行。

比如 Auth 组件:

scss 复制代码
plugins {
    //id("com.android.library")  
    id("com.android.application")
}

// 使用自定义插件
apply<ModuleGradlePlugin>()

android {
    namespace = "com.newki.auth"
}

dependencies {
    //依赖到对应组件的Api模块
    implementation(project(":cpt-auth-api"))
    implementation(project(":cpt-profile-api"))
    implementation(project(":app-api"))
}

你把 library 替换到 application 你甚至都不要改动其他配置,因为 ModuleGradlePlugin 我们的自定义插件中已经做了 library 与 application 的兼容处理。

但是我还是喜欢另一种方案,直接定义独立运行模块,开发过程中运行 runalone 模块去开发调试,整体测试的时候才打包 app 壳整体项目。

如图:

这种方案的话项目会多一些文件,但是最终打包不会影响最终应用的大小,用于调试组件模块比较方便。

效果图:

由于我的 Auth 独立运行模块只有 Auth 和 Profile 模块,可以看到在 Auth 页面中调用 App 模块的路由会无效。

三、ViewBinding 与 Hilt 的示例

在我们的页面中,我们通过泛型传递 ViewBinding 和 ViewModel 的对象,ViewModel 我们又是通过 Hilt 依赖注入的,这里就拿出来一起说说。

3.1 ViewBinding 的使用与封装

相对于 DataBinding 来说,ViewBinding 的使用很简单,不了解其中差异的可以看我之前的文章 【findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?】

首先我们需要配置中开启ViewBinding

ini 复制代码
    buildFeatures {
        viewBinding = true
    }

封装:

kotlin 复制代码
abstract class BaseVDBActivity<VM : ViewModel,VB : ViewBinding>(
   private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
 
    //由于传入了参数,可以直接构建ViewModel
    protected val mViewModel: VM by lazy {
        ViewModelProvider(viewModelStore, defaultViewModelProviderFactory).get(vmClass)
    }
 
    //如果使用DataBinding,自己再赋值
}

使用:

kotlin 复制代码
class MainActivity : BaseVDBActivity<ActivityMainBinding, MainViewModel>(
    ActivityMainBinding::inflate,
    MainViewModel::class.java
) {
    //就可以直接使用ViewBinding与ViewModel 
    fun test() {
        mBinding.iconIv.visibility = View.VISIBLE
        mViewModel.data1.observe(this) {
        }
    }
}

大家一般都是这么使用,每次都要传递一个构造,相对麻烦,我这里用反射的创建方式通过泛型直接创建:

简单的Activity基类:

kotlin 复制代码
/**
 * 最底层的Activity,给其他Activity继承,一般不直接用这个
 */
abstract class AbsActivity : AppCompatActivity(), ConnectivityReceiver.ConnectivityReceiverListener {

    /**
     * 获取Context对象
     */
    protected lateinit var mActivity: Activity
    protected lateinit var mContext: Context

    abstract fun setContentView()

    abstract fun initViewModel()

    abstract fun init(savedInstanceState: Bundle?)

    /**
     * 从intent中解析数据,具体子类来实现
     */
    protected open fun getDataFromIntent(intent: Intent) {}

     ...
}

带ViewModel的基类:

kotlin 复制代码
abstract class BaseVMActivity<VM : BaseViewModel> : AbsActivity() {

    protected lateinit var mViewModel: VM

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        startObserve()
    }

    //使用这个方法简化ViewModel的获取
    protected inline fun <reified VM : BaseViewModel> getViewModel(): VM {
        val viewModel: VM by viewModels()
        return viewModel
    }

    //反射自动获取ViewModel实例
    protected open fun createViewModel(): VM {
        return ViewModelProvider(this).get(getVMCls(this))
    }

    override fun initViewModel() {
        mViewModel = createViewModel()
        //观察网络数据状态
        mViewModel.getActionLiveData().observe(this, stateObserver)
    }

    override fun setContentView() {
        setContentView(getLayoutIdRes())
    }

    abstract fun getLayoutIdRes(): Int
    abstract fun startObserve()

    override fun onNetworkConnectionChanged(isConnected: Boolean, networkType: NetWorkUtil.NetworkType?) {
    }
    ...
}

通过反射创建ViewModel,下面就在此基础上再推出支持ViewBinding的基类:

kotlin 复制代码
abstract class BaseVVDActivity<VM : BaseViewModel, VB : ViewBinding> : BaseVMActivity<VM>() {

    private var _binding: VB? = null
    protected val mBinding: VB
        get() = requireNotNull(_binding) { "ViewBinding对象为空" }

    // 反射创建ViewBinding
    protected open fun createViewBinding() {

        try {
            val clazz: Class<*> = (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[1] as Class<VB>
            val inflateMethod = clazz.getMethod("inflate", LayoutInflater::class.java)
            _binding = inflateMethod.invoke(null, layoutInflater) as VB
        } catch (e: Exception) {
            e.printStackTrace()
            throw IllegalArgumentException("无法通过反射创建ViewBinding对象")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        createViewBinding()
        super.onCreate(savedInstanceState)
    }

    override fun setContentView() {
        setContentView(mBinding.root)
    }

    override fun getLayoutIdRes(): Int = 0

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

使用:

kotlin 复制代码
class AuthLoginActivity : BaseVVDActivity<LoginViewModel, ActivityAuthLoginBinding>(), saf by SAF() {

    override fun startObserve() {

    }

    override fun init(savedInstanceState: Bundle?) {}
}

3.2 Hilt 的使用

新版 Hilt 的使用我之前的文章也有详细的讲解,不了解的可以看看,【Android开发为什么要用Hilt?new个对象这么简单的事为什么要把它复杂化?】

由于我们在 DefaultGradlePlugin 已经封装好了,hilt 的依赖和 kapt 等配置。

我们可以直接使用:

kotlin 复制代码
@HiltAndroidApp
class App :BaseApplication(){
    override fun onCreate() {
        super.onCreate()
    }

}

注入全局的依赖:

kotlin 复制代码
/**
 * 全局的DI注入
 */
@Module
@InstallIn(SingletonComponent::class)
class ApplicationDIModule {

    @Provides
    fun provideMyApplication(application: Application): App {
        return application as App
    }

    //全局的Gson,使用框架进行容错处理
    @Provides
    @Singleton
    fun provideGson(): Gson {
        return GsonFactory.getSingletonGson()
    }

}

在Activity 和 ViewModel 中分别注入 Gson 对象:

less 复制代码
@AndroidEntryPoint
class AuthLoginActivity : BaseVVDActivity<LoginViewModel, ActivityAuthLoginBinding>() {

    @Inject
    lateinit var mGson: Gson

}

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val mGson: Gson,
) : BaseViewModel() {

    fun testGson(innerGson: Gson) {

        MyLogUtils.w("是否是同一个Gson:${innerGson == mGson}")
    }
}

Log 如下:

Hilt 的使用相对比较简单,如果不了解可以参考我上面的链接。

四、MVI + UserCase的逻辑

MVI 的架构其实理解之后并不难,我对于全网的MVI架构做了简单的归纳整理,想要了解的可以看看我之前的文章【尘埃落地 , 遍历全网Android-MVI架构,从简单到复杂学习总结一波】

在MVI架构中,所有的UI逻辑都是通过状态(State)和意图(Intent)来管理的,这样做的好处是可以让UI的状态预测变得更加容易,同时也使得状态管理变得更加清晰。我在代码中定义了Intent、State、Effect,以及如何通过 ViewModel 来响应 Intent 并更新 State 或发送 Effect 。这样的结构有助于保持代码的可维护性和可测试性。

下面是一些核心代码:

less 复制代码
@Keep
interface IUIEffect

@Keep
interface IUiIntent

@Keep
interface IUiState

把 MVI 封装到 ViewModel 中去:

kotlin 复制代码
abstract class BaseISViewModel<I : IUiIntent, S : IUiState> : BaseViewModel() {

    private val _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<S> = _uiStateFlow

    //页面事件的 Channel 分发
    private val _uiIntentFlow = Channel<I>(Channel.UNLIMITED)

    //更新页面状态
    fun updateUiState(reducer: S.() -> S) {
        _uiStateFlow.update { reducer(_uiStateFlow.value) }
    }

    //更新State
    fun <T> sendUiState(reducer: T.() -> T) {

    }

    //发送页面事件
    fun sendUiIntent(uiIntent: I) {
        viewModelScope.launch {
            _uiIntentFlow.send(uiIntent)
        }
    }

    init {
        // 这里是通过Channel的方式自动分发的。
        viewModelScope.launch {
            //收集意图 (观察者模式改变之后就自动更新)用于协程通信的,所以需要在协程中调用
            _uiIntentFlow.consumeAsFlow().collect { intent ->
                handleIntent(intent)
            }
        }

    }

    //每个页面的 UiState 都不相同,必须实自己去创建
    protected abstract fun initUiState(): S

    //每个页面处理的 UiIntent 都不同,必须实现自己页面对应的状态处理
    protected abstract fun handleIntent(intent: I)

}

如果想要 EIS 三者都用上,可以用这个基类:

kotlin 复制代码
abstract class BaseEISViewModel<E : IUIEffect, I : IUiIntent, S : IUiState> : BaseISViewModel<I, S>() {

    //一次性事件,无需更新
    private val _effectFlow = MutableSharedFlow<E>()
    val uiEffectFlow: SharedFlow<E> by lazy { _effectFlow.asSharedFlow() }

    //两种方式发射
    protected fun sendEffect(builder: suspend () -> E?) = viewModelScope.launch {
        builder()?.let { _effectFlow.emit(it) }
    }

    //两种方式发射
    protected suspend fun sendEffect(effect: E) = _effectFlow.emit(effect)

}

使用:

比如我们在 Profile 组件中使用网络请求并展示,我们先定义对应的 EIS 类:

kotlin 复制代码
//Effect
sealed class ProfileEffect : IUIEffect {
    data class ToastArticle(val msg: String?) : ProfileEffect()
}

//Intent
sealed class ProfileIntent : IUiIntent {
    object FetchArticle : ProfileIntent()
    object FetchBanner : ProfileIntent()
}

//State
data class ProfileState(val bannerUiState: BannerUiState, val articleUiState: ArticleUiState) : IUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val banner: List<Banner>) : BannerUiState()
}

sealed class ArticleUiState {
    object INIT : ArticleUiState()
    data class SUCCESS(val article: List<TopArticleBean>) : ArticleUiState()
}

在 ProfileViewModel 中我们的写法:

kotlin 复制代码
@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val repository: ProfileRepository,
    val savedState: SavedStateHandle
) : BaseEISViewModel<ProfileEffect, ProfileIntent, ProfileState>() {

    override fun initUiState(): ProfileState = ProfileState(BannerUiState.INIT, ArticleUiState.INIT)

    override fun handleIntent(intent: ProfileIntent) {
        when (intent) {
            ProfileIntent.FetchBanner -> fetchBanner()
            ProfileIntent.FetchArticle -> fetchArticle()
        }
    }

    //测试加载 WanAndroid - Banner 的数据
    private fun fetchBanner() {

        launchOnUI {

            //开始Loading
            loadStartProgress()

            val bannerResult = repository.fetchBanner()

            if (bannerResult is OkResult.Success) {
                //成功
                loadHideProgress()

                updateUiState {
                    copy(bannerUiState = BannerUiState.SUCCESS(bannerResult.data))
                }

            } else {
                val message = (bannerResult as OkResult.Error).exception.message
                sendEffect(ProfileEffect.ToastArticle(message))
            }

        }

    }

    //加载页面数据,这里使用测试接口 WanAndroid - Article 的数据
    private fun fetchArticle() {

        launchOnUI {

            loadStartLoading()

            val articleResult = repository.fetchArticle()

            if (articleResult is OkResult.Success) {
                //成功
                loadSuccess()

                updateUiState {
                    copy(articleUiState = ArticleUiState.SUCCESS(articleResult.data))
                }

            } else {
                val message = (articleResult as OkResult.Error).exception.message
                sendEffect(ProfileEffect.ToastArticle(message))
            }

        }

    }

}

接下来我们需要在 Activity 中发送 Intent 和接收 State 或 Effect

kotlin 复制代码
 override fun startObserve() {
        //分开监听所有的状态
        lifecycleScope.launch {
            mViewModel.uiStateFlow
                .map { it.bannerUiState }
                .distinctUntilChanged()
                .collect { state ->
                    when (state) {
                        is BannerUiState.INIT -> {}
                        is BannerUiState.SUCCESS -> {
                            toast(state.banner.toString())
                        }
                    }
                }
        }

        lifecycleScope.launch {
            mViewModel.uiStateFlow
                .map { it.articleUiState }
                .distinctUntilChanged()
                .collect { state ->
                    when (state) {
                        is ArticleUiState.INIT -> {}
                        is ArticleUiState.SUCCESS -> {
                            toast(state.article.toString())
                        }
                    }
                }
        }

        //效果的SharedFlow监听
        lifecycleScope.launch {
            mViewModel.uiEffectFlow
                .collect {
                    when (it) {
                        is ProfileEffect.ToastArticle -> {
                            toast(it.msg)
                        }
                    }
                }
        }

    }

    override fun init(savedInstanceState: Bundle?) {

        mViewModel.sendUiIntent(ProfileIntent.FetchArticle)

        mBinding.btnProfile.click {
            mViewModel.sendUiIntent(ProfileIntent.FetchArticle)
        }

        mBinding.btnBanner.click {
            //这里使用 WanAndroid - Banner 的数据用于测试
            mViewModel.sendUiIntent(ProfileIntent.FetchBanner)
        }
    }

效果图:

至于 UserCase 我们可以理解为数据仓库与 ViewModel 中间的一层,对于一些固定的常用的逻辑做单独的封装,我们的 ViewModel 是可以直接用数据仓库也可以选择性的使用 UserCase 。

如果你对 UserCase 不太了解,可以移步大佬的文章 Android 官方架构中的 UseCase 该怎么写? 参考。

我在这里举个不恰当的例子,我们把获取文章列表的处理放入到 UserCase 中(实际上没有必要):

kotlin 复制代码
@Singleton
class ArticleUserCase @Inject constructor(
    private val repository: ProfileRepository
) {

    //唯一入口
    suspend fun invoke(): OkResult<List<TopArticleBean>> {
        //模拟一些其他特殊的逻辑,如果只是网络请求,直接在ViewModel中用Repository发起即可,这里仅为测试
        return repository.fetchTopArticle()
        //或者可以拿到数据之后做其他的操作最后返回给外部
    }

}

我们就可以在 ViewModel 中注入这个单例类去使用:

kotlin 复制代码
@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val repository: ProfileRepository,
    val savedState: SavedStateHandle
) : BaseEISViewModel<ProfileEffect, ProfileIntent, ProfileState>() {

    @Inject
    lateinit var articleUserCase: ArticleUserCase

    ...

    private fun fetchArticle() {

        launchOnUI {

            loadStartLoading()

            val articleResult = articleUserCase.invoke()

            if (articleResult is OkResult.Success) {
                //成功
                loadSuccess()

                updateUiState {
                    copy(articleUiState = ArticleUiState.SUCCESS(articleResult.data))
                }

            } else {
                val message = (articleResult as OkResult.Error).exception.message
                sendEffect(ProfileEffect.ToastArticle(message))
            }

        }

    }

}

为什么说这个逻辑不恰当,因为数据仓库可以直接在 ViewModel 中使用的,我们日常开发会把一些数据逻辑或 API 逻辑用 UserCase 封装方便任意地方快速调用,例如用户状态的校验,人脸身份校验,指纹校验等。

除了 Profile 组件,我在 Auth 组件中也有 MVI 的一些变种使用,例如 UIState 的数量只有一个怎么解决,UIIntent 要传递参数如何解决,具体的代码可以去 Demo 中查看,这里就不贴一些重复的代码。

总结:

为什么本文我一直强调 Demo ,因为真的只是 Demo 性质啊,可以用于交流与学习,也仅供大家参考,万不可直接生搬硬套直接使用。如果想要用于真实项目开发,那么还有很多东西需要修改和测试,你需要自己把握。

本项目其实都是针对一些开发中遇到的痛点做出的调整,比如为什么要这样的方式做版本管理,为什么要这么组件化,为什么要用Hilt,为什么要用ViewBinding,为什么要用 MVI 架构,等等都是实际开发中感觉到痛了才会想要改善,并且是随着项目越来越大这种"痛感"越来越无法忍受,所以才会想做这个Demo。

你可能遇到的问题,我先帮你问了。

为什么我Clone下来Hilt无法通过编译?

Gradle 依赖冲突,需要排重和指定版本,后续版本已修复。

为什么你的 ARouter 可以在 Gradle8.0 以上运行?

确实 ARouter 无法运行在高版本,trasform 已经被移除,但是有很多基于ARouter实现的第三方库可以用,代码中备注了,当然你可以参考文章自己进行修改【传送门1】【传送门2】

我用其他路由可不可以?

总的来说 ARouter 原理都被我们翻烂了,比较熟悉才选用的,你当然可以用其他的路由,例如支持 Gradle 高版本的 TheRouter 路由,或者其他的路由都行,其实基本功能都是类似的。

为什么你用 XML 不用 Compose ?我要用 Compose 可以吗?

当然可以,你把 ViewBinding 的配置去掉,加入 Compose 的一些依赖,你甚至连 Activity 的基类都不需要了,我甚至把 Compose 的依赖都留好了,直接依赖使用即可。

我们为什么不用 Compose?肯定是因为我菜嘛...

因为我们的开发团队都不了解 Compose 没有相关经验相对来说比较抗拒,我倒是想用但也不是我说了算啊...

再就是考虑到当前还是 XML - View 体系的开发者更多一些,也更成熟稳定,所以 Demo 还是用的 XML 体系,不过后期我可能会更新 Compose 版本 Demo 自己玩玩,说不准。

好了,闲话就说到这里,如果有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

最后本文源码奉上,恳请各位大佬高工指点 【传送门】

当然你也可以关注我的老Kotlin项目,会有一些零散的知识点,我有时间我都会持续更新。

PS: 其实我很早就有这个想法去做这个项目,为什么现在才做? 因为平常上班有项目在忙,没有那么多碎片化的时间,下班去做?... 我下班都不开AS的好吧,唯一有时间的就是过年这几天,所以看Git提交记录这个项目和文章基本上是过年期间完成的,不过过年也忙,白天到处走亲戚基本不在家,都是过年的抽几天大晚上肝出来的。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

相关推荐
Dnelic-2 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年12 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿14 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛16 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法16 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter18 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快19 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl19 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5