相关链接:

nowinandroid项目链接

nowinandroid作为Google官方的app,在github上其实是有开源的,并且这个项目一向在保护,关于Android一些新式的技术,都会作为更新的要点,从nowinandroid中也会学到许多现代Android开发的一些先进思维,关于咱们的app架构升级会供给一些新的思路。

所以【现代Android技术探究】将会作为新开的一个专题,首要针对nowinandroid中一些核心技术,例如组件化模块化架构、gradle、lint、单元测试等等进行叙述,由于整个项目我也没有彻底看完,只能从全体到局部讲起,最终再深挖细节。

1 nowinandroid架构规划

从下图看,nowinandroid的全体架构规划选用的是模块化的规划思维。

现代Android技术探索 -- 将gradle迁移到kotlin版本

app相当于一个壳工程;core-x模块则是一些根底的模块,像network(网络相关)、ui(组件库)、database(数据库)等,为app供给根底才能;而feature-x模块则是详细的事务完结层,app将会直接依靠这些事务模块。

所以全体的架构仍是非常清晰的,标准的模块化的架构规划。

现代Android技术探索 -- 将gradle迁移到kotlin版本

已然谈到了模块化,咱们看下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】,或者扫码重视,每周不定时更新,也有惊喜红包哦,也能够后台留言感兴趣的专题,给各位同伴们产出文章。

现代Android技术探索 -- 将gradle迁移到kotlin版本