Port upstream improvements and major version upgrades

Phase 1 - Low-risk changes:
- Enum.values() → Enum.entries (Kotlin modern idiom)
- SDK 36 + minSdk 23 across all convention plugins
- Simplify ConnectivityManager (remove API level branching for minSdk 23)
- Roborazzi 1.51.0 → 1.56.0

Phase 2 - Major version upgrades:
- Kotlin 2.1.20 → 2.3.0
- KSP 2.1.20-1.0.32 → 2.3.0 (KSP1 → KSP2)
- Compose Multiplatform 1.8.0 → 1.10.0
- Gradle 8.13 → 9.3.1
- Ktorfit 2.5.1 → 2.7.2 (with compilerPluginVersion 2.3.3)
- Fix Gradle 9 failOnNoDiscoveredTests default change
- Fix KSP2 task dependency for Koin annotation processing

Phase 3 - Module graph automation:
- Add Graph.kt with graphDump/graphUpdate tasks for KMP modules
- Add RootConventionPlugin to register graph tasks
- Add CI graph update/check steps in Build.yaml

Phase 4 - Spotless migration into convention plugins:
- Add Spotless.kt with configureSpotlessForAndroid/configureSpotlessForJvm
- Integrate into KmpLibrary, CmpApplication, JvmLibrary, AndroidTest plugins
- Add inline spotless config for root project and app-nia-catalog
- Delete gradle/init.gradle.kts (no longer needed)
- Update all references (CI, pre-push, IDE configs, AGENTS.md)
pull/2064/head
Mercury Li 2 weeks ago
parent 79d859ad95
commit 7ff2124d33

@ -10,7 +10,7 @@ Thanks for submitting a pull request. To accept your pull request we need you do
**Ensure tests pass and code is formatted correctly**
- Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug`
- Fix code formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
- Fix code formatting: `./gradlew spotlessApply`
**Add a description**

@ -48,7 +48,35 @@ jobs:
run: ./gradlew :build-logic:convention:check
- name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
run: ./gradlew spotlessCheck
- name: Update Graphs
id: graphs_update
continue-on-error: true
run: ./gradlew graphUpdate
- name: Check Graphs
id: graphs_verify
run: |
if ! git diff --quiet "**/README.md"; then
echo "::error::Module graph updates detected. Please run './gradlew graphUpdate' locally and commit the changes."
exit 1
fi
- name: Prevent updating graphs if this is a fork
id: checkfork_graphs
continue-on-error: false
if: steps.graphs_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Module graph updates detected, please update graphs with: ./gradlew graphUpdate" && exit 1
- name: Push updated graphs if available
uses: stefanzweifel/git-auto-commit-action@v5
if: steps.graphs_update.outcome == 'success' && steps.graphs_verify.outcome == 'failure' && github.event_name == 'pull_request'
with:
file_pattern: '**/README.md'
disable_globbing: true
commit_message: "Updates module graphs"
- name: Check Dependency Guard
id: dependencyguard_verify

@ -4,7 +4,7 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--init-script=gradle/init.gradle.kts" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>

@ -11,7 +11,7 @@ Now in Android (KMP edition) — a Kotlin Multiplatform fork of Google's Now in
```bash
# Format code (must pass before merge)
./gradlew spotlessApply
./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
./gradlew spotlessCheck
# Build
./gradlew :app:assemble # main app (all variants)
@ -43,6 +43,9 @@ Now in Android (KMP edition) — a Kotlin Multiplatform fork of Google's Now in
# Build-logic check
./gradlew :build-logic:convention:check
# Module graphs
./gradlew graphUpdate # update README.md module graphs
# Badging check
./gradlew :app:checkReleaseBadging
```

@ -26,6 +26,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.compose)
alias(libs.plugins.spotless)
}
kotlin {
@ -97,8 +98,8 @@ android {
versionCode = 1
versionName = "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
minSdk = 24
targetSdk = 34
compileSdk = 35
targetSdk = 36
compileSdk = 36
// The UI catalog does not depend on content from the app, however, it depends on modules
// which do, so we must specify a default value for the contentType dimension.
missingDimensionStrategy("contentType", "demo")
@ -147,10 +148,24 @@ compose.desktop {
}
}
compose.experimental {
}
dependencyGuard {
configuration("releaseRuntimeClasspath")
}
spotless {
kotlin {
target("src/**/*.kt")
ktlint(libs.versions.ktlint.get())
.editorConfigOverride(mapOf("android" to "true"))
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("*.kts")
targetExclude("**/build/**/*.kts")
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
format("xml") {
target("src/**/*.xml")
licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
}
}

@ -11,83 +11,100 @@ androidx.autofill:autofill:1.0.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
androidx.compose.animation:animation-android:1.8.0
androidx.compose.animation:animation-core-android:1.8.0
androidx.compose.animation:animation-core:1.8.0
androidx.compose.animation:animation:1.8.0
androidx.compose.foundation:foundation-android:1.8.0
androidx.compose.foundation:foundation-layout-android:1.8.0
androidx.compose.foundation:foundation-layout:1.8.0
androidx.compose.foundation:foundation:1.8.0
androidx.compose.animation:animation-android:1.10.0
androidx.compose.animation:animation-core-android:1.10.0
androidx.compose.animation:animation-core:1.10.0
androidx.compose.animation:animation:1.10.0
androidx.compose.foundation:foundation-android:1.10.0
androidx.compose.foundation:foundation-layout-android:1.10.0
androidx.compose.foundation:foundation-layout:1.10.0
androidx.compose.foundation:foundation:1.10.0
androidx.compose.material3.adaptive:adaptive-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout-android:1.1.0
androidx.compose.material3.adaptive:adaptive-layout:1.1.0
androidx.compose.material3.adaptive:adaptive:1.1.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.2
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.2
androidx.compose.material3:material3-android:1.3.2
androidx.compose.material3:material3:1.3.2
androidx.compose.material:material-android:1.8.0
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.4.0
androidx.compose.material3:material3-adaptive-navigation-suite:1.4.0
androidx.compose.material3:material3-android:1.4.0
androidx.compose.material3:material3:1.4.0
androidx.compose.material:material-android:1.10.0
androidx.compose.material:material-icons-core-android:1.7.6
androidx.compose.material:material-icons-core:1.7.6
androidx.compose.material:material-icons-extended-android:1.7.6
androidx.compose.material:material-icons-extended:1.7.6
androidx.compose.material:material-ripple-android:1.8.0
androidx.compose.material:material-ripple:1.8.0
androidx.compose.material:material:1.8.0
androidx.compose.runtime:runtime-android:1.8.1
androidx.compose.runtime:runtime-saveable-android:1.8.1
androidx.compose.runtime:runtime-saveable:1.8.1
androidx.compose.runtime:runtime:1.8.1
androidx.compose.ui:ui-android:1.8.1
androidx.compose.ui:ui-geometry-android:1.8.1
androidx.compose.ui:ui-geometry:1.8.1
androidx.compose.ui:ui-graphics-android:1.8.1
androidx.compose.ui:ui-graphics:1.8.1
androidx.compose.ui:ui-text-android:1.8.1
androidx.compose.ui:ui-text:1.8.1
androidx.compose.ui:ui-tooling-preview-android:1.8.1
androidx.compose.ui:ui-tooling-preview:1.8.1
androidx.compose.ui:ui-unit-android:1.8.1
androidx.compose.ui:ui-unit:1.8.1
androidx.compose.ui:ui-util-android:1.8.1
androidx.compose.ui:ui-util:1.8.1
androidx.compose.ui:ui:1.8.1
androidx.compose.material:material-ripple-android:1.10.0
androidx.compose.material:material-ripple:1.10.0
androidx.compose.material:material:1.10.0
androidx.compose.runtime:runtime-android:1.10.0
androidx.compose.runtime:runtime-annotation-android:1.10.0
androidx.compose.runtime:runtime-annotation:1.10.0
androidx.compose.runtime:runtime-retain-android:1.10.0
androidx.compose.runtime:runtime-retain:1.10.0
androidx.compose.runtime:runtime-saveable-android:1.10.0
androidx.compose.runtime:runtime-saveable:1.10.0
androidx.compose.runtime:runtime:1.10.0
androidx.compose.ui:ui-android:1.10.0
androidx.compose.ui:ui-geometry-android:1.10.0
androidx.compose.ui:ui-geometry:1.10.0
androidx.compose.ui:ui-graphics-android:1.10.0
androidx.compose.ui:ui-graphics:1.10.0
androidx.compose.ui:ui-text-android:1.10.0
androidx.compose.ui:ui-text:1.10.0
androidx.compose.ui:ui-tooling-preview-android:1.10.0
androidx.compose.ui:ui-tooling-preview:1.10.0
androidx.compose.ui:ui-unit-android:1.10.0
androidx.compose.ui:ui-unit:1.10.0
androidx.compose.ui:ui-util-android:1.10.0
androidx.compose.ui:ui-util:1.10.0
androidx.compose.ui:ui:1.10.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.15.0
androidx.core:core:1.15.0
androidx.core:core-ktx:1.16.0
androidx.core:core-viewtree:1.0.0
androidx.core:core:1.16.0
androidx.customview:customview-poolingcontainer:1.0.0
androidx.documentfile:documentfile:1.0.0
androidx.dynamicanimation:dynamicanimation:1.0.0
androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.4.1
androidx.graphics:graphics-path:1.0.1
androidx.interpolator:interpolator:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.8.7
androidx.lifecycle:lifecycle-common-jvm:2.8.7
androidx.lifecycle:lifecycle-common:2.8.7
androidx.lifecycle:lifecycle-livedata-core:2.8.7
androidx.lifecycle:lifecycle-process:2.8.7
androidx.lifecycle:lifecycle-runtime-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.7
androidx.lifecycle:lifecycle-runtime-compose:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.7
androidx.lifecycle:lifecycle-runtime-ktx:2.8.7
androidx.lifecycle:lifecycle-runtime:2.8.7
androidx.lifecycle:lifecycle-viewmodel-android:2.8.7
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7
androidx.lifecycle:lifecycle-viewmodel:2.8.7
androidx.legacy:legacy-support-core-utils:1.0.0
androidx.lifecycle:lifecycle-common-java8:2.9.4
androidx.lifecycle:lifecycle-common-jvm:2.9.4
androidx.lifecycle:lifecycle-common:2.9.4
androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4
androidx.lifecycle:lifecycle-livedata-core:2.9.4
androidx.lifecycle:lifecycle-livedata:2.9.4
androidx.lifecycle:lifecycle-process:2.9.4
androidx.lifecycle:lifecycle-runtime-android:2.9.4
androidx.lifecycle:lifecycle-runtime-compose-android:2.9.4
androidx.lifecycle:lifecycle-runtime-compose:2.9.4
androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.4
androidx.lifecycle:lifecycle-runtime-ktx:2.9.4
androidx.lifecycle:lifecycle-runtime:2.9.4
androidx.lifecycle:lifecycle-viewmodel-android:2.9.4
androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.4
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4
androidx.lifecycle:lifecycle-viewmodel:2.9.4
androidx.loader:loader:1.0.0
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
androidx.print:print:1.0.0
androidx.profileinstaller:profileinstaller:1.4.1
androidx.savedstate:savedstate-ktx:1.2.1
androidx.savedstate:savedstate:1.2.1
androidx.savedstate:savedstate-android:1.3.3
androidx.savedstate:savedstate-compose-android:1.3.3
androidx.savedstate:savedstate-compose:1.3.3
androidx.savedstate:savedstate-ktx:1.3.3
androidx.savedstate:savedstate:1.3.3
androidx.startup:startup-runtime:1.1.1
androidx.tracing:tracing:1.2.0
androidx.transition:transition:1.6.0
androidx.vectordrawable:vectordrawable-animated:1.1.0
androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.window.extensions.core:core:1.0.0
androidx.window:window-core-android:1.3.0
androidx.window:window-core:1.3.0
androidx.window:window:1.3.0
androidx.window:window-core-android:1.5.0
androidx.window:window-core:1.5.0
androidx.window:window:1.5.0
com.google.accompanist:accompanist-drawablepainter:0.37.3
com.google.guava:listenablefuture:1.0
com.squareup.okio:okio-jvm:3.11.0
@ -96,43 +113,49 @@ io.coil-kt.coil3:coil-compose-core-android:3.2.0
io.coil-kt.coil3:coil-compose-core:3.2.0
io.coil-kt.coil3:coil-core-android:3.2.0
io.coil-kt.coil3:coil-core:3.2.0
org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.4
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.4
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.4
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.4
org.jetbrains.androidx.window:window-core:1.3.1
org.jetbrains.compose.animation:animation-core:1.8.0
org.jetbrains.compose.animation:animation:1.8.0
org.jetbrains.compose.annotation-internal:annotation:1.8.0
org.jetbrains.compose.collection-internal:collection:1.8.0
org.jetbrains.compose.components:components-resources-android:1.8.0
org.jetbrains.compose.components:components-resources:1.8.0
org.jetbrains.compose.components:components-ui-tooling-preview-android:1.8.0
org.jetbrains.compose.components:components-ui-tooling-preview:1.8.0
org.jetbrains.compose.foundation:foundation-layout:1.8.0
org.jetbrains.compose.foundation:foundation:1.8.0
org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6
org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6
org.jetbrains.androidx.savedstate:savedstate:1.3.6
org.jetbrains.androidx.window:window-core:1.4.0
org.jetbrains.compose.animation:animation-core:1.10.0
org.jetbrains.compose.animation:animation:1.10.0
org.jetbrains.compose.annotation-internal:annotation:1.10.0
org.jetbrains.compose.collection-internal:collection:1.10.0
org.jetbrains.compose.components:components-resources-android:1.10.0
org.jetbrains.compose.components:components-resources:1.10.0
org.jetbrains.compose.components:components-ui-tooling-preview-android:1.10.0
org.jetbrains.compose.components:components-ui-tooling-preview:1.10.0
org.jetbrains.compose.foundation:foundation-layout:1.10.0
org.jetbrains.compose.foundation:foundation:1.10.0
org.jetbrains.compose.material3.adaptive:adaptive-layout:1.1.0
org.jetbrains.compose.material3.adaptive:adaptive:1.1.0
org.jetbrains.compose.material3:material3-adaptive-navigation-suite:1.8.0
org.jetbrains.compose.material3:material3:1.8.0
org.jetbrains.compose.material3.adaptive:adaptive:1.1.2
org.jetbrains.compose.material3:material3-adaptive-navigation-suite:1.9.0
org.jetbrains.compose.material3:material3:1.9.0
org.jetbrains.compose.material:material-icons-core:1.7.3
org.jetbrains.compose.material:material-icons-extended:1.7.3
org.jetbrains.compose.material:material-ripple:1.8.0
org.jetbrains.compose.material:material:1.8.0
org.jetbrains.compose.runtime:runtime-saveable:1.8.0
org.jetbrains.compose.runtime:runtime:1.8.0
org.jetbrains.compose.ui:ui-backhandler-android:1.8.0
org.jetbrains.compose.ui:ui-backhandler:1.8.0
org.jetbrains.compose.ui:ui-geometry:1.8.0
org.jetbrains.compose.ui:ui-graphics:1.8.0
org.jetbrains.compose.ui:ui-text:1.8.0
org.jetbrains.compose.ui:ui-unit:1.8.0
org.jetbrains.compose.ui:ui-util:1.8.0
org.jetbrains.compose.ui:ui:1.8.0
org.jetbrains.kotlin:kotlin-stdlib:2.1.20
org.jetbrains.compose.material:material-ripple:1.10.0
org.jetbrains.compose.material:material:1.10.0
org.jetbrains.compose.runtime:runtime-saveable:1.10.0
org.jetbrains.compose.runtime:runtime:1.10.0
org.jetbrains.compose.ui:ui-backhandler-android:1.9.1
org.jetbrains.compose.ui:ui-backhandler:1.9.1
org.jetbrains.compose.ui:ui-geometry:1.10.0
org.jetbrains.compose.ui:ui-graphics:1.10.0
org.jetbrains.compose.ui:ui-text:1.10.0
org.jetbrains.compose.ui:ui-unit:1.10.0
org.jetbrains.compose.ui:ui-util:1.10.0
org.jetbrains.compose.ui:ui:1.10.0
org.jetbrains.kotlin:kotlin-stdlib:2.3.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3
org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0

@ -1,6 +1,6 @@
package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
minSdkVersion:'21'
targetSdkVersion:'34'
package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' platformBuildVersionCode='36' compileSdkVersion='36' compileSdkVersionCodename='16'
minSdkVersion:'23'
targetSdkVersion:'36'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.POST_NOTIFICATIONS'

@ -42,6 +42,7 @@ dependencies {
compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
implementation(libs.truth)
lintChecks(libs.androidx.lint.gradle)
}
@ -99,5 +100,9 @@ gradlePlugin {
id = "nowinandroid.di.koin"
implementationClass = "KoinConventionPlugin"
}
register("root") {
id = "nowinandroid.root"
implementationClass = "RootConventionPlugin"
}
}
}

@ -1,17 +1,17 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.ApplicationExtension

@ -1,17 +1,17 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.LibraryExtension

@ -1,17 +1,17 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.ApplicationExtension

@ -1,22 +1,23 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.TestExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configureSpotlessForAndroid
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
@ -27,10 +28,11 @@ class AndroidTestConventionPlugin : Plugin<Project> {
with(target) {
apply(plugin = "com.android.test")
apply(plugin = "org.jetbrains.kotlin.android")
configureSpotlessForAndroid()
extensions.configure<TestExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 35
defaultConfig.targetSdk = 36
configureGradleManagedDevices(this)
}
}

@ -1,17 +1,17 @@
/*
* Copyright 2024 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.ApplicationExtension
@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.configureBadgingTasks
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.configureSpotlessForAndroid
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
@ -44,9 +45,10 @@ class CmpApplicationConventionPlugin : Plugin<Project> {
// apply("com.dropbox.dependency-guard")
}
configureComposeMultiplatformApp()
configureSpotlessForAndroid()
extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
defaultConfig.targetSdk = 36
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
configureGradleManagedDevices(this)
@ -68,7 +70,6 @@ class CmpApplicationConventionPlugin : Plugin<Project> {
"androidMainImplementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
"androidMainImplementation"(libs.findLibrary("androidx.tracing.ktx").get())
}
}
}
}

@ -1,17 +1,17 @@
/*
* Copyright 2024 The Android Open Source Project
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.gradle.LibraryExtension
@ -21,7 +21,6 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
// Convention plugin for the Compose Multiplatform feature module
class CmpFeatureConventionPlugin : Plugin<Project> {
@ -62,7 +61,6 @@ class CmpFeatureConventionPlugin : Plugin<Project> {
"androidInstrumentedTestImplementation"(libs.findLibrary("androidx.test.junit").get())
"androidInstrumentedTestImplementation"(libs.findLibrary("androidx.test.runner").get())
}
}
}
}

@ -1,20 +1,21 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.configureSpotlessForJvm
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -28,6 +29,7 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
apply(plugin = "nowinandroid.android.lint")
configureKotlinJvm()
configureSpotlessForJvm()
dependencies {
"testImplementation"(libs.findLibrary("kotlin.test").get())
}

@ -18,21 +18,23 @@ import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configureKotlinMultiplatform
import com.google.samples.apps.nowinandroid.configureSpotlessForAndroid
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class KmpLibraryConventionPlugin: Plugin<Project> {
class KmpLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
plugins.apply("com.android.library")
plugins.apply("org.jetbrains.kotlin.multiplatform")
configureKotlinMultiplatform()
configureSpotlessForAndroid()
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
defaultConfig.targetSdk = 36
configureGradleManagedDevices(this)
// The resource prefix is derived from the module name,
// so resources inside ":core:module1" must be prefixed with "core_module1_"

@ -21,7 +21,7 @@ import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
class KoinConventionPlugin: Plugin<Project> {
class KoinConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
@ -49,7 +49,16 @@ class KoinConventionPlugin: Plugin<Project> {
}
project.tasks.withType(KotlinCompilationTask::class.java).configureEach {
if(name != "kspCommonMainKotlinMetadata") {
if (name != "kspCommonMainKotlinMetadata") {
dependsOn("kspCommonMainKotlinMetadata")
}
}
// KSP2 uses KspAATask which isn't a KotlinCompilationTask but still
// needs to depend on kspCommonMainKotlinMetadata for the shared source set.
project.tasks.configureEach {
if (name.startsWith("ksp") && name != "kspCommonMainKotlinMetadata" &&
name.contains("Kotlin")
) {
dependsOn("kspCommonMainKotlinMetadata")
}
}

@ -19,7 +19,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class KotlinInjectConventionPlugin: Plugin<Project> {
class KotlinInjectConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
@ -42,4 +42,4 @@ class KotlinInjectConventionPlugin: Plugin<Project> {
}
}
}
}
}

@ -0,0 +1,27 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.google.samples.apps.nowinandroid.configureGraphTasks
import org.gradle.api.Plugin
import org.gradle.api.Project
class RootConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
configureGraphTasks()
}
}
}

@ -17,10 +17,10 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
class SqlDelightConventionPlugin: Plugin<Project> {
class SqlDelightConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("app.cash.sqldelight")
}
}
}
}

@ -1,17 +1,17 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
@ -30,6 +30,6 @@ import org.gradle.api.Project
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project,
) = beforeVariants {
it.androidTest.enable = it.androidTest.enable
&& project.projectDir.resolve("src/androidTest").exists()
it.androidTest.enable = it.androidTest.enable &&
project.projectDir.resolve("src/androidTest").exists()
}

@ -1,17 +1,17 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
@ -123,7 +123,6 @@ fun Project.configureBadgingTasks(
badging = project.layout.buildDirectory.file(
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
)
}
val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
@ -141,7 +140,6 @@ fun Project.configureBadgingTasks(
this.updateBadgingTaskName = updateBadgingTaskName
output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName")
}
}
}

@ -1,17 +1,17 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid

@ -0,0 +1,320 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import java.io.File
/**
* Module types for the KMP fork of Now in Android.
* Maps to convention plugin IDs.
*/
enum class PluginType(
val id: String,
val displayName: String,
val color: String,
) {
CmpApplication(
id = "nowinandroid.cmp.application",
displayName = "cmp-application",
color = "#CAFFBF",
),
CmpFeature(
id = "nowinandroid.cmp.feature",
displayName = "cmp-feature",
color = "#FFD6A5",
),
KmpLibrary(
id = "nowinandroid.kmp.library",
displayName = "kmp-library",
color = "#9BF6FF",
),
JvmLibrary(
id = "nowinandroid.jvm.library",
displayName = "jvm-library",
color = "#BDB2FF",
),
AndroidTest(
id = "nowinandroid.android.test",
displayName = "android-test",
color = "#A0C4FF",
),
}
/**
* Edge types representing different dependency configurations.
*/
enum class EdgeType(val mermaidStyle: String) {
Api("-->"),
Implementation("-.->"),
}
data class GraphEdge(
val from: String,
val to: String,
val type: EdgeType,
val label: String? = null,
)
private val supportedConfigurations = listOf(
"commonMainApi" to EdgeType.Api,
"commonMainImplementation" to EdgeType.Implementation,
"api" to EdgeType.Api,
"implementation" to EdgeType.Implementation,
"baselineProfile" to EdgeType.Implementation,
"testedApks" to EdgeType.Implementation,
)
private val labeledConfigurations = setOf("baselineProfile", "testedApks")
/**
* Detects the plugin type of a project based on which convention plugins are applied.
*/
fun Project.pluginType(): PluginType? {
return PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) }
}
/**
* Collects all project dependency edges for the given project.
*/
fun Project.collectEdges(): List<GraphEdge> {
val edges = mutableListOf<GraphEdge>()
for ((configName, edgeType) in supportedConfigurations) {
val config = configurations.findByName(configName) ?: continue
config.dependencies.filterIsInstance<ProjectDependency>().forEach { dep ->
val label = if (configName in labeledConfigurations) configName else null
val depPath = dep.path
edges.add(GraphEdge(path, depPath, edgeType, label))
}
}
return edges
}
/**
* Generates the Mermaid graph text for a given root project, showing all
* modules that are reachable from the specified project.
*/
fun generateMermaidGraph(
rootProjectPath: String,
allEdges: Map<String, List<GraphEdge>>,
allPluginTypes: Map<String, PluginType?>,
ignoredProjects: Set<String> = emptySet(),
): String {
// Find all reachable modules from the root
val reachable = mutableSetOf(rootProjectPath)
val queue = ArrayDeque<String>()
queue.add(rootProjectPath)
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
allEdges[current]?.forEach { edge ->
if (edge.to !in reachable && edge.to !in ignoredProjects) {
reachable.add(edge.to)
queue.add(edge.to)
}
}
}
val relevantEdges = allEdges.values.flatten()
.filter { it.from in reachable && it.to in reachable }
.filter { it.from !in ignoredProjects && it.to !in ignoredProjects }
.sortedWith(compareBy({ it.from }, { it.to }))
val modules = reachable.filter { it !in ignoredProjects }.sorted()
// Group modules by top-level parent for subgraph generation
val grouped = modules.groupBy { path ->
val parts = path.removePrefix(":").split(":")
if (parts.size > 1) ":${parts.first()}" else null
}
val sb = StringBuilder()
sb.appendLine("```mermaid")
sb.appendLine("---")
sb.appendLine("config:")
sb.appendLine(" layout: elk")
sb.appendLine(" elk:")
sb.appendLine(" nodePlacementStrategy: SIMPLE")
sb.appendLine("---")
sb.appendLine("graph TB")
// Render subgraphs for grouped modules
for ((group, members) in grouped.toSortedMap(nullsLast(compareBy { it }))) {
if (group != null && members.size > 1) {
sb.appendLine(" subgraph $group")
sb.appendLine(" direction TB")
for (member in members.sorted()) {
val shortName = member.split(":").last()
val pluginType = allPluginTypes[member]
val classDef = pluginType?.displayName ?: "unknown"
sb.appendLine(" $member[$shortName]:::$classDef")
}
sb.appendLine(" end")
}
}
// Render ungrouped modules (top-level modules)
for ((group, members) in grouped.toSortedMap(nullsLast(compareBy { it }))) {
if (group == null || members.size == 1) {
for (member in members.sorted()) {
val shortName = member.split(":").last()
val pluginType = allPluginTypes[member]
val classDef = pluginType?.displayName ?: "unknown"
sb.appendLine(" $member[$shortName]:::$classDef")
}
}
}
sb.appendLine()
// Render edges
for (edge in relevantEdges) {
val labelPart = if (edge.label != null) "|${edge.label}| " else ""
sb.appendLine(" ${edge.from} ${edge.type.mermaidStyle}$labelPart ${edge.to}")
}
sb.appendLine()
// Render classDef styles
val usedTypes = modules.mapNotNull { allPluginTypes[it] }.toSet()
for (type in PluginType.entries) {
sb.appendLine("classDef ${type.displayName} fill:${type.color},stroke:#000,stroke-width:2px,color:#000;")
}
sb.appendLine("classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;")
sb.appendLine("```")
return sb.toString()
}
/**
* Generates the legend section for the graph.
*/
fun generateLegend(): String {
val sb = StringBuilder()
sb.appendLine("<details><summary>Graph legend</summary>")
sb.appendLine()
sb.appendLine("```mermaid")
sb.appendLine("graph TB")
for (type in PluginType.entries) {
sb.appendLine(" ${type.displayName}[${type.displayName}]:::${type.displayName}")
}
sb.appendLine()
sb.appendLine(" cmp-application -.-> cmp-feature")
sb.appendLine(" kmp-library --> jvm-library")
sb.appendLine()
for (type in PluginType.entries) {
sb.appendLine("classDef ${type.displayName} fill:${type.color},stroke:#000,stroke-width:2px,color:#000;")
}
sb.appendLine("classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;")
sb.appendLine("```")
sb.appendLine()
sb.appendLine("</details>")
return sb.toString()
}
/**
* Updates a README.md file, replacing content between graph region markers.
*/
fun updateReadmeGraph(readmeFile: File, graphContent: String) {
if (!readmeFile.exists()) return
val content = readmeFile.readText()
val startMarker = "<!--region graph-->"
val endMarker = "<!--endregion-->"
val startIdx = content.indexOf(startMarker)
val endIdx = content.indexOf(endMarker)
if (startIdx == -1 || endIdx == -1) return
val newContent = buildString {
append(content.substring(0, startIdx + startMarker.length))
appendLine()
append(graphContent)
appendLine(generateLegend())
append(content.substring(endIdx))
}
readmeFile.writeText(newContent)
}
/**
* Registers the `graphDump` and `graphUpdate` tasks on the root project.
*/
fun Project.configureGraphTasks() {
val ignoredProjects = providers.gradleProperty("graph.ignoredProjects")
.orElse("")
.map { it.split(",").map(String::trim).filter(String::isNotEmpty).toSet() }
tasks.register("graphDump") {
group = "documentation"
description = "Dumps the module dependency graph as Mermaid text"
doLast {
val allEdges = mutableMapOf<String, List<GraphEdge>>()
val allPluginTypes = mutableMapOf<String, PluginType?>()
subprojects.forEach { sub ->
allEdges[sub.path] = sub.collectEdges()
allPluginTypes[sub.path] = sub.pluginType()
}
subprojects.forEach { sub ->
val readmeFile = sub.file("README.md")
if (readmeFile.exists() && readmeFile.readText().contains("<!--region graph-->")) {
val graph = generateMermaidGraph(
rootProjectPath = sub.path,
allEdges = allEdges,
allPluginTypes = allPluginTypes,
ignoredProjects = ignoredProjects.get(),
)
println("=== ${sub.path} ===")
println(graph)
}
}
}
}
tasks.register("graphUpdate") {
group = "documentation"
description = "Updates README.md files with module dependency graphs"
doLast {
val allEdges = mutableMapOf<String, List<GraphEdge>>()
val allPluginTypes = mutableMapOf<String, PluginType?>()
subprojects.forEach { sub ->
allEdges[sub.path] = sub.collectEdges()
allPluginTypes[sub.path] = sub.pluginType()
}
subprojects.forEach { sub ->
val readmeFile = sub.file("README.md")
if (readmeFile.exists() && readmeFile.readText().contains("<!--region graph-->")) {
val graph = generateMermaidGraph(
rootProjectPath = sub.path,
allEdges = allEdges,
allPluginTypes = allPluginTypes,
ignoredProjects = ignoredProjects.get(),
)
updateReadmeGraph(readmeFile, graph)
println("Updated: ${readmeFile.relativeTo(rootDir)}")
}
}
}
}
}

@ -76,7 +76,6 @@ internal fun Project.configureJacoco(
"create${variant.name.capitalize()}CombinedCoverageReport",
JacocoReport::class,
) {
classDirectories.setFrom(
allJars,
allDirectories.map { dirs ->
@ -97,7 +96,7 @@ internal fun Project.configureJacoco(
sourceDirectories.setFrom(
files(
variant.sources.java.toFilePaths(),
variant.sources.kotlin.toFilePaths()
variant.sources.kotlin.toFilePaths(),
),
)
@ -110,7 +109,6 @@ internal fun Project.configureJacoco(
)
}
variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
.use(reportTask)
.toGet(

@ -20,15 +20,13 @@ import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/**
@ -38,10 +36,10 @@ internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = 35
compileSdk = 36
defaultConfig {
minSdk = 21
minSdk = 23
}
compileOptions {
@ -53,6 +51,13 @@ internal fun Project.configureKotlinAndroid(
}
}
// Gradle 9 defaults failOnNoDiscoveredTests to true. KMP modules may declare
// commonTest dependencies without having Android-specific test files, so disable
// the check to avoid false failures.
tasks.withType<Test>().configureEach {
failOnNoDiscoveredTests = false
}
configureKotlin()
dependencies {
@ -71,6 +76,10 @@ internal fun Project.configureKotlinJvm() {
targetCompatibility = JavaVersion.VERSION_11
}
tasks.withType<Test>().configureEach {
failOnNoDiscoveredTests = false
}
configureKotlin()
}

@ -21,12 +21,8 @@ import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.konan.target.HostManager
/**
* A plugin that applies the Kotlin Multiplatform plugin and configures it for the project.

@ -1,17 +1,17 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid

@ -1,3 +1,19 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.ApplicationExtension
@ -7,7 +23,7 @@ import com.android.build.api.dsl.ProductFlavor
@Suppress("EnumEntryName")
enum class FlavorDimension {
contentType
contentType,
}
// The content for the app can either come from local static data which is useful for demo
@ -24,12 +40,12 @@ fun configureFlavors(
flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {},
) {
commonExtension.apply {
FlavorDimension.values().forEach { flavorDimension ->
FlavorDimension.entries.forEach { flavorDimension ->
flavorDimensions += flavorDimension.name
}
productFlavors {
NiaFlavor.values().forEach { niaFlavor ->
NiaFlavor.entries.forEach { niaFlavor ->
register(niaFlavor.name) {
dimension = niaFlavor.dimension.name
flavorConfigurationBlock(this, niaFlavor)

@ -1,17 +1,17 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
@ -49,7 +49,9 @@ internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtensio
javaSources.zip(kotlinSources) { javaDirs, kotlinDirs ->
javaDirs + kotlinDirs
}
} else javaSources ?: kotlinSources
} else {
javaSources ?: kotlinSources
}
if (artifact != null && testSources != null) {
tasks.register(
@ -96,8 +98,9 @@ internal abstract class PrintApkLocationTask : DefaultTask() {
val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
?: throw RuntimeException("Cannot load APKs")
if (builtArtifacts.elements.size != 1)
if (builtArtifacts.elements.size != 1) {
throw RuntimeException("Expected one APK !")
}
val apk = File(builtArtifacts.elements.single().outputFile).toPath()
println(apk)
}

@ -1,17 +1,17 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid

@ -0,0 +1,64 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
/**
* Configures Spotless for Android/KMP modules (Kotlin + KTS + XML).
*
* Uses `target("src/ **")` instead of `target("** /") + targetExclude("build/")` to work around
* [spotless#2717](https://github.com/diffplug/spotless/issues/2717).
*/
fun Project.configureSpotlessForAndroid() {
configureSpotlessCommon()
val rootDir = isolated.rootProject.projectDirectory
extensions.configure<SpotlessExtension> {
format("xml") {
target("src/**/*.xml")
licenseHeaderFile(rootDir.file("spotless/copyright.xml").asFile, "(<[^!?])")
}
}
}
/**
* Configures Spotless for JVM-only modules (Kotlin + KTS, no XML).
*/
fun Project.configureSpotlessForJvm() {
configureSpotlessCommon()
}
private fun Project.configureSpotlessCommon() {
apply(plugin = "com.diffplug.spotless")
val rootDir = isolated.rootProject.projectDirectory
extensions.configure<SpotlessExtension> {
kotlin {
target("src/**/*.kt")
ktlint(libs.findVersion("ktlint").get().toString())
.editorConfigOverride(mapOf("android" to "true"))
licenseHeaderFile(rootDir.file("spotless/copyright.kt").asFile)
}
format("kts") {
target("*.kts")
targetExclude("**/build/**/*.kts")
licenseHeaderFile(rootDir.file("spotless/copyright.kts").asFile, "(^(?![\\/ ]\\*).*$)")
}
}
}

@ -54,9 +54,25 @@ plugins {
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.secrets) apply false
alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation
alias(libs.plugins.nowinandroid.root)
alias(libs.plugins.spotless)
alias(libs.plugins.jetbrains.compose) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.sqldelight.gradle.plugin) apply false
alias(libs.plugins.ktrofit) apply false
alias(libs.plugins.buildkonfig) apply false
}
spotless {
kotlin {
target("build-logic/convention/src/**/*.kt")
ktlint(libs.versions.ktlint.get())
.editorConfigOverride(mapOf("android" to "true"))
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("*.kts", "build-logic/**/*.kts")
targetExclude("**/build/**/*.kts")
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
}

@ -23,8 +23,6 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.NetworkRequest.Builder
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
@ -79,13 +77,8 @@ internal class ConnectivityManagerNetworkMonitor(
.flowOn(ioDispatcher)
.conflate()
@Suppress("DEPRECATION")
private fun ConnectivityManager.isCurrentlyConnected() = when {
VERSION.SDK_INT >= VERSION_CODES.M ->
activeNetwork
?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
else -> activeNetworkInfo?.isConnected
} ?: false
private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
val networkCapabilities = getNetworkCapabilities(activeNetwork) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}

@ -27,6 +27,10 @@ plugins {
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}
ktorfit {
compilerPluginVersion.set("2.3.3")
}
android {
namespace = "com.google.samples.apps.nowinandroid.core.network"
testOptions {

@ -1,59 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
val ktlintVersion = "1.5.0"
initscript {
val spotlessVersion = "7.0.2"
repositories {
mavenCentral()
}
dependencies {
classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
}
}
rootProject {
subprojects {
apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
kotlin {
target("**/*.kt")
targetExclude("**/build/**/*.kt")
ktlint(ktlintVersion).editorConfigOverride(
mapOf(
"android" to "true",
),
)
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("**/*.kts")
targetExclude("**/build/**/*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/**/*.xml")
// Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
}
}
}
}

@ -29,17 +29,19 @@ androidxWork = "2.10.0"
coil = "3.2.0"
dependencyGuard = "0.5.0"
jacoco = "0.8.12"
kotlin = "2.1.20"
kotlin = "2.3.0"
kotlinxCoroutines = "1.10.2"
kotlinxDatetime = "0.6.1"
kotlinxSerializationJson = "1.8.0"
ksp = "2.1.20-1.0.32"
ksp = "2.3.0"
logback = "1.5.15"
material3adaptive = "1.1.0"
moduleGraph = "2.7.1"
ktlint = "1.5.0"
robolectric = "4.16"
roborazzi = "1.51.0"
roborazzi = "1.56.0"
secrets = "2.0.1"
spotless = "8.2.1"
truth = "1.4.4"
turbine = "1.2.0"
androidx-constraintlayout = "2.2.0"
@ -47,7 +49,7 @@ androidx-espresso-core = "3.6.1"
androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
compose-ui-tooling = "1.8.1"
compose-plugin = "1.8.0"
compose-plugin = "1.10.0"
sqldelight = "2.0.2"
kotlinInject = "0.8.0"
multiplatform-settings = "1.3.0"
@ -55,7 +57,7 @@ kermit = "2.0.5"
koin = "4.1.0"
koin-annotations = "2.0.0"
ktor = "3.1.2"
ktrofit = "2.5.1"
ktrofit = "2.7.2"
buildKonfig = "0.15.2"
lifecycle-viewmodel-compose = "2.9.0"
navigation-compose = "2.9.0-beta02"
@ -185,6 +187,7 @@ android-tools-common = { group = "com.android.tools", name = "common", version.r
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
spotless-gradlePlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@ -200,6 +203,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@ -218,3 +222,4 @@ nowinandroid-sqldelight = { id = "nowinandroid.sqldelight" }
nowinandroid-cmp-feature = { id = "nowinandroid.cmp.feature" }
nowinandroid-cmp-application = { id = "nowinandroid.cmp.application" }
nowinandroid-di-koin = { id = "nowinandroid.di.koin" }
nowinandroid-root = { id = "nowinandroid.root" }

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

@ -88,7 +88,7 @@ done
if [[ -n "$run_checks" ]]; then
# pre-push usually executes in the repository root, but just to be safe...
cd "$(git rev-parse --show-toplevel)"
./gradlew --init-script gradle/init.gradle.kts --no-configuration-cache check
./gradlew check
exit $?
fi

Loading…
Cancel
Save