相关链接:
nowinandroid作为Google官方的app,在github上其实是有开源的,而且这个项目一直在维护,对于Android一些新兴的技术,都会作为更新的重点,从nowinandroid中也会学到很多现代Android开发的一些先进思想,对于我们的app架构升级会提供一些新的思路。
所以【现代Android技术探索】将会作为新开的一个专题,主要针对nowinandroid中一些核心技术,例如组件化模块化架构、gradle、lint、单元测试等等进行讲述,因为整个项目我也没有完全看完,只能从整体到局部讲起,最后再深挖细节。
1 nowinandroid架构设计
从下图看,nowinandroid的整体架构设计采用的是模块化的设计思想。
app相当于一个壳工程;core-x模块则是一些基础的模块,像network(网络相关)、ui(组件库)、database(数据库)等,为app提供基础能力;而feature-x模块则是具体的业务实现层,app将会直接依赖这些业务模块。
所以整体的架构还是非常清晰的,标准的模块化的架构设计。
既然谈到了模块化,我们看下nowinandroid是如何完成依赖管理的,相信会有对我们开发有用的东西。
2 nowinandroid版本管理
在nowinandroid中,gradle脚本都是kotlin编写,而不是传统的groovy语法,其实groovy编写gradle脚本一直都有一个痛点就是代码提示不好,而使用kotlin更符合Android开发者的习惯,因此慢慢地kotlin会成为gradle脚本开发的主流语言,在Google开发者文档中,也已经将kotlin作为官方开发语言了。但是使用kotlin编写脚本在编译时没有groovy的速度快,性能上差点,但是也是在编译时,并不影响运行时速度。
2.1 kotlin编写脚本
其实,如果我们熟悉了gradle脚本的结构,那么在转成kotlin之后,只需要关注几个点就可以快速的上手。当我们创建一个新项目之后,会自动生成根project和app对应的gradle脚本,默认是groovy语言。
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
id 'org.jetbrains.kotlin.jvm' version '1.7.10' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.lay.layzproject"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
implementation project(':datastore')
implementation project(':handler')
}
那么在迁移到kotlin之后,记住首先从根project开始,将gradle脚本改为.kts后缀的文件,对于‘ ’修饰的成员变量,全部变成(" ")。
plugins {
id("com.android.application") version ("7.2.1") apply false
id("com.android.library") version ("7.2.1") apply false
id("org.jetbrains.kotlin.android") version ("1.7.10") apply false
id("org.jetbrains.kotlin.jvm") version ("1.7.10") apply false
}
tasks.register("clean", Delete::class.java) {
delete(rootProject.buildDir)
}
对于task的创建,采用TaskContainer的register方法创建。
像groovy中比较有特色的闭包,在kotlin中与lambda表达式基本是一致的,这部分基本上可以不用改变,需要改变的就是闭包内的方法处理,这里在写的时候,会有代码提示。
例如在修改app模块下的gradle脚本时,android闭包下对应的就是BaseAppModuleExtension对象,
/**
* Configures the [android][com.android.build.gradle.internal.dsl.BaseAppModuleExtension] extension.
*/
fun org.gradle.api.Project.`android`(configure: Action<com.android.build.gradle.internal.dsl.BaseAppModuleExtension>): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)
在这个对象中,存在defaultConfig、buildTypes等常见的配置参数,所以在Kotlin中,仿照groovy进行配置即可。
open class BaseAppModuleExtension(
dslServices: DslServices,
bootClasspathConfig: BootClasspathConfig,
buildOutputs: NamedDomainObjectContainer<BaseVariantOutput>,
sourceSetManager: SourceSetManager,
extraModelInfo: ExtraModelInfo,
private val publicExtensionImpl: ApplicationExtensionImpl
) : AppExtension(
dslServices,
bootClasspathConfig,
buildOutputs,
sourceSetManager,
extraModelInfo,
true
), InternalApplicationExtension by publicExtensionImpl {
// Overrides to make the parameterized types match, due to BaseExtension being part of
// the previous public API and not wanting to paramerterize that.
override val buildTypes: NamedDomainObjectContainer<BuildType>
get() = publicExtensionImpl.buildTypes as NamedDomainObjectContainer<BuildType>
override val defaultConfig: DefaultConfig
get() = publicExtensionImpl.defaultConfig as DefaultConfig
override val productFlavors: NamedDomainObjectContainer<ProductFlavor>
get() = publicExtensionImpl.productFlavors as NamedDomainObjectContainer<ProductFlavor>
override val sourceSets: NamedDomainObjectContainer<AndroidSourceSet>
get() = publicExtensionImpl.sourceSets
override val viewBinding: ViewBindingOptions =
dslServices.newInstance(
ViewBindingOptionsImpl::class.java,
publicExtensionImpl.buildFeatures,
dslServices
)
override val composeOptions: ComposeOptions = publicExtensionImpl.composeOptions
override val bundle: BundleOptions = publicExtensionImpl.bundle as BundleOptions
override val flavorDimensionList: MutableList<String>
get() = flavorDimensions
override val buildToolsRevision: Revision
get() = Revision.parseRevision(buildToolsVersion, Revision.Precision.MICRO)
override val libraryRequests: MutableCollection<LibraryRequest>
get() = publicExtensionImpl.libraryRequests
}
2.1.1 android节点
修改成kotlin语法如下:
android{
//BaseAppModuleExtension
compileSdk = 32
defaultConfig {
//ApplicationDefaultConfig
applicationId = "com.lay.layzproject"
minSdk = 21
targetSdk = 32
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes{
release {
//ApplicationBuildType
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"),"proguard-rules.pro")
}
}
compileOptions{
sourceCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
targetCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
}
kotlinOptions{
jvmTarget = "1.8"
}
}
2.1.2 dependencies节点
修改成kotlin语法如下:
dependencies {
implementation("androidx.core:core-ktx:1.7.0")
implementation ("androidx.appcompat:appcompat:1.5.1")
implementation ("com.google.android.material:material:1.7.0")
implementation ("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation ("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation ("androidx.test.espresso:espresso-core:3.5.0")
implementation (project (":datastore"))
implementation (project (":handler"))
}
这里也只是简单的将初始化工程中的gradle脚本修改成kotlin,实际项目开发中也不会跑偏,大致也就是上述的这些场景。
2.2 version catalog版本管理
像在组件化或者模块化的架构设计中,因为模块众多,所有的模块可能会有一些共同的依赖配置项,我们需要做到的就是三方库的版本保持一致,一般都是创建一个config.gradle,在其中创建一些扩展字段用于其他模块复用。
ext {
libs = [
"coreKtx": "androidx.core:core-ktx:1.7.0"
]
}
但是在kotlin脚本中,这些全部失效了,无法通过定义ext的方式进行扩展,但是有对应的catalog可以帮助我们实现这个能力。
什么是catalog,其实就是就是一个版本目录,对于Android开发人员来说,主要工作就是往表中配置三方库,但是需要注意格式。
第一步:在gradle文件夹下,创建libs.versions.toml文件
在toml文件中,需要声明两个标签,[versions]代表三方库的版本,[libraries]代表三方库的信息,像group、name等,version.ref就是引用了在[versions]中定义的版本号。
[versions]
androidxCore = "1.7.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
第二步:在setting.gradle文件中声明允许使用version catalog
Using dependency catalogs requires the activation of the matching feature preview
在toml文件中配置完成之后,编译发现报上面的错误,原因就是如果要使用versions catalog,就需要配置开关。
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
//开关
enableFeaturePreview("VERSION_CATALOGS")
repositories {
google()
mavenCentral()
}
}
如果开启了VERSION_CATALOGS这个开关,那么系统会默认生成一个libs文件对应libs.versions.toml文件。
implementation(libs.androidx.core.ktx)
那么在每个模块使用的时候,就可以直接拿到在toml文件中定义的三方库,从源码中看,就是根据定义的libraries的key获取的。
public Provider<MinimalExternalModuleDependency> getKtx() { return create("androidx.core.ktx"); }
其实这样使用的效果其实跟groovy中定义的扩展类似,而且使用versions catalog会更加规范,在nowinandroid中,就是这样定义的,如下:
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.startup)
implementation(libs.androidx.work.ktx)
implementation(libs.hilt.ext.work)
3 nowinandroid依赖管理
在第一节的架构图中,我们看到业务模块一定要依赖基础的core模块,否则无法完成网络请求、数据持久化等操作,但是从feature-author模块的gradle中发现,dependencies中只依赖了datetime一个三方库,那么跟其他模块的依赖是如何完成的呢?
plugins {
id("nowinandroid.android.library")
id("nowinandroid.android.feature")
id("nowinandroid.android.library.compose")
id("nowinandroid.android.library.jacoco")
id("dagger.hilt.android.plugin")
id("nowinandroid.spotless")
}
dependencies {
implementation(libs.kotlinx.datetime)
}
3.1 includeBuild替代buildSrc
其实从plugins中不难看出,既然没有直接通过implementation的方式直接引用,那么应该就是采用了gradle插件的形式,在编译时配置项目依赖。
在之前关于gradle插件的编写,都是创建buildSrc文件夹来完成的,其实buildSrc有一个最大的问题就是:在buildSrc中做微小的改动就会导致整个项目的全量编译,随着项目的增大,就会变得越来越慢。
因此在nowinandroid中,并没有使用传统的buildSrc,而是采用了includeBuild,使用这种方式可以将任意一个项目变为插件工程,而且实现的效果与buildSrc一致,但编译速度比buildSrc快好几个等级。
所以接下来就是使用includeBuild的方式:
(1)创建一个Java或者Kotlin library工程,并在settings.gradle中引入插件工程
pluginManagement {
//引入插件工程
includeBuild("build-logic")
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
(2)配置插件工程gradle依赖,与之前类似
plugins{
`kotlin-dsl`
}
java{
sourceCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
targetCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
}
repositories{
google()
mavenCentral()
}
dependencies{
compileOnly("com.android.tools.build:gradle:7.2.2")
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
}
(3)自定义插件
class AppDepsPlugin : Plugin<Project>{
override fun apply(target: Project) {
print("AppDepsPlugin --- ${target.name}")
}
}
在以往的buildSrc开发中,因为全局只有一个工程,因此会直接编译后配置到classpath下,然后需要创建一个META-INF文件夹,声明插件的名称,在Module中引入插件,但这种方式就比较鸡肋了,需要一种动态注册的方式,而在nowinandroid中,我们并没有看到一堆META-INF文件,而是采用下面这种方式。
(4)插件注册
只要有任何插件的创建,都可以在gradlePlugin # plugins中进行注册。
gradlePlugin{
plugins {
register("androidAppModuleConfig"){
id = "app_deps_config"
implementationClass = "com.tal.build_logic.plugin.AppDepsPlugin"
}
}
}
看下原始的插件声明文件,是不是和上面的声明很类似:
implementation-class=com.lay.asm.ASMPlugin
但是我们这里是通过动态注册的方式完成,对应的id就是可以在每个模块下的声明的插件id,implementationClass对应的就是插件的全类名。
Type-safe dependency accessors is an incubating feature.
> Task :build-logic:compileKotlin
> Task :build-logic:compileJava NO-SOURCE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:inspectClassesForKotlinIC
> Task :build-logic:jar
> Configure project :app
AppDepsPlugin --- app
这样我们的插件就生效了。
3.2 自定义插件完成依赖配置
class AppDepsPlugin : Plugin<Project>{
override fun apply(target: Project) {
println("AppDepsPlugin --- ${target.name}")
with(target){
//从上到下
with(pluginManager){
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
}
//获取全局版本配置
val libs = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
//配置依赖
dependencies {
add("implementation",libs.findDependency("androidx-core-ktx").get())
add("implementation",project(":datastore"))
add("implementation",project(":handler"))
}
}
}
}
这里我简单介绍一下,其实在使用插件进行依赖配置时,对于Android开发人员是非常便捷的,因为完全可以根据gradle脚本中的配置顺序进行处理,因为有kotlin-dsl插件,所以像dependencies这种可以直接进行配置。
如果像android这种节点,可以通过扩展函数来查找对应的类进行配置。
extensions.configure(BaseAppModuleExtension::class.java){
defaultConfig {
targetSdk = 21
}
}
因为我们之前在toml文件中做了全局的版本管理,所以在进行依赖配置时,可以通过extensions来获取libs文件,以便通过findLibrary或者findDependency来获取。
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("org.jetbrains.kotlin.kapt")
}
extensions.configure<LibraryExtension> {
defaultConfig {
testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
}
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", project(":core-model"))
add("implementation", project(":core-ui"))
add("implementation", project(":core-designsystem"))
add("implementation", project(":core-data"))
add("implementation", project(":core-common"))
add("implementation", project(":core-navigation"))
add("testImplementation", project(":core-testing"))
add("androidTestImplementation", project(":core-testing"))
add("implementation", libs.findLibrary("coil.kt").get())
add("implementation", libs.findLibrary("coil.kt.compose").get())
add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
add("implementation", libs.findLibrary("kotlinx.coroutines.android").get())
add("implementation", libs.findLibrary("hilt.android").get())
add("kapt", libs.findLibrary("hilt.compiler").get())
// TODO : Remove this dependency once we upgrade to Android Studio Dolphin b/228889042
// These dependencies are currently necessary to render Compose previews
add(
"debugImplementation",
libs.findLibrary("androidx.customview.poolingcontainer").get()
)
}
}
}
}
通过这种方式就可以实现全局的依赖配置,仅需要通过plugin id即可,而相较于之前完全通过gradle脚本来配置,这种配置显然更加灵活,同时也满足了开闭原则,如果需要新的配置,只需要更换plugin id即可。
app模块插件配置:
plugins {
id("app_deps_config")
}
如果感兴趣的伙伴,可以看之前的一篇文章,关于组件化优化的 ->
Android Gradle的神奇之处 ---- 组件化优化
当然拿到nowinandroid工程之后,首先关注的就是整体的架构,以及模块之间的配置,由此引申到现在主流的Android开发用到的技术,当然如果我们的项目中gradle文件太多,也不要一次性地把gradle文件全部替换成.kts文件,因为两者是完全兼容的,一点一点地改,防止一片爆红。
最近刚开通了微信公众号,各位伙伴可以搜索【layz4Android】,或者扫码关注,每周不定时更新,也有惊喜红包🧧哦,也可以后台留言感兴趣的专题,给各位伙伴们产出文章。