Reputation: 21
I'm pretty new with Android Compose so I decided to make an app where I can dynamically change the style after the client chooses a fruit. After changing the style, a couple of SVG files take that style (color) to fill an area of the graphic. In the first couple of interactions (choose a fruit, pick fruit from the tree, blend them, drink juice) everything works fine and the right colors are displayed in the SVG files. After the fourth or fifth round the last fruit's color is shown in the SVG though. If I choose the same fruit again (directly after seeing the wrong color) the right color will be shown.
Here is my code: MainActivity.kt
package com.example.app.juicer
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.juicer.dto.Fruit
import com.example.app.juicer.dto.StandardScreenCompounder
import com.example.app.juicer.ui.theme.JuicerTheme
import kotlin.random.Random.Default.nextInt
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
JuicerTheme() {
JuicerApp()
}
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JuicerApp() {
var selectedFruitColorStyle by remember { mutableStateOf(R.style.DefaultFruitColor) }
var step by remember { mutableStateOf(0) }
var squeezingLevel by remember { mutableStateOf(0) }
var chosenFruit by remember { mutableStateOf(Fruit(R.string.clover_sgl, R.string.clover_prl, R.style.CloverColor, R.drawable.clover)) }
var collectedFruits by remember { mutableStateOf(0) }
val fruits = listOf(
Fruit(R.string.apple_sgl, R.string.apple_prl, R.style.AppleColor, R.drawable.apple),
Fruit(R.string.banana_sgl, R.string.banana_prl, R.style.BananaColor, R.drawable.banana),
Fruit(R.string.pineapple_sgl, R.string.pineapple_prl, R.style.PineappleColor, R.drawable.pineapple),
Fruit(R.string.grape_sgl, R.string.grape_prl, R.style.GrapeColor, R.drawable.grape),
Fruit(R.string.lemon_sgl, R.string.lemon_prl, R.style.LemonColor, R.drawable.lemon),
)
JuicerTheme(darkTheme = false, dynamicColor = true, selectedStyle = selectedFruitColorStyle) {
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
),
title = {
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
}
)
},
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
when (step) {
1 -> FruitChooser(
fruits,
stringResource(id = R.string.text_1),
{ fruit ->
chosenFruit = fruit
selectedFruitColorStyle = chosenFruit.style
},
{ step++ }
)
2 -> OverlappedFruitContent(
StandardScreenCompounder(
"screen2",
stringResource(id = R.string.text_2, stringResource(id = chosenFruit.namePlural)),
chosenFruit,
R.drawable.tree,
R.string.pic_desc_2
),
{
if (collectedFruits < 3) {
collectedFruits++
} else {
step++
}
},
collectedFruits
)
3 -> StandardContent(
StandardScreenCompounder(
"screen3",
stringResource(id = R.string.text_3, stringResource(id = chosenFruit.namePlural)),
chosenFruit,
R.drawable.blender,
R.string.pic_desc_3
),
{
if (squeezingLevel < 3) {
squeezingLevel++
} else {
step++
}
},
{
while (squeezingLevel <= 3) {
Thread.sleep(500)
squeezingLevel++
}
step++
},
)
4 -> StandardContent(
StandardScreenCompounder(
"screen3",
stringResource(id = R.string.text_4, stringResource(id = chosenFruit.name)),
chosenFruit,
R.drawable.full_glass,
R.string.pic_desc_3
),
{ step++ },
{},
)
5 -> StandardContent(
StandardScreenCompounder(
"screen4",
stringResource(id = R.string.text_5),
chosenFruit,
R.drawable.empty_glass,
R.string.pic_desc_4
),
{
step = 0
collectedFruits = 0
squeezingLevel = 0
selectedFruitColorStyle = R.style.DefaultFruitColor
},
{},
)
else -> StandardContent(
StandardScreenCompounder(
"screen0",
stringResource(id = R.string.text_0),
chosenFruit,
R.drawable.temperature,
R.string.pic_desc_0
),
{ step++ },
{},
)
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StandardContent(
compoundedComponent: StandardScreenCompounder,
onClick: () -> Unit,
onHold: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
) {
Image(
painter = painterResource(compoundedComponent.imageBackground),
contentDescription = stringResource(id = compoundedComponent.imageDescription),
modifier = Modifier
.align(Alignment.Center)
.combinedClickable(
onClick = { onClick() },
onLongClick = { onHold() },
)
//.clickable { onClick() }
.background(
Color(0xFFC0E4F5),
shape = RoundedCornerShape(size = 20.dp)
)
.height(300.dp)
.width(300.dp)
)
Text(
text = compoundedComponent.screenText,
fontSize = 24.sp,
color = Color.Black,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@Composable
fun OverlappedFruitContent(
compoundedComponent: StandardScreenCompounder,
onClick: () -> Unit,
collectedFruits: Int,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
) {
Image(
painter = painterResource(compoundedComponent.imageBackground),
contentDescription = stringResource(id = compoundedComponent.imageDescription),
modifier = Modifier
.align(Alignment.Center)
.background(
Color(0xFFC0E4F5),
shape = RoundedCornerShape(size = 20.dp)
)
.height(300.dp)
.width(300.dp)
)
for (i in collectedFruits..4) {
Image(
painter = painterResource(compoundedComponent.fruit.pic),
contentDescription = stringResource(id = compoundedComponent.fruit.name),
modifier = Modifier
.align(Alignment.TopStart)
.offset(x = (nextInt(100, 250)).dp, y = (nextInt(250, 325)).dp)
.clickable {
onClick()
}
.height(50.dp)
.width(50.dp)
)
}
Text(
text = compoundedComponent.screenText,
fontSize = 24.sp,
color = Color.Black,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@Composable
fun FruitChooser(
fruits: List<Fruit>,
screenText: String,
onFruitChosen: (Fruit) -> Unit,
onNextStep: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
fruits.forEach { fruit ->
Row (
modifier = Modifier
.padding(16.dp)
) {
Image(
painter = painterResource(fruit.pic),
contentDescription = stringResource(fruit.name),
modifier = Modifier
.align(Alignment.CenterVertically)
.clickable {
onFruitChosen(fruit)
onNextStep()
}
.background(
Color(0xFFCEF0CF),
shape = RoundedCornerShape(size = 20.dp)
)
.height(50.dp)
.width(50.dp)
)
Spacer(
modifier = Modifier
.width(16.dp)
)
Text(
text = stringResource(fruit.name),
fontSize = 18.sp,
color = Color.Black,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
modifier = Modifier
.align(Alignment.CenterVertically)
.clickable {
onFruitChosen(fruit)
onNextStep()
}
)
}
}
}
Text(
text = screenText,
fontSize = 24.sp,
color = Color.Black,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
Theme.kt
package com.example.app.juicer.ui.theme
import android.app.Activity
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@RequiresApi(Build.VERSION_CODES.S)
@Composable
fun JuicerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
selectedStyle: Int,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val activity = context as? Activity
val colors = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
SideEffect {
activity?.setTheme(selectedStyle)
}
// DisposableEffect(selectedStyle) {
// activity?.setTheme(selectedStyle)
// onDispose { }
// }
Log.i("JuicerTheme", "Selected style: $selectedStyle")
MaterialTheme(
colorScheme = colors,
typography = Typography,
content = content
)
}
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FruitColor">
<attr name="selectedFruitColor" format="color" />
</declare-styleable>
</resources>
res/values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Juicer" parent="android:Theme.Material.Light.NoActionBar" />
<style name="AppleColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#DC738A</item>
</style>
<style name="BananaColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#FFCE6C</item>
</style>
<style name="CloverColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#49B86E</item>
</style>
<style name="GrapeColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#67529d</item>
</style>
<style name="LemonColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#fcc219</item>
</style>
<style name="PineappleColor" parent="DefaultFruitColor">
<item name="selectedFruitColor">#FFCC4D</item>
</style>
<style name="DefaultFruitColor">
<item name="selectedFruitColor">#BDFDFF</item>
</style>
</resources>
Other files...
blender.xml (SVG file)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
...
<path
android:pathData="M160.46,279.26h48.99v-38.79c0,-12.85 10.42,-23.27 23.27,-23.27S256,227.62 256,240.48v38.79h48.99l25.86,-232.72H134.6L160.46,279.26z"
android:fillColor="?attr/selectedFruitColor"/>
...
<path
android:pathData="M209.45,279.26h-48.99l-25.86,-232.72h-23.42H87.77l28.4,255.57l0.31,2.78l0,0.05l0.02,0.16c0.04,0.38 0.12,0.74 0.18,1.11c0.12,0.75 0.27,1.49 0.46,2.21c0.08,0.32 0.15,0.64 0.25,0.96c0.1,0.33 0.23,0.65 0.35,0.97c0.14,0.37 0.26,0.75 0.41,1.12c0.12,0.29 0.27,0.58 0.41,0.87c0.17,0.38 0.35,0.76 0.54,1.13c0.14,0.27 0.31,0.53 0.46,0.79c0.22,0.37 0.43,0.73 0.66,1.09c0.17,0.25 0.35,0.5 0.53,0.74c0.25,0.34 0.49,0.68 0.76,1.01c0.19,0.24 0.4,0.47 0.6,0.7c0.27,0.31 0.55,0.62 0.83,0.92c0.22,0.23 0.45,0.44 0.68,0.66c0.29,0.28 0.59,0.55 0.89,0.81c0.25,0.21 0.5,0.42 0.76,0.62c0.31,0.24 0.62,0.47 0.94,0.7c0.28,0.2 0.56,0.39 0.84,0.57c0.32,0.2 0.64,0.4 0.97,0.59c0.3,0.17 0.61,0.35 0.91,0.51c0.33,0.17 0.68,0.33 1.02,0.49c0.32,0.15 0.64,0.29 0.97,0.43c0.35,0.14 0.72,0.27 1.08,0.39c0.33,0.12 0.66,0.23 0.99,0.34c0.39,0.12 0.79,0.21 1.18,0.3c0.32,0.08 0.64,0.16 0.96,0.23c0.37,0.07 0.74,0.12 1.11,0.17c0.1,0.01 0.19,0.03 0.29,0.04c0.28,0.04 0.55,0.09 0.82,0.12c0.75,0.07 1.52,0.12 2.29,0.12h20.09h73.01v-46.55h-23.28V279.26z"
android:fillColor="#8CB7E8"/>
</vector>
full_glass.xml (SVG file)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="160dp"
android:height="200dp"
android:viewportWidth="160"
android:viewportHeight="200">
<path
android:pathData="M93.732,189.375H33.21a3.051,3.051 0,0 1,-3.006 -2.528L5.949,47.256a3.05,3.05 0,0 1,3.006 -3.572H117.987a3.05,3.05 0,0 1,3.005 3.572L96.737,186.847A3.05,3.05 0,0 1,93.732 189.375Z"
android:fillColor="#69cdd8"/>
<path
android:pathData="M93.732,189.375H33.21a3.051,3.051 0,0 1,-3.006 -2.528L8.06,59.4l6.883,-1.652a71.493,71.493 0,0 1,21.19 -1.88A108.184,108.184 0,0 1,56.993 59.5l18.68,5.757A71.9,71.9 0,0 0,92.157 68.3a56.33,56.33 0,0 0,8.061 -0.062,42.446 42.446,0 0,0 5.945,-0.9 33.627,33.627 0,0 0,4.413 -1.326l7.653,-2.851L96.737,186.847A3.05,3.05 0,0 1,93.732 189.375Z"
android:fillColor="?attr/selectedFruitColor"/>
...
</vector>
res/values/strings/strings.xml
<resources>
<string name="app_name">Juice Juice Juice</string>
<string name="text_0">Thirsty? Would you like to drink something?</string>
<string name="text_1">Select the fruit for preparing your juice</string>
<string name="text_2">Tap the tree to collect your %s</string>
<string name="text_3">Blend your %s</string>
<string name="text_4">Tap the %s juice to drink it</string>
<string name="pic_desc_0">A very warm day</string>
<string name="pic_desc_1">A fruit tree</string>
<string name="pic_desc_2">A fruit to squeeze</string>
<string name="pic_desc_3">A juice to enjoy</string>
<string name="pic_desc_4">An empty glass to refill</string>
<string name="text_5">Tap the empty glass to start again</string>
<string name="apple_prl">apples</string>
<string name="banana_prl">bananas</string>
<string name="grape_prl">grapes</string>
<string name="lemon_prl">lemons</string>
<string name="pineapple_prl">pineapples</string>
<string name="clover_prl">clovers</string>
<string name="apple_sgl">apple</string>
<string name="banana_sgl">banana</string>
<string name="grape_sgl">grape</string>
<string name="lemon_sgl">lemon</string>
<string name="pineapple_sgl">pineapple</string>
<string name="clover_sgl">clover</string>
</resources>
Hoping the theme and colors to be changed everytime I select a new fruit, ...
All of them with the same result: In the fourth or fifth round the color shown is the same as in the round before.
I've debugged the app and I can see there's a recomposition every time. The right style also seems to be chosen - at least I see that in the log entry that I produce in the Theme.kt but the color don't get replaced. Whether Android forgets the color of the style that should be displayed and take the last one used or the attribute (attrs.xml and SVG file) caches the color unexpectedly.
Upvotes: 2
Views: 24