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

2,295 阅读21分钟

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

前言

如果项目拆分的过于细致,层级太多导致同事看不懂代码了😏 这 ... 快救救孩子吧。

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

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

Demo 的各种依赖可以说是相对较新的,使用的一些常规的官方插件和流行的第三方插件如 Retrofit,至于其他的小功能模块,例如 Log 框架,Json解析框架,图片加载框架等,由于很多人的使用习惯不同,对于这些三方小插件我不做过多介绍,你可以自行替换你需要的对应框架即可。

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

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

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

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

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

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

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

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

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

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

image.png

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

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

/**
 * @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 的 单例类中定义一些依赖与版本:

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:

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 中使用即可:

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:

/**
 * @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 只需要这样就可以了:

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 中就只需要这样即可:

plugins {
    id("com.android.library")
}

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

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

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

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

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

2.1 组件拆分

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

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

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

image.png

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

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

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

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

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

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

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

auth - build.gradle.kts:

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

使用:

        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模块定义路由:

interface IAppService : IProvider {
    fun getPushTokenId(): String
    fun getAppVersion(): AndroidVersion
}

App 组件定义Entiry:

data class AndroidVersion(val code: String, val url: String)

App 组件实现路由:

@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 组件定义的接口:

interface IProfileService : IProvider {

    suspend fun fetchUserProfile(): UserProfile
}

Profile 组件定义的Entiry:

data class UserProfile(val userId: String, val userName: String, val gender: Int)

Profile 组件实现的路由:

@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 模块的逻辑调用。

@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()}")

            }

        }

    }


}

效果图:

template-01.gif

2.3 组件独立运行

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

比如 Auth 组件:

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 壳整体项目。

如图:

image.png

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

效果图:

template-02.gif

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

三、ViewBinding 与 Hilt 的示例

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

3.1 ViewBinding 的使用与封装

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

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

    buildFeatures {
        viewBinding = true
    }

封装:

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,自己再赋值
}

使用:

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

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

简单的Activity基类:

/**
 * 最底层的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的基类:

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的基类:

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
    }
}

使用:

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 等配置。

我们可以直接使用:

@HiltAndroidApp
class App :BaseApplication(){
    override fun onCreate() {
        super.onCreate()
    }

}

注入全局的依赖:

/**
 * 全局的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 对象:

@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 如下:

image.png

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

四、MVI + UserCase的逻辑

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

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

下面是一些核心代码:

@Keep
interface IUIEffect

@Keep
interface IUiIntent

@Keep
interface IUiState

把 MVI 封装到 ViewModel 中去:

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 三者都用上,可以用这个基类:

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 类:

//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 中我们的写法:

@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

 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)
        }
    }

效果图:

template-03.gif

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

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

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

@Singleton
class ArticleUserCase @Inject constructor(
    private val repository: ProfileRepository
) {

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

}

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

@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 自己玩玩,说不准。

为什么你用Catalogs,BuildSrc太慢了过时了

已经写了文章还在审核中,期待一下,其实他们都有各自的优缺点,后期文章发布了我会在此更新链接。

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

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

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

PS: 其实我很早就有这个想法去总结一下, 由于平常上班有项目在忙,没有那么多碎片化的时间,至于下班...我要追番打游戏的好吧,唯一有时间的就是过年这几天,所以看Git提交记录这个项目和文章基本上都是过年的时候抽了几天晚上肝出来的。

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

Ok,这一期就此完结。