# 2024AndroidTemplate **Repository Path**: Yefenyun99/2024-android-template ## Basic Information - **Project Name**: 2024AndroidTemplate - **Description**: 博客有完整介绍,博客主页地址:https://juejin.cn/user/2384195547303688 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 27 - **Created**: 2024-06-24 - **Last Updated**: 2024-06-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- theme: smartblue highlight: agate --- # 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开发依赖版本管理的几种方式】](https://juejin.cn/post/7098878694449479688),在两年后的2024年再看来是有点落伍了。 为什么不继续用了呢?因为还是不够方便,不能点击查看,虽然可以仿继承实现,但是封装的细度不够,难免也会有一处改动多处修改的问题。而通过 buildSrc + gradle.kts 的方式会更加的方便。 通过 buildSrc 来统一管理依赖版本,这是之前就很流行的方案了,如何创建如何使用?如果你不了解我想你可能需要搜索引擎一下,我没必要复制粘贴不然篇幅太长了。 但是通过 Kotlin 的方式搭配 gradle.kts 的方案,通过扩展方法的使用、继承的使用可以更加便捷的封装 gradle 版本与依赖版本,可以更方便的管理依赖版本。通过使用函数式定义,可以快速的点击跳转到指定的依赖或依赖组。 gradle.kts 是什么?怎么用?这... 这不是本文的重点啊,我默认当做你已经会了,如果实在不了解可以先搜索引擎了解下,也不是什么高深的知识点。 接下来继续,在本 Demo 的 buildSrc 中有代码如下: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/afba15fcce514946ac12862f5bc44ef2~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=431&h=361&s=32148&e=png&b=2b2d30) 我们现在 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 { 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() 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() android { namespace = "com.newki.auth" } ``` 这样library 和 application 模块就都能使用一套配置,封装起来再使用是不是很方便呢?如果需要修改只需要修改一处基类即可,如果该 library 有特殊的地方需要重写的地方也可以在对应的 build.gradle.kts 重写配置。 ### 二、组件化与路由与独立运行配置 组件与路由是密不可分的整体,有组件必有路由,这里的组件化与组件化拆分也是基于路由来实现的。 #### 2.1 组件拆分 组件化大家不是都会吗?我前两年也出过类似的文章[【Android组件化,这可能是最完美的形态吧】](https://juejin.cn/post/7099636408045961224) 之前一直是按照这个思路开发的,但是随着项目的演变,组件越来越多,由于没有拆分组件,导致很多的重复数据仓库和冗余的公共服务模块,导致我们的开发者苦不堪言难以维护,所以在新的架构中我们一定要注意组件的拆分。 如何划分组件?一张图秒懂: ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90d02bf152484f48b83000a7f19256c7~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=265&h=626&s=37997&e=png&b=2c2e31) 说了这么多,为什么要把一个组件拆分为主组件与Api组件? 主要是为了逻辑分离,路由分离,其他组件可能用到此组件的地方都在Api中定义,常见如数据仓库,接口,自定义对象等。 为什么会有重复数据仓库和冗余的公共服务模块呢? 例如上图中的 Auth 组件,它需要在用户登录完成之后,调用到 Profile 组件的用户详情接口,然后告诉 App 组件登录成功,那么此时我应该怎么写? 把 Profile 组件中的用户详情数据仓库复制一份? 如果每一个组件都这么搞,那么组件化就无意义,一旦要修改还得每一个组件都检查去修改,那么组件化的意义何在?起到了反作用。 告诉 App 组件登录成功,写入缓存,App 模块是我的上级,我如何能操作我的上级组件?大家常用的做法就是逻辑下沉,放入到公共的 Service 组件中去,这是一个办法,但是不够优雅,一旦有问题就下沉导致逻辑划分不清晰,公共模块臃肿,一旦产品逻辑变动会有大量冗余资源和代码。 怎么解决这些问题呢?就是上面说到的拆分组件,把一个组件分为主组件与Api组件,Auth 组件就只需要依赖对于的 Api 组件即可通过路由操作了。 auth - build.gradle.kts: ``` plugins { id("com.android.library") } // 使用自定义插件 apply() 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() { companion object { fun startInstance() { commContext().gotoActivity() } } override fun getLayoutIdRes(): Int = R.layout.activity_auth_login override fun startObserve() { } override fun init(savedInstanceState: Bundle?) { findViewById