diff --git a/.editorconfig b/.editorconfig index a81f2b3..b53e621 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,9 @@ +[**/build/generated/**] +ktlint = disabled + +[**/_template**] +ktlint = disabled + [*.{kt,kts}] max_line_length = 140 @@ -11,6 +17,8 @@ ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true continuation_indent_size = 4 + +ktlint_code_style = android_studio ktlint_standard_import-ordering = disabled ktlint_standard_function-signature = disabled ktlint_standard_context-receiver-wrapping = disabled @@ -19,5 +27,4 @@ ktlint_standard_no-empty-first-line-in-class-body = disabled ktlint_standard_no-blank-line-in-list = disabled ktlint_standard_blank-line-before-declaration = disabled ktlint_standard_annotation = disabled - - +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/.github/workflows/enterprise.yml b/.github/workflows/enterprise.yml index e482231..cc791f4 100644 --- a/.github/workflows/enterprise.yml +++ b/.github/workflows/enterprise.yml @@ -25,4 +25,6 @@ jobs: SIGNING_KEYSTORE_PASSWORD: android SIGNING_KEY_ALIAS: androiddebugkey SIGNING_KEY_PASSWORD: android - APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} \ No newline at end of file + APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} + # TODO Set up `GRADLE_CACHE_ENCRYPTION_KEY` for this GitHub repository + GRADLE_CACHE_ENCRYPTION_KEY: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5d94cb8..9ef7eea 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,3 +12,6 @@ jobs: with: LINT_GRADLE_TASKS: lintCheck TEST_GRADLE_TASKS: testDevEnterpriseUnitTest + secrets: + # TODO Set up `GRADLE_CACHE_ENCRYPTION_KEY` for this GitHub repository + GRADLE_CACHE_ENCRYPTION_KEY: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 022de89..aa27767 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -50,12 +52,8 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions.apply { - jvmTarget = JavaVersion.VERSION_17.toString() + sourceCompatibility = ProjectSettings.JavaCompatibility + targetCompatibility = ProjectSettings.JavaCompatibility } sourceSets { @@ -132,10 +130,11 @@ android { } kotlin { - jvmToolchain(JavaVersion.VERSION_17.majorVersion.toInt()) + jvmToolchain(ProjectSettings.JvmToolchainVersion) compilerOptions { optIn.add("kotlin.RequiresOptIn") + jvmTarget = JvmTarget.fromTarget(ProjectSettings.KotlinJvmTargetNum) } } @@ -181,12 +180,12 @@ dependencies { implementation(libs.okHttp) implementation(libs.logging) implementation(libs.retrofit) + implementation(libs.retrofit.converter) implementation(libs.coil) implementation(libs.coil.network) // Serialization implementation(libs.serialization.json) - implementation(libs.serialization.converter) // Other implementation(libs.timber) diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt index 0b78fb0..e6704f5 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt @@ -18,10 +18,7 @@ import javax.inject.Singleton * Uses [kotlinx.serialization] to serialize and deserialize objects into Strings. */ @Singleton -class JsonPersistence @Inject constructor( - val dataStore: DataStore, - val json: Json, -) { +class JsonPersistence @Inject constructor(val dataStore: DataStore, val json: Json) { /** * Saves provided [value] in persistence under provided [key]. diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt index b6822eb..57c2bd7 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt @@ -13,9 +13,7 @@ import javax.inject.Singleton * [DataStore]-backed Persistence which allows storage and observing of persisted entities. */ @Singleton -class PrimitivePersistence @Inject constructor( - private val dataStore: DataStore, -) { +class PrimitivePersistence @Inject constructor(private val dataStore: DataStore) { suspend fun get(key: Preferences.Key): T? = dataStore.data.firstOrNull()?.run { this[key] diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/remote/ApiService.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/remote/ApiService.kt index dd36fd4..f273e90 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/remote/ApiService.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/remote/ApiService.kt @@ -11,8 +11,5 @@ interface ApiService { suspend fun user(): SampleApiModel @Serializable - data class SampleApiModel( - val id: String, - @Contextual val dateTime: ZonedDateTime, - ) + data class SampleApiModel(val id: String, @Contextual val dateTime: ZonedDateTime) } diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt index 982d3fd..f705c78 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.modules.SerializersModule @InstallIn(SingletonComponent::class) class ApplicationModule { private val Context.dataStore: DataStore by preferencesDataStore( - name = Constants.DataStore.DEFAULT_DATASTORE_NAME + name = Constants.DataStore.DEFAULT_DATASTORE_NAME, ) @Provides diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/NetworkModule.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/NetworkModule.kt index 513fd46..79f7df0 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/NetworkModule.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/NetworkModule.kt @@ -4,7 +4,6 @@ import app.futured.androidprojecttemplate.BuildConfig import app.futured.androidprojecttemplate.data.remote.ApiService import app.futured.androidprojecttemplate.tools.Constants.Api.BASE_PROD_URL import app.futured.androidprojecttemplate.tools.Constants.Api.TIMEOUT_IN_SECONDS -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,6 +14,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Singleton diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/navigation/Destinations.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/navigation/Destinations.kt index 4bd04bf..0413f1d 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/navigation/Destinations.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/navigation/Destinations.kt @@ -38,20 +38,19 @@ sealed class Destination( data object Detail : Destination( route = "detail/{title}?subtitle={subtitle}?value={value}", destinationScreen = { DetailScreen(navigation = it) }, - arguments = - listOf( - navArgument("title") { - type = NavType.StringType - }, - navArgument("subtitle") { - type = NavType.StringType - defaultValue = "Default subtitle" - }, - navArgument("value") { - type = NavType.StringType - nullable = true - }, - ), + arguments = listOf( + navArgument("title") { + type = NavType.StringType + }, + navArgument("subtitle") { + type = NavType.StringType + defaultValue = "Default subtitle" + }, + navArgument("value") { + type = NavType.StringType + nullable = true + }, + ), ) { fun buildRoute(title: String, subtitle: String?, value: String?): String = route .withArgument("title", title) diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt index 399a963..4eeadc1 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt @@ -14,7 +14,9 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -abstract class BaseViewModel : ViewModel(), CoroutineScopeOwner { +abstract class BaseViewModel : + ViewModel(), + CoroutineScopeOwner { abstract val viewState: VS override val coroutineScope: CoroutineScope = viewModelScope diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/extensions/GeneralExtensions.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/extensions/GeneralExtensions.kt index 2539ce6..b4f3d7e 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/extensions/GeneralExtensions.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/extensions/GeneralExtensions.kt @@ -10,13 +10,11 @@ inline fun withNonNullValue(receiver: T?, block: (T) -> Unit) if (receiver != null) block(receiver) } -inline fun safe(block: () -> T): T? { - return try { - block() - } catch (e: Exception) { - e.printStackTrace() - null - } +inline fun safe(block: () -> T): T? = try { + block() +} catch (e: Exception) { + e.printStackTrace() + null } fun T?.orThrow(): T = this ?: error("UnexpectedError") // Dev error diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/_templateScreen/_TEMPLATEScreen.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/_templateScreen/_TEMPLATEScreen.kt index e670198..9a946f6 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/_templateScreen/_TEMPLATEScreen.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/_templateScreen/_TEMPLATEScreen.kt @@ -1,5 +1,4 @@ @file:OptIn(ExperimentalMaterial3Api::class) -@file:Suppress("PackageNaming") package app.futured.androidprojecttemplate.ui.screens._templateScreen @@ -111,9 +110,9 @@ sealed class TEMPLATEEvent : Event() data object NavigateBackEvent : TEMPLATEEvent() @HiltViewModel -class TEMPLATEViewModel @Inject constructor( - override val viewState: TEMPLATEViewState, -) : BaseViewModel(), TEMPLATE.Actions { +class TEMPLATEViewModel @Inject constructor(override val viewState: TEMPLATEViewState) : + BaseViewModel(), + TEMPLATE.Actions { override fun onNavigateBack() { sendEvent(NavigateBackEvent) } diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailViewModel.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailViewModel.kt index 15aa24c..cf62112 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailViewModel.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailViewModel.kt @@ -5,9 +5,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class DetailViewModel @Inject constructor( - override val viewState: DetailViewState, -) : BaseViewModel(), Detail.Actions { +class DetailViewModel @Inject constructor(override val viewState: DetailViewState) : + BaseViewModel(), + Detail.Actions { override fun onNavigateBack() { sendEvent(NavigateBackEvent) } diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeViewModel.kt index 4dea20e..2f6ad21 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeViewModel.kt @@ -5,9 +5,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class HomeViewModel @Inject constructor( - override val viewState: HomeViewState, -) : BaseViewModel(), Home.Actions { +class HomeViewModel @Inject constructor(override val viewState: HomeViewState) : + BaseViewModel(), + Home.Actions { override fun onIncrementCounter() { viewState.counter++ } diff --git a/buildSrc/src/main/kotlin/ProjectSettings.kt b/buildSrc/src/main/kotlin/ProjectSettings.kt index 2c88bf4..85541b6 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -1,3 +1,5 @@ +import org.gradle.api.JavaVersion + object ProjectSettings { const val applicationId = "app.futured.androidprojecttemplate" const val compileSdkVersion = 35 @@ -6,6 +8,10 @@ object ProjectSettings { val versionName = System.getenv("ANDROID_VERSION_NAME") ?: "1.0.0" val versionCode = System.getenv("ANDROID_BUILD_NUMBER")?.toIntOrNull() ?: 1 + val JavaCompatibility = JavaVersion.VERSION_17 + val KotlinJvmTargetNum = JavaCompatibility.majorVersion + val JvmToolchainVersion = KotlinJvmTargetNum.toInt() + object Gradle { const val TaskGroup = "futured" } diff --git a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt index e0056aa..39c5a4b 100644 --- a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt +++ b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt @@ -25,6 +25,12 @@ open class LintCheck : DefaultTask() { .forEach { dependsOn("${it.path}:detekt") } + + project.subprojects + .filter { it.plugins.hasPlugin("com.android.library") || it.plugins.hasPlugin("com.android.application") } + .forEach { + dependsOn("${it.path}:lintDevRelease") + } } } } diff --git a/gradle.properties b/gradle.properties index 194e247..550b14a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,14 @@ -org.gradle.jvmargs=-Xmx4g +#Gradle +org.gradle.jvmargs=-Xmx6g -Xms256m -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g" +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true +org.gradle.configuration-cache=true +# Use this flag carefully, in case some of the plugins are not fully compatible. +org.gradle.configuration-cache.problems=warn -android.useAndroidX=true - -kapt.incremental.apt=true +#Kotlin +kotlin.code.style=official -kapt.include.compile.classpath=false +#Android +android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09dd4c4..747a7b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,42 +1,37 @@ [versions] -# https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility -agp = "8.8.1" +agp = "8.11.0" gradleVersionsPlugin = "0.50.0" -kotlin = "2.1.0" -ksp = "2.1.0-1.0.29" # Must be compatible with: `kotlin` -androidxComposeBom = "2024.12.01" -androidxActivity = "1.7.2" -androidxSplashScreen = "1.2.0-alpha01" -hilt = "2.55" +kotlin = "2.2.0" +ksp = "2.2.0-2.0.2" # Must be compatible with: `kotlin` +androidxComposeBom = "2025.06.01" +hilt = "2.56.2" arkitekt = "5.2.2" timber = "5.0.1" testCore = "1.6.1" -testRunner = "1.5.2" +testRunner = "1.6.2" junit = "1.2.1" -mockk = "1.13.8" -serializationJson = "1.6.3" -serializationConverter = "1.0.0" +mockk = "1.14.4" +serializationJson = "1.8.1" okhttp = "4.12.0" -retrofit = "2.11.0" -navigation = "2.8.5" +retrofit = "3.0.0" +navigation = "2.9.0" hiltNavigation = "1.2.0" composeLint = "1.4.2" -androidx-activity-compose = "1.9.3" -jdkDesugaring = "2.1.4" -androidx = "1.15.0" -appcompat = "1.7.0" -lifecycle = "2.8.7" -preference = "1.2.1" +androidx-activity-compose = "1.10.1" +jdkDesugaring = "2.1.5" +androidx = "1.16.0" +appcompat = "1.7.1" +lifecycle = "2.9.1" activity = "1.9.0" -detekt = "1.23.6" -ktlintGradle = "12.1.2" -ktlint = "1.2.1" +detekt = "1.23.8" +ktlintGradle = "12.3.0" +ktlint = "1.6.0" google-servicesPlugin = "4.4.2" -google-firebaseAppDistributionPlugin = "5.0.0" +google-firebaseAppDistributionPlugin = "5.1.1" sheethappens = "1.0.3" splash-screen = "1.0.1" -coil = "3.0.4" -datastore = "1.1.2" +coil = "3.2.0" +datastore = "1.1.7" [libraries] @@ -62,14 +57,12 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle" } activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } -androidx-preferences = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" } # Serialization serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serializationJson" } -serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "serializationConverter" } # MVVM arkitekt-usecases = { group = "app.futured.arkitekt", name = "cr-usecases", version.ref = "arkitekt" } @@ -87,6 +80,7 @@ test-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "j okHttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } coil = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc25..ff23a68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME