相关链接:
nowinandroid项目链接
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】,或者扫码重视,每周不定时更新,也有惊喜红包哦,也能够后台留言感兴趣的专题,给各位同伴们产出文章。