diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fea8cbb..5bd99bf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -53,7 +53,10 @@ dependencies { implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.kotlinx.collections.immutable) debugImplementation(platform(libs.androidx.compose.bom)) debugImplementation(libs.androidx.compose.ui.tooling) + + lintChecks(libs.compose.lint.checks) } diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt index 02d8063..d85dcaa 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt @@ -29,8 +29,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.juandiana.reflexgame.R +import com.juandiana.reflexgame.ui.theme.ExtendedReflexGameTheme import com.juandiana.reflexgame.ui.theme.ReflexGameTheme -import com.juandiana.reflexgame.ui.theme.spacing +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import java.util.Locale @Composable @@ -51,19 +53,20 @@ fun MainScreenRoute(viewModel: MainViewModel) { @Composable fun MainScreen( showTitle: Boolean, - squares: List, + squares: ImmutableList, scoreText: String, buttonText: String, elapsedTimeText: String, columnCount: Int, onSquareTouched: (Int) -> Unit, - onButtonClicked: () -> Unit + onButtonClicked: () -> Unit, + modifier: Modifier = Modifier ) { - Surface(modifier = Modifier.fillMaxSize()) { + Surface(modifier = modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() - .padding(MaterialTheme.spacing.medium) + .padding(ExtendedReflexGameTheme.spacing.medium) ) { Text( modifier = Modifier @@ -72,20 +75,20 @@ fun MainScreen( text = stringResource(id = R.string.game_over).uppercase(Locale.getDefault()), style = MaterialTheme.typography.displayLarge ) - Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + Spacer(modifier = Modifier.height(ExtendedReflexGameTheme.spacing.medium)) SquaresGrid( modifier = Modifier.fillMaxWidth(), squares = squares, columnCount = columnCount, onSquareTouched = onSquareTouched ) - Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + Spacer(modifier = Modifier.height(ExtendedReflexGameTheme.spacing.medium)) HeadsUpDisplay( modifier = Modifier.fillMaxWidth(), scoreText = scoreText, elapsedTimeText = elapsedTimeText ) - Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + Spacer(modifier = Modifier.height(ExtendedReflexGameTheme.spacing.medium)) Button( onClick = onButtonClicked, modifier = Modifier.fillMaxWidth() @@ -101,7 +104,7 @@ fun MainScreen( @Composable private fun SquaresGrid( - squares: List, + squares: ImmutableList, columnCount: Int, onSquareTouched: (Int) -> Unit, modifier: Modifier = Modifier @@ -109,7 +112,7 @@ private fun SquaresGrid( Column( modifier = modifier .background(Color.Black) - .padding(MaterialTheme.spacing.border) + .padding(ExtendedReflexGameTheme.spacing.border) ) { squares.chunked(columnCount).forEachIndexed { rowIndex, rowSquares -> Row(modifier = Modifier.fillMaxWidth()) { @@ -137,7 +140,7 @@ private fun SquaresGrid( private fun HeadsUpDisplay( scoreText: String, elapsedTimeText: String, - modifier: Modifier + modifier: Modifier = Modifier ) { Row( modifier = modifier, @@ -147,7 +150,7 @@ private fun HeadsUpDisplay( text = stringResource(id = R.string.score), fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) + Spacer(modifier = Modifier.width(ExtendedReflexGameTheme.spacing.small)) Text( modifier = Modifier.weight(1f), text = scoreText @@ -156,7 +159,7 @@ private fun HeadsUpDisplay( text = stringResource(id = R.string.time), fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) + Spacer(modifier = Modifier.width(ExtendedReflexGameTheme.spacing.small)) Text(text = elapsedTimeText) } } @@ -167,7 +170,7 @@ private fun SquareItem( onSquareTouched: () -> Unit, modifier: Modifier = Modifier ) { - val border = MaterialTheme.spacing.border + val border = ExtendedReflexGameTheme.spacing.border val color = when (squareState) { SquareState.CLEAR -> Color.White SquareState.FLASHED -> Color.Blue @@ -198,7 +201,7 @@ private fun MainScreenPreview() { ReflexGameTheme { MainScreen( showTitle = false, - squares = listOf( + squares = persistentListOf( SquareState.CLEAR, SquareState.FLASHED, SquareState.CLEAR, diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt index 5c41098..dc5178b 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt @@ -1,5 +1,7 @@ package com.juandiana.reflexgame.ui +import kotlinx.collections.immutable.ImmutableList + enum class SquareState { CLEAR, FLASHED, @@ -9,7 +11,7 @@ enum class SquareState { data class MainScreenState( val showTitle: Boolean, - val squares: List, + val squares: ImmutableList, val scoreText: String, val buttonText: String, val elapsedTimeText: String diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt index 4a91c56..7a82ddb 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList class MainViewModel( private val resourcesProvider: ResourcesProvider, @@ -99,7 +101,7 @@ class MainViewModel( } else { SquareState.CLEAR } - } + }.toImmutableList() ) } @@ -117,7 +119,7 @@ class MainViewModel( } else { SquareState.CLEAR } - } + }.toImmutableList() ) } @@ -135,7 +137,8 @@ class MainViewModel( } } - private fun clearSquares(): List = List(gameEngine.squaresCount) { SquareState.CLEAR } + private fun clearSquares(): ImmutableList = + List(gameEngine.squaresCount) { SquareState.CLEAR }.toImmutableList() private fun elapsedSecondsSince(startTime: Long): Long { return ((SystemClock.elapsedRealtime() - startTime) / 1000L).coerceAtLeast(0L) diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt index 9136913..6195e8e 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt @@ -1,7 +1,6 @@ package com.juandiana.reflexgame.ui.theme -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable +import android.annotation.SuppressLint import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.unit.Dp @@ -14,8 +13,5 @@ internal data class ReflexGameSpacing( val medium: Dp = 16.dp ) +@SuppressLint("ComposeCompositionLocalUsage") internal val LocalReflexGameSpacing = staticCompositionLocalOf { ReflexGameSpacing() } - -internal val MaterialTheme.spacing: ReflexGameSpacing - @Composable - get() = LocalReflexGameSpacing.current diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt index c505e88..d72b710 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt @@ -12,15 +12,18 @@ private val ReflexGameLightColorScheme = lightColorScheme( onSecondary = Black ) +internal object ExtendedReflexGameTheme { + val spacing: ReflexGameSpacing + @Composable + get() = LocalReflexGameSpacing.current +} + @Composable internal fun ReflexGameTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = ReflexGameLightColorScheme, - typography = ReflexGameTypography - ) { - CompositionLocalProvider( - LocalReflexGameSpacing provides ReflexGameSpacing(), - content = content - ) + CompositionLocalProvider(LocalReflexGameSpacing provides ReflexGameSpacing()) { + MaterialTheme( + colorScheme = ReflexGameLightColorScheme, + typography = ReflexGameTypography + ) { content() } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd7bc67..3a876a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,18 @@ [versions] android-gradle-plugin = "9.0.1" kotlin = "2.3.10" +kotlinx-collections-immutable = "0.4.0" androidx-activity = "1.12.4" androidx-lifecycle = "2.10.0" androidx-compose-bom = "2026.02.00" +compose-lints = "1.4.2" [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [libraries] +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } @@ -18,3 +21,4 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +compose-lint-checks = { module = "com.slack.lint.compose:compose-lint-checks", version.ref = "compose-lints" }