From 9018c26d2242aac1c422d8a270864c29eae7eade Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Thu, 19 Mar 2026 15:30:40 +0100 Subject: [PATCH 1/7] Update dependencies & migrate to AGP9 --- CLAUDE.md | 95 +++++++++++++++++++ app/build.gradle.kts | 63 ++++++------ build.gradle.kts | 1 - buildSrc/src/main/kotlin/ProjectSettings.kt | 4 +- .../DependencyUpdates.kt | 24 +++-- .../androidprojecttemplate/LintCheck.kt | 2 +- gradle.properties | 3 - gradle/libs.versions.toml | 40 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 9 files changed, 163 insertions(+), 71 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4b61b3b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with code in this repository. + +## Common Commands + +- `./gradlew lintCheck` - Run ktlint and detekt checks (same as CI) +- `./gradlew ktlintFormat` - Automatically fix code style issues +- `./gradlew test` - Run unit tests +- `./gradlew clean` - Remove all build artifacts + +## Module Structure + +Single-module project with the main app under `:app`. Build logic lives in `buildSrc/` and `convention-plugins/`. + +Package root: `app.futured.androidprojecttemplate` + +## Architecture + +**MVVM** with [Arkitekt](https://github.com/futuredapp/arkitekt) library: + +1. **ViewModel** (`*ViewModel.kt`) - extends `BaseViewModel`, implements `Actions` interface from the screen +2. **ViewState** (`*ViewState.kt`) - holds mutable Compose state (`var counter by mutableIntStateOf(0)`) +3. **Events** (`*Events.kt`) - sealed class of one-time events (navigation, toasts); collected via `EventsEffect` +4. **Screen** (`*Screen.kt`) - Composable; receives `viewState`, collects events, delegates interactions to `Actions` + +Actions are defined as a nested interface inside the Screen object: +```kotlin +object HomeScreen { + interface Actions { + fun onIncrementCounter() + fun onNavigateToDetail() + } +} +``` + +## Navigation + +Jetpack Navigation Compose with: +- **`Destinations.kt`** - sealed class with typed route objects (support arguments, deep links) +- **`NavRouter`** - interface abstracting navigation operations, implemented by `NavRouterImpl` +- **`NavGraph.kt`** - registers all screens/dialogs in `NavHost` + +Arguments are passed as serialized Base64 strings via `SavedStateHandle`. + +## Dependency Injection + +**Hilt** throughout: +- `@HiltAndroidApp` on `App`, `@AndroidEntryPoint` on `AppActivity` +- `@HiltViewModel` on ViewModels, `@ViewModelScoped` on ViewState +- Modules: `ApplicationModule` (singletons), `NetworkModule` (Retrofit/OkHttp) + +## Network Layer + +Retrofit + OkHttp + kotlinx.serialization: +- Base URL and constants in `Constants.kt` +- API interface: `ApiService.kt` with suspend functions +- Models annotated with `@Serializable` +- `ZonedDateTimeSerializer` provided for date handling + +## Build Flavors + +Flavor dimension: `api` with three flavors: +- **mock** - local mock data +- **dev** - development API +- **prod** - production API + +Build types: `debug`, `enterprise` (minified, debug key), `release` (minified, release key). + +## Code Style + +- Max line length: **140 characters** +- Indent: **4 spaces**, trailing commas allowed +- Ktlint code style: `android_studio` +- Detekt config: `config/detekt.yml` + +## Naming Conventions + +- `HomeViewModel`, `HomeViewState`, `HomeEvents`, `HomeScreen` +- Event objects: `NavigateToDetailEvent`, `NavigateBackEvent` +- Action methods: `onIncrementCounter()`, `onNavigateToDetail()` (prefix `on`) +- Composable functions: PascalCase; preview functions: `private fun HomePreview()` + +## Design System + +Material3 via `MaterialTheme`. Colors, typography, shapes, and dimensions defined in `app/src/main/kotlin/.../ui/theme/`. + +Use `Dimensions.kt` tokens for spacing — avoid raw `dp` literals where theme tokens exist. + +## Testing + +- Unit tests: JUnit 4 + MockK +- Instrumented tests: AndroidJUnit4 +- Run unit tests: `./gradlew test` +- Run module tests: `./gradlew :app:test` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa27767..37daae6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.hilt) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.serialization) @@ -26,14 +25,6 @@ android { versionName = ProjectSettings.versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - javaCompileOptions { - annotationProcessorOptions { - arguments.apply { - put("room.schemaLocation", "$projectDir/schemas") - } - } - } } packaging { @@ -57,12 +48,15 @@ android { } sourceSets { - getByName("main").java.setSrcDirs(setOf("src/main/kotlin")) - create(ProjectSettings.Flavor.DEV).java.setSrcDirs(setOf("src/dev/kotlin")) - create(ProjectSettings.Flavor.PROD).java.setSrcDirs(setOf("src/prod/kotlin")) - create(ProjectSettings.Flavor.MOCK).java.setSrcDirs(setOf("src/mock/kotlin")) - getByName("test").java.setSrcDirs(setOf("src/test/kotlin")) - getByName("androidTest").java.setSrcDirs(setOf("src/androidTest/kotlin")) + named("main") { + kotlin.directories += setOf("src/main/kotlin") + } + named("test") { + kotlin.directories += setOf("src/test/kotlin") + } + named("androidTest") { + kotlin.directories += setOf("src/androidTest/kotlin") + } } signingConfigs { @@ -81,26 +75,24 @@ android { } buildTypes { - buildTypes { - getByName(ProjectSettings.BuildType.DEBUG) { - isMinifyEnabled = false - isShrinkResources = false - signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.DEBUG) - } - create(ProjectSettings.BuildType.ENTERPRISE) { - isMinifyEnabled = true - isShrinkResources = true - signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.DEBUG) - proguardFile(getDefaultProguardFile("proguard-android.txt")) - proguardFile(file("proguard-rules.pro")) - } - getByName(ProjectSettings.BuildType.RELEASE) { - isMinifyEnabled = true - isShrinkResources = true - signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.RELEASE) - proguardFile(getDefaultProguardFile("proguard-android.txt")) - proguardFile(file("proguard-rules.pro")) - } + getByName(ProjectSettings.BuildType.DEBUG) { + isMinifyEnabled = false + isShrinkResources = false + signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.DEBUG) + } + create(ProjectSettings.BuildType.ENTERPRISE) { + isMinifyEnabled = true + isShrinkResources = true + signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.DEBUG) + proguardFile(getDefaultProguardFile("proguard-android-optimize.txt")) + proguardFile(file("proguard-rules.pro")) + } + getByName(ProjectSettings.BuildType.RELEASE) { + isMinifyEnabled = true + isShrinkResources = true + signingConfig = signingConfigs.getByName(ProjectSettings.BuildType.RELEASE) + proguardFile(getDefaultProguardFile("proguard-android-optimize.txt")) + proguardFile(file("proguard-rules.pro")) } } @@ -163,6 +155,7 @@ dependencies { implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material.icons) debugImplementation(libs.androidx.compose.ui.tooling) // MVVM diff --git a/build.gradle.kts b/build.gradle.kts index 7a31ff6..b28e566 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,6 @@ import io.gitlab.arturbosch.detekt.report.ReportMergeTask plugins { alias(libs.plugins.android.application) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.ktlint) apply false diff --git a/buildSrc/src/main/kotlin/ProjectSettings.kt b/buildSrc/src/main/kotlin/ProjectSettings.kt index 85541b6..0f9f8c8 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -2,8 +2,8 @@ import org.gradle.api.JavaVersion object ProjectSettings { const val applicationId = "app.futured.androidprojecttemplate" - const val compileSdkVersion = 35 - const val targetSdk = 35 + const val compileSdkVersion = 36 + const val targetSdk = 36 const val minSdk = 29 val versionName = System.getenv("ANDROID_VERSION_NAME") ?: "1.0.0" val versionCode = System.getenv("ANDROID_BUILD_NUMBER")?.toIntOrNull() ?: 1 diff --git a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/DependencyUpdates.kt b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/DependencyUpdates.kt index cd9795c..6aa8a9d 100644 --- a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/DependencyUpdates.kt +++ b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/DependencyUpdates.kt @@ -2,24 +2,32 @@ package app.futured.androidprojecttemplate import ProjectSettings import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent +import org.gradle.api.Action @Suppress("UnnecessaryAbstractClass") abstract class DependencyUpdates : DependencyUpdatesTask() { init { - group = ProjectSettings.TASK_GROUP + group = ProjectSettings.Gradle.TaskGroup this.resolutionStrategy { componentSelection { - all { - val rejected = listOf("alpha", "beta", "rc", "cr", "m", "preview", "testing") - .map { qualifier -> Regex("(?i).*[.-]$qualifier[.\\d-]*") } - .any { it.matches(candidate.version) } - if (rejected) { - reject("Release candidate") + all( + Action { + if (isNonStable(candidate.version) && !isNonStable(currentVersion)) { + reject("Release candidate") + } } - } + ) } } } + + fun isNonStable(version: String): Boolean { + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isStable = stableKeyword || regex.matches(version) + return isStable.not() + } } diff --git a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt index 39c5a4b..1b69eed 100644 --- a/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt +++ b/buildSrc/src/main/kotlin/app/futured/androidprojecttemplate/LintCheck.kt @@ -11,7 +11,7 @@ open class LintCheck : DefaultTask() { group = ProjectSettings.Gradle.TaskGroup /* - These tasks runs for each subproject that has applied ktlint or detekt plugins. + These tasks run for each subproject that has applied ktlint or detekt plugins. */ configure { project.subprojects diff --git a/gradle.properties b/gradle.properties index 550b14a..5b48cfc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,3 @@ org.gradle.configuration-cache.problems=warn #Kotlin kotlin.code.style=official - -#Android -android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 747a7b2..43c6492 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,37 +1,37 @@ [versions] -agp = "8.11.0" -gradleVersionsPlugin = "0.50.0" -kotlin = "2.2.0" -ksp = "2.2.0-2.0.2" # Must be compatible with: `kotlin` -androidxComposeBom = "2025.06.01" -hilt = "2.56.2" +agp = "9.1.0" +gradleVersionsPlugin = "0.53.0" +kotlin = "2.3.0" +ksp = "2.3.4" +androidxComposeBom = "2026.03.00" +hilt = "2.59.2" arkitekt = "5.2.2" timber = "5.0.1" testCore = "1.6.1" testRunner = "1.6.2" junit = "1.2.1" mockk = "1.14.4" -serializationJson = "1.8.1" -okhttp = "4.12.0" +serializationJson = "1.10.0" +okhttp = "5.3.2" retrofit = "3.0.0" navigation = "2.9.0" -hiltNavigation = "1.2.0" +hiltNavigation = "1.3.0" composeLint = "1.4.2" -androidx-activity-compose = "1.10.1" +androidx-activity-compose = "1.13.0" jdkDesugaring = "2.1.5" -androidx = "1.16.0" +androidx = "1.18.0" appcompat = "1.7.1" -lifecycle = "2.9.1" -activity = "1.9.0" +lifecycle = "2.10.0" +activity = "1.13.0" detekt = "1.23.8" -ktlintGradle = "12.3.0" +ktlintGradle = "14.2.0" ktlint = "1.6.0" -google-servicesPlugin = "4.4.2" -google-firebaseAppDistributionPlugin = "5.1.1" +google-servicesPlugin = "4.4.4" +google-firebaseAppDistributionPlugin = "5.2.1" sheethappens = "1.0.3" -splash-screen = "1.0.1" -coil = "3.2.0" -datastore = "1.1.7" +splash-screen = "1.2.0" +coil = "3.4.0" +datastore = "1.2.1" [libraries] @@ -43,6 +43,7 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-core" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } # https://developer.android.com/studio/write/java8-support#library-desugaring-versions @@ -100,7 +101,6 @@ gradlePlugin-versions = { module = "com.github.ben-manes:gradle-versions-plugin" [plugins] android-application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } firebase-distribution = { id = "com.google.firebase.appdistribution", version.ref = "google-firebaseAppDistributionPlugin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68..c61a118 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.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 766a0b17200c7e5c2117be20f96e23f993863cb6 Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Fri, 20 Mar 2026 17:05:45 +0100 Subject: [PATCH 2/7] Update arkitekt --- app/build.gradle.kts | 4 +- .../tools/arch/BaseViewModel.kt | 56 ------------------- .../_templateScreen/_TEMPLATEScreen.kt | 8 +-- .../ui/screens/detail/DetailScreen.kt | 6 +- .../ui/screens/detail/DetailViewModel.kt | 7 +-- .../ui/screens/home/HomeScreen.kt | 6 +- .../ui/screens/home/HomeViewModel.kt | 7 +-- app/src/main/res/values-night/styles.xml | 2 +- app/src/main/res/values/styles.xml | 11 +--- gradle/libs.versions.toml | 6 +- settings.gradle.kts | 1 + 11 files changed, 25 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37daae6..62ac596 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,8 +158,8 @@ dependencies { implementation(libs.androidx.compose.material.icons) debugImplementation(libs.androidx.compose.ui.tooling) - // MVVM - implementation(libs.arkitekt.usecases) + // Arkitekt + implementation(libs.arkitekt.compose) // Hilt implementation(libs.hilt.android) 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 deleted file mode 100644 index 4eeadc1..0000000 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/arch/BaseViewModel.kt +++ /dev/null @@ -1,56 +0,0 @@ -package app.futured.androidprojecttemplate.tools.arch - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.futured.arkitekt.core.ViewState -import app.futured.arkitekt.core.event.Event -import app.futured.arkitekt.crusecases.CoroutineScopeOwner -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -abstract class BaseViewModel : - ViewModel(), - CoroutineScopeOwner { - abstract val viewState: VS - - override val coroutineScope: CoroutineScope = viewModelScope - - private val eventChannel = Channel>(Channel.BUFFERED) - val events = eventChannel - .receiveAsFlow() - .flowOn(Dispatchers.Main) - - fun sendEvent(event: Event) = viewModelScope.launch { - eventChannel.send(event) - } -} - -/** - * When [EventsEffect] enters composition, it will start observing the event flow from it's viewModel. - * Each event sent from ViewModel goes through [observer] lambda which can be used to react to a specific event. - * Use the [onEvent] function to filter out the event you are interested in. - * - * @param observer Event receiver lambda. - */ -@Composable -fun BaseViewModel.EventsEffect( - observer: suspend Event.() -> Unit, -) { - LaunchedEffect(this) { - events.collect { - observer(it) - } - } -} - -inline fun > Event<*>.onEvent(action: (E) -> Unit) { - if (this is E) { - action(this) - } -} 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 9a946f6..c884af7 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 @@ -18,13 +18,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import app.futured.androidprojecttemplate.navigation.NavRouter -import app.futured.androidprojecttemplate.tools.arch.BaseViewModel -import app.futured.androidprojecttemplate.tools.arch.EventsEffect -import app.futured.androidprojecttemplate.tools.arch.onEvent import app.futured.androidprojecttemplate.tools.compose.ScreenPreviews import app.futured.androidprojecttemplate.ui.components.Showcase +import app.futured.arkitekt.compose.BaseViewModel +import app.futured.arkitekt.compose.EventsEffect +import app.futured.arkitekt.compose.onEvent import app.futured.arkitekt.core.ViewState import app.futured.arkitekt.core.event.Event import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailScreen.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailScreen.kt index ce18eeb..12328d7 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailScreen.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/detail/DetailScreen.kt @@ -17,13 +17,13 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import app.futured.androidprojecttemplate.navigation.NavRouter -import app.futured.androidprojecttemplate.tools.arch.EventsEffect -import app.futured.androidprojecttemplate.tools.arch.onEvent import app.futured.androidprojecttemplate.tools.compose.ScreenPreviews import app.futured.androidprojecttemplate.ui.components.AddFloatingActionButton import app.futured.androidprojecttemplate.ui.components.Showcase +import app.futured.arkitekt.compose.EventsEffect +import app.futured.arkitekt.compose.onEvent @Composable fun DetailScreen( 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 cf62112..26b2ddb 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 @@ -1,13 +1,12 @@ package app.futured.androidprojecttemplate.ui.screens.detail -import app.futured.androidprojecttemplate.tools.arch.BaseViewModel +import app.futured.arkitekt.compose.BaseViewModel 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/HomeScreen.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeScreen.kt index 392c75e..93f7979 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/ui/screens/home/HomeScreen.kt @@ -15,13 +15,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import app.futured.androidprojecttemplate.navigation.NavRouter -import app.futured.androidprojecttemplate.tools.arch.EventsEffect -import app.futured.androidprojecttemplate.tools.arch.onEvent import app.futured.androidprojecttemplate.tools.compose.ScreenPreviews import app.futured.androidprojecttemplate.ui.components.AddFloatingActionButton import app.futured.androidprojecttemplate.ui.components.Showcase +import app.futured.arkitekt.compose.EventsEffect +import app.futured.arkitekt.compose.onEvent @Composable fun HomeScreen( 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 2f6ad21..9be9eb2 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 @@ -1,13 +1,12 @@ package app.futured.androidprojecttemplate.ui.screens.home -import app.futured.androidprojecttemplate.tools.arch.BaseViewModel +import app.futured.arkitekt.compose.BaseViewModel 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/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 506c586..32cfc8b 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,7 +1,7 @@ diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 06bee7c..4c48330 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,17 +1,10 @@ - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43c6492..bf7ba73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ kotlin = "2.3.0" ksp = "2.3.4" androidxComposeBom = "2026.03.00" hilt = "2.59.2" -arkitekt = "5.2.2" +arkitekt = "6.X.X-SNAPSHOT" timber = "5.0.1" testCore = "1.6.1" testRunner = "1.6.2" @@ -65,8 +65,8 @@ androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", v # Serialization serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serializationJson" } -# MVVM -arkitekt-usecases = { group = "app.futured.arkitekt", name = "cr-usecases", version.ref = "arkitekt" } +# Arkitekt +arkitekt-compose = { group = "app.futured.arkitekt", name = "compose", version.ref = "arkitekt" } # Other timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b1a0c98..b21c7d3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://central.sonatype.com/repository/maven-snapshots/") } } From df7c1b9a63598b9aa5f717cec0643f4a21f024c2 Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Fri, 20 Mar 2026 17:10:43 +0100 Subject: [PATCH 3/7] Remove unused variable --- .../ui/screens/detail/DetailViewModel.kt | 4 +++- .../androidprojecttemplate/ui/screens/home/HomeViewModel.kt | 4 +++- buildSrc/src/main/kotlin/ProjectSettings.kt | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) 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 26b2ddb..a51a113 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,7 +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 9be9eb2..43dbae6 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,7 +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 0f9f8c8..a2a0396 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -41,6 +41,4 @@ object ProjectSettings { val KEY_PASSWORD = System.getenv("ANDROID_KEY_PASSWORD") ?: "" val STORE_PASSWORD = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: "" } - - const val TASK_GROUP = "futured" } From 419b10cbd85d57f0a8153c9bd189535a28c96922 Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Mon, 23 Mar 2026 11:12:29 +0100 Subject: [PATCH 4/7] Fix test task --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1f7b94f..3cfad11 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -11,7 +11,7 @@ jobs: uses: futuredapp/.github/.github/workflows/android-cloud-check.yml@2.0.0 with: LINT_GRADLE_TASKS: lintCheck - TEST_GRADLE_TASKS: testDevEnterpriseUnitTest + TEST_GRADLE_TASKS: testDevDebugUnitTest secrets: # TODO Set up `GRADLE_CACHE_ENCRYPTION_KEY` for this GitHub repository GRADLE_CACHE_ENCRYPTION_KEY: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} From f4bb42c46d6e2b71fa60805fc919aee304ad2189 Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Mon, 23 Mar 2026 11:35:51 +0100 Subject: [PATCH 5/7] Add Arkitekt API reference to CLAUDE.md --- CLAUDE.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 4b61b3b..9707173 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,63 @@ object HomeScreen { } ``` +## Arkitekt API Reference + +### `BaseCoreViewModel` +- `abstract val viewState: VS` — injected ViewState +- `sendEvent(event: Event)` — sends a one-time event to the UI layer + +### `BaseViewModel` (extends `BaseCoreViewModel`, implements `CoroutineScopeOwner`) +- Provides `coroutineScope` backed by `viewModelScope` +- Inherits all `CoroutineScopeOwner` extension functions below + +### Use cases +Always extend the Arkitekt base classes — never use plain `suspend` functions with `invoke()`: + +```kotlin +// UseCase — single async operation +class SignInUseCase @Inject constructor(...) : UseCase() { + override suspend fun build(args: Unit) { /* business logic */ } +} + +// FlowUseCase — streaming operation +class ObserveSomethingUseCase @Inject constructor(...) : FlowUseCase() { + override fun build(args: Unit): Flow = /* … */ +} +``` + +### `CoroutineScopeOwner` — use-case execution in ViewModels + +```kotlin +// Async execution with callbacks (preferred; cancels previous by default) +someUseCase.execute { + onStart { /* show loading */ } + onSuccess { value -> sendEvent(MyEvent) } // sendEvent is non-suspend, safe here + onError { throwable -> /* … */ } +} + +// Suspend execution — use inside launchWithHandler for error handling +launchWithHandler { + val result = someUseCase.execute() // returns Result + result.getOrNull() // or getOrThrow(), getOrDefault(), fold(…) +} + +// Flow use case +someFlowUseCase.execute { + onStart { } + onNext { value -> } + onError { throwable -> } + onComplete { } +} +``` + +### `EventsEffect` / `onEvent` +```kotlin +EventsEffect { + onEvent { /* handle */ } +} +``` + ## Navigation Jetpack Navigation Compose with: From 65e753764f66310ce08e744f99e46826b858d74e Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Mon, 23 Mar 2026 13:29:24 +0100 Subject: [PATCH 6/7] Remove source sets --- app/build.gradle.kts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62ac596..8229fa5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,18 +47,6 @@ android { targetCompatibility = ProjectSettings.JavaCompatibility } - sourceSets { - named("main") { - kotlin.directories += setOf("src/main/kotlin") - } - named("test") { - kotlin.directories += setOf("src/test/kotlin") - } - named("androidTest") { - kotlin.directories += setOf("src/androidTest/kotlin") - } - } - signingConfigs { getByName(ProjectSettings.BuildType.DEBUG) { storeFile = rootProject.file("./keystore/debug.jks") From 737db31746d61f49cabded026b0e65e8f0e3926d Mon Sep 17 00:00:00 2001 From: "jan.mikulik" Date: Mon, 23 Mar 2026 13:45:02 +0100 Subject: [PATCH 7/7] Fix test gradle task + clean CLAUDE.md --- .github/workflows/nightly.yml | 2 +- CLAUDE.md | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6c87484..e262dba 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,7 +36,7 @@ jobs: needs: generate_version uses: futuredapp/.github/.github/workflows/android-cloud-nightly-build.yml@2.0.0 with: - TEST_GRADLE_TASKS: testDevEnterpriseUnitTest + TEST_GRADLE_TASKS: testDevDebugUnitTest PACKAGE_GRADLE_TASK: packageDevEnterpriseUniversalApk UPLOAD_GRADLE_TASK: appDistributionUploadDevEnterprise # TODO Verify app distribution groups diff --git a/CLAUDE.md b/CLAUDE.md index 9707173..c7366ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,8 +13,6 @@ This file provides guidance to Claude Code when working with code in this reposi Single-module project with the main app under `:app`. Build logic lives in `buildSrc/` and `convention-plugins/`. -Package root: `app.futured.androidprojecttemplate` - ## Architecture **MVVM** with [Arkitekt](https://github.com/futuredapp/arkitekt) library: @@ -91,15 +89,6 @@ EventsEffect { } ``` -## Navigation - -Jetpack Navigation Compose with: -- **`Destinations.kt`** - sealed class with typed route objects (support arguments, deep links) -- **`NavRouter`** - interface abstracting navigation operations, implemented by `NavRouterImpl` -- **`NavGraph.kt`** - registers all screens/dialogs in `NavHost` - -Arguments are passed as serialized Base64 strings via `SavedStateHandle`. - ## Dependency Injection **Hilt** throughout: @@ -107,14 +96,6 @@ Arguments are passed as serialized Base64 strings via `SavedStateHandle`. - `@HiltViewModel` on ViewModels, `@ViewModelScoped` on ViewState - Modules: `ApplicationModule` (singletons), `NetworkModule` (Retrofit/OkHttp) -## Network Layer - -Retrofit + OkHttp + kotlinx.serialization: -- Base URL and constants in `Constants.kt` -- API interface: `ApiService.kt` with suspend functions -- Models annotated with `@Serializable` -- `ZonedDateTimeSerializer` provided for date handling - ## Build Flavors Flavor dimension: `api` with three flavors: