Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
133 changes: 133 additions & 0 deletions CLAUDE.md
Copy link
Copy Markdown
Member

@matejsemancik matejsemancik Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need init script to remove / edit this after cloning the template

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture-related part is fine, but project and business-related part will get outdated as you init the project and start working on it. Maybe we could just delete template-related stuff from it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned it up a little bit 👍🏻

Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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/`.

## Architecture

**MVVM** with [Arkitekt](https://github.com/futuredapp/arkitekt) library:

1. **ViewModel** (`*ViewModel.kt`) - extends `BaseViewModel<ViewState>`, 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()
}
}
```

## Arkitekt API Reference

### `BaseCoreViewModel<VS>`
- `abstract val viewState: VS` — injected ViewState
- `sendEvent(event: Event<VS>)` — sends a one-time event to the UI layer

### `BaseViewModel<VS>` (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<ARGS, RESULT> — single async operation
class SignInUseCase @Inject constructor(...) : UseCase<Unit, Unit>() {
override suspend fun build(args: Unit) { /* business logic */ }
}

// FlowUseCase<ARGS, T> — streaming operation
class ObserveSomethingUseCase @Inject constructor(...) : FlowUseCase<Unit, MyModel>() {
override fun build(args: Unit): Flow<MyModel> = /* … */
}
```

### `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<T>
result.getOrNull() // or getOrThrow(), getOrDefault(), fold(…)
}

// Flow use case
someFlowUseCase.execute {
onStart { }
onNext { value -> }
onError { throwable -> }
onComplete { }
}
```

### `EventsEffect` / `onEvent`
```kotlin
EventsEffect {
onEvent<MyEvent> { /* handle */ }
}
```

## Dependency Injection

**Hilt** throughout:
- `@HiltAndroidApp` on `App`, `@AndroidEntryPoint` on `AppActivity`
- `@HiltViewModel` on ViewModels, `@ViewModelScoped` on ViewState
- Modules: `ApplicationModule` (singletons), `NetworkModule` (Retrofit/OkHttp)

## 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`
61 changes: 21 additions & 40 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -26,14 +25,6 @@ android {
versionName = ProjectSettings.versionName

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

javaCompileOptions {
annotationProcessorOptions {
arguments.apply {
put("room.schemaLocation", "$projectDir/schemas")
}
}
}
}

packaging {
Expand All @@ -56,15 +47,6 @@ android {
targetCompatibility = ProjectSettings.JavaCompatibility
}

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"))
}

signingConfigs {
getByName(ProjectSettings.BuildType.DEBUG) {
storeFile = rootProject.file("./keystore/debug.jks")
Expand All @@ -81,26 +63,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"))
}
}

Expand Down Expand Up @@ -163,10 +143,11 @@ 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
implementation(libs.arkitekt.usecases)
// Arkitekt
implementation(libs.arkitekt.compose)

// Hilt
implementation(libs.hilt.android)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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<DetailViewState>(),
Detail.Actions {

override fun onNavigateBack() {
sendEvent(NavigateBackEvent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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<HomeViewState>(),
Home.Actions {

override fun onIncrementCounter() {
viewState.counter++
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-night/styles.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.App" parent="Theme.Base">
<item name="android:windowLightStatusBar" tools:ignore="NewApi">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
Expand Down
Loading
Loading