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,这一期就此完结。