Initial commit

This commit is contained in:
2025-06-12 23:20:21 +02:00
parent 1656e706a0
commit 714cdb6795
122 changed files with 3335 additions and 916 deletions

View File

@ -0,0 +1,29 @@
package dev.adriankuta.flights
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
internal fun Project.configureAndroidLint(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
lint {
baseline = file("lint-baseline.xml")
warningsAsErrors = true
disable += "AndroidGradlePluginVersion"
warning += "LintBaseline" // Still have report remind us of baseline issues
disable += "GradleDependency" // We want to mange dependency updates in own PRs.
disable += "MissingTranslation" // Translations are apart from our normal process at the moment
// region - Bug in lint/AGP apparently
// TODO Maybe it is, maybe it isnt. Its a transitory, hard to replicate bug. Ideally
// we want these lint rules applied.
disable += "StateFlowValueCalledInComposition"
disable += "FlowOperatorInvokedInComposition"
disable += "WrongNavigateRouteType"
disable += "WrongStartDestinationType"
disable += "UnknownIssueId"
// endregion
}
}
}

View File

@ -0,0 +1,50 @@
package dev.adriankuta.flights
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
internal fun Project.configureCompose(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
with(pluginManager) {
apply("org.jetbrains.kotlin.plugin.compose")
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
commonExtension.apply {
buildFeatures {
compose = true
}
}
extensions.configure<ComposeCompilerGradlePluginExtension> {
enableStrongSkippingMode = true
includeSourceInformation = true
}
dependencies {
val bom = libs.findLibrary("androidx-compose-bom").get()
"androidTestImplementation"(platform(bom))
"androidTestImplementation"(libs.findLibrary("androidx.compose.ui.test.junit").get())
"debugImplementation"(platform(bom))
"debugImplementation"(libs.findLibrary("androidx.compose.ui.tooling").get())
//"debugImplementation"(libs.findLibrary("androidx.compose.ui.testManifest").get())
"implementation"(platform(bom))
"implementation"(libs.findLibrary("androidx.compose.material3").get())
"implementation"(libs.findLibrary("androidx.compose.ui").get())
"implementation"(libs.findLibrary("androidx.compose.ui.graphics").get())
"implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get())
"implementation"(libs.findLibrary("androidx.lifecycle.runtime.compose").get())
"implementation"(libs.findLibrary("androidx.navigation.compose").get())
"implementation"(libs.findLibrary("kotlinx.collections.immutable").get())
}
}

View File

@ -0,0 +1,83 @@
package dev.adriankuta.flights
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import java.io.FileWriter
internal fun Project.configureDetektDependencies() {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"detektPlugins"(libs.findLibrary("detekt.ktlint").get())
"detektPlugins"(libs.findLibrary("detekt.compose").get())
}
}
internal fun Project.configureDetektForNonUiModule() =
createDetektConfigFile(NonDefault.trimIndent())
internal fun Project.configureDetektForComposeModuleExceptions() =
createDetektConfigFile(ComposeExceptions.trimIndent())
internal fun Project.createDetektConfigFile(deviationsFromDefaultConfig: String) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
if (!file(DetektConfigPath).exists()) {
val detektConfigFile = project.file(DetektConfigPath)
detektConfigFile.parentFile.mkdirs()
detektConfigFile.createNewFile()
val writer = FileWriter(detektConfigFile)
writer.write(deviationsFromDefaultConfig)
writer.close()
}
configure<DetektExtension> {
toolVersion = libs.findVersion("detekt").get().toString()
config.setFrom(files(DetektConfigPath))
buildUponDefaultConfig = true
}
}
private const val DetektConfigPath = "config/detekt/detekt.yml"
private val NonDefault = """
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true
"""
private val ComposeExceptions = """
# Exceptions for compose. See https://detekt.dev/docs/introduction/compose
naming:
FunctionNaming:
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'
TopLevelPropertyNaming:
constantPattern: '[A-Z][A-Za-z0-9]*'
complexity:
LongParameterList:
ignoreAnnotated: ['Composable']
TooManyFunctions:
ignoreAnnotatedFunctions: ['Preview']
style:
MagicNumber:
ignorePropertyDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotated: ['Composable']
UnusedPrivateMember:
ignoreAnnotated: ['Composable']
""" + NonDefault

View File

@ -0,0 +1,43 @@
package dev.adriankuta.flights
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.ApplicationProductFlavor
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
@Suppress("EnumEntryName")
enum class FlavorDimension {
cost
}
// The content for the app can either come from local static data which is useful for demo
// purposes, or from a production backend server which supplies up-to-date, real content.
// These two product flavors reflect this behaviour.
@Suppress("EnumEntryName")
enum class FlightsFlavor(
val dimension: FlavorDimension,
val applicationIdSuffix: String? = null,
) {
free(FlavorDimension.cost),
paid(FlavorDimension.cost, applicationIdSuffix = ".paid")
}
fun Project.configureFlavors(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
flavorDimensions += FlavorDimension.cost.name
productFlavors {
FlightsFlavor.values().forEach {
create(it.name) {
dimension = it.dimension.name
if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) {
if (it.applicationIdSuffix != null) {
applicationIdSuffix = it.applicationIdSuffix
}
}
}
}
}
}
}

View File

@ -0,0 +1,48 @@
package dev.adriankuta.flights
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.ManagedVirtualDevice
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
internal fun Project.configureGradleManagedDevices(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
testOptions {
managedDevices {
devices.register("pixel2api30", ManagedVirtualDevice::class.java) {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp"
}
// Get rid of devices.register block and uncomment the following once we can update our AGP version.
// localDevices.create("pixel2api30") {
// // Use device profiles you typically see in Android Studio.
// device = "Pixel 2"
// // Use only API levels 27 and higher.
// apiLevel = 30
// // To include Google services, use "google".
// systemImageSource = "aosp"
// }
groups.create("default") {
targetDevices.add(devices.getByName("pixel2api30"))
}
}
}
}
dependencies {
// Something has gone awry in 8.3.0-rc01. I robbed the following from here:
// https://sourceforge.net/projects/guava.mirror/files/v32.1.0/
// If we are not on 8.3.0-rc01 anymore then remove this and run a test with gradle managed
// device (ie ./gradlew pixel2api30DevDebugAndroidTest). If you dont see a dependancy problem
// with guava:listenablefuture then we are good to remove it.
modules {
module("com.google.guava:listenablefuture") {
replacedBy("com.google.guava:guava", "listenablefuture is part of guava")
}
}
}
}

View File

@ -0,0 +1,23 @@
package dev.adriankuta.flights
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
internal fun Project.configureHilt() {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.compiler").get())
// For instrumentation tests
"androidTestImplementation"(libs.findLibrary("hilt.testing").get())
"kspAndroidTest"(libs.findLibrary("hilt.compiler").get())
// For local unit tests hilt-android-testing
"testImplementation"(libs.findLibrary("hilt.testing").get())
"kspTest"(libs.findLibrary("hilt.compiler").get())
}
}

View File

@ -0,0 +1,18 @@
package dev.adriankuta.flights
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
internal fun Project.configureInstrumentation() {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"androidTestImplementation"(libs.findLibrary("truth").get())
"androidTestImplementation"(libs.findLibrary("mockk.android").get())
"androidTestImplementation"(libs.findLibrary("androidx.test.rules").get())
"androidTestImplementation"(libs.findLibrary("androidx.test.uiautomator").get())
//"androidTestImplementation"(libs.findLibrary("turbine").get())
}
}

View File

@ -0,0 +1,148 @@
package dev.adriankuta.flights
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
import java.util.Locale
// Mostly robbed off: https://github.com/android/nowinandroid/blob/main/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt
// ./gradlew jacocoTestReport will run lint, all the tests and generate the test coverage.
internal fun Project.configureJacoco(
androidComponentsExtension: AndroidComponentsExtension<*, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
configure<JacocoPluginExtension> {
toolVersion = libs.findVersion("jacoco").get().toString()
}
val jacocoTestReport = tasks.create("jacocoTestReport") {
group = "Verification"
description = "Create test coverage reports"
}
androidComponentsExtension.onVariants { variant ->
if (variant.name.contains("DevDebug", ignoreCase = true)) {
val unitTestTaskName = "test${variant.name.capitalize()}UnitTest"
// val instrumentedTaskName = "defaultGroup${variant.name.capitalize()}AndroidTest" // This does dick on AGP version ancient (Weavr)
val instrumentedTaskName = "connected${variant.name.capitalize()}AndroidTest"
val lintTaskName = "lint${variant.name.capitalize()}"
val detektTaskName = "detekt${variant.name.capitalize()}"
val buildDir = layout.buildDirectory.get().asFile
val reportTask =
tasks.register(
"jacoco${unitTestTaskName.capitalize()}Report",
JacocoReport::class,
) {
dependsOn(unitTestTaskName)
// Dont bother with teh instrumented test if there isnt any.
if (file("${projectDir}/src/androidTest").walk().any { it.isFile }) {
dependsOn(instrumentedTaskName)
}
dependsOn(lintTaskName)
dependsOn(detektTaskName)
reports {
xml.required.set(true)
html.required.set(true)
}
classDirectories.setFrom(
fileTree("$buildDir/tmp/kotlin-classes/${variant.name}") {
exclude(coverageExclusions)
},
)
sourceDirectories.setFrom(
files(
"$projectDir/src/main/java",
"$projectDir/src/main/kotlin",
),
)
executionData.setFrom(
fileTree("$buildDir") {
include("outputs/unit_test_code_coverage/devDebugUnitTest/*.exec")
include("outputs/managed_device_code_coverage/debug/flavors/dev/**/*.ec")
},
)
}
jacocoTestReport.dependsOn(reportTask)
}
}
tasks.withType<Test>().configureEach {
configure<JacocoTaskExtension> {
// Required for JaCoCo + Robolectric
// https://github.com/robolectric/robolectric/issues/2230
isIncludeNoLocationClasses = true
// Required for JDK 11 with the above
// https://github.com/gradle/gradle/issues/5184#issuecomment-391982009
excludes = listOf("jdk.internal.*")
}
}
}
private val coverageExclusions = listOf(
// data binding
"android/databinding/**/*.class",
"**/android/databinding/*Binding.class",
"**/android/databinding/*",
"**/androidx/databinding/*",
"**/BR.*",
// android
"**/R.class",
"**/R$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
"android/**/*.*",
// butterKnife
"**/*\$ViewInjector*.*",
"**/*\$ViewBinder*.*",
// dagger
"**/*_MembersInjector.class",
"**/Dagger*Component.class",
"**/Dagger*Component\$Builder.class",
"**/*Module_*Factory.class",
"**/di/module/*",
"**/*_Factory*.*",
"**/*Module*.*",
"**/*Dagger*.*",
"**/*Hilt*.*",
// kotlin
"**/*MapperImpl*.*",
"**/*\$ViewInjector*.*",
"**/*\$ViewBinder*.*",
"**/BuildConfig.*",
"**/*Component*.*",
"**/*BR*.*",
"**/Manifest*.*",
"**/*\$Lambda$*.*",
"**/*Companion*.*",
"**/*Module*.*",
"**/*Dagger*.*",
"**/*Hilt*.*",
"**/*MembersInjector*.*",
"**/*_MembersInjector.class",
"**/*_Factory*.*",
"**/*_Provide*Factory*.*",
"**/*Extensions*.*",
"**/*JsonAdapter.*",
)
private fun String.capitalize() = replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}

View File

@ -0,0 +1,68 @@
package dev.adriankuta.flights
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.provideDelegate
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
commonExtension.apply {
compileSdk = libs.findVersion("compileSdk").get().toString().toInt()
defaultConfig {
minSdk = libs.findVersion("minSdk").get().toString().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
packaging {
resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}")
resources.excludes.add("/META-INF/LICENSE.md")
resources.excludes.add("/META-INF/LICENSE-notice.md")
resources.excludes.add("/META-INF/INDEX.LIST")
}
}
with(extensions.getByType<KotlinAndroidProjectExtension>()) {
compilerOptions {
// Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project
allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs.addAll(
listOf(
"-opt-in=kotlin.RequiresOptIn",
"-Xwhen-guards",
// Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
),
)
jvmTarget.set(JvmTarget.JVM_11)
}
}
dependencies {
"implementation"(libs.findLibrary("androidx.core.ktx").get())
"implementation"(libs.findLibrary("kotlinx.coroutines.android").get())
"implementation"(libs.findLibrary("timber").get())
"coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
}
}

View File

@ -0,0 +1,57 @@
package dev.adriankuta.flights
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
internal fun Project.configureLibrary() {
with(pluginManager) {
apply("com.android.library")
apply("io.gitlab.arturbosch.detekt")
apply("org.jetbrains.kotlin.android")
apply("org.gradle.jacoco")
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
val targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
// android block
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = targetSdk
defaultConfig.consumerProguardFiles("consumer-rules.pro")
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
debug {
testCoverage {
enableUnitTestCoverage =
false // Test coverage spanks memory on the CI with ye older AGP
enableAndroidTestCoverage = false
}
}
}
configureFlavors(this)
configureAndroidLint(this)
// configureGradleManagedDevices(this)
}
// val extension = extensions.getByType<LibraryAndroidComponentsExtension>()
// configureJacoco(extension)
configureDetektDependencies()
//configureSonar()
configureUnitTests()
extensions.configure<KotlinAndroidProjectExtension> {
//explicitApi()
}
}

View File

@ -0,0 +1,33 @@
package dev.adriankuta.flights
import org.gradle.api.Project
// If have got Sonar running locally you can try it by setting up a project and then running this
// script:
//
// #!/bin/bash -e
//
//./gradlew clean
//./gradlew jacocoTestReport
//./gradlew sonar \
// -Dsonar.projectKey=YourProjectKey \
// -Dsonar.projectName='YourProjectName' \
// -Dsonar.host.url=http://localhost:9000 \
// -Dsonar.token=sqp_sonarqubelkeywhateveritis
//
internal fun Project.configureSonar() {
// configure<SonarExtension> {
// properties {
// val buildDir = layout.buildDirectory.get().toString()
// property("sonar.androidLint.reportPaths", "$buildDir/reports/lint-results-devDebug.xml")
// property("sonar.core.codeCoveragePlugin", "jacoco")
// property("sonar.coverage.jacoco.xmlReportPaths", "$buildDir/reports/jacoco/**/*.xml")
// property("sonar.gradle.skipCompile", true)
// property("sonar.kotlin.detekt.reportPaths", "$buildDir/reports/detekt/devDebug.xml")
// property("sonar.verbose", true)
//
// property("sonar.junit.reportPaths",
// "$buildDir/test-results/testDevDebugUnitTest,$buildDir/outputs/androidTest-results/managedDevice/debug/flavors/dev/pixel2api30")
// }
// }
}

View File

@ -0,0 +1,18 @@
package dev.adriankuta.flights
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
internal fun Project.configureUnitTests() {
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
"testImplementation"(libs.findLibrary("junit4").get())
"testImplementation"(libs.findLibrary("androidx.test.core").get())
"testImplementation"(libs.findLibrary("kotlinx.coroutines.test").get())
"testImplementation"(libs.findLibrary("truth").get())
"testImplementation"(libs.findLibrary("mockk.android").get())
//"testImplementation"(libs.findLibrary("turbine").get())
}
}