Reputation: 273
I'm trying to structure a simple application in a way that makes it easier to refactor by importing all values into a composable using data classes.
All the values are passed through successfully. However, whenever I try to add functionally into the composable it doesn't work despite showing no errors.
Here is the file that contains the main activity and the composable Test1
.
It displays a text that says "Hello World" with a red font and a blue button underneath it.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Test1(
Test1Properties(
textProperties = TextProperties(),
buttonProperties = ButtonProperties(),
),
Test1Functions(),
)
}
}
}
@Composable
fun Test1(
test1Properties: Test1Properties,
test1Functions: Test1Functions,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize(),
) {
Text(
text = test1Properties.textProperties.text,
color = test1Properties.textProperties.color,
fontSize = test1Properties.textProperties.fontSize,
fontWeight = test1Properties.textProperties.fontWeight,
)
Button(
onClick = { test1Functions.changeTextPropertiesText(test1Properties) },
colors = ButtonDefaults.buttonColors(
containerColor = test1Properties.buttonProperties.containerColor
),
) {
Text(text = test1Properties.buttonProperties.text)
}
}
}
Here is the file containing all of the data classes that hold all of the values for the composable.
There are three data classes. One for the Text composable, one for the button, and one to hold them all together. There is one class to hold the functionally of the composable called Test1Functions
. It only has one function to change the color of the Text Composable to blue.
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
data class Test1Properties(
val textProperties: TextProperties,
val buttonProperties: ButtonProperties,
)
data class TextProperties(
var text: String = "Hello World",
var color: Color = Color.Red,
val fontSize: TextUnit = 30.sp,
val fontWeight: FontWeight = FontWeight.Bold,
)
data class ButtonProperties(
val onClick: () -> Unit = {},
val text: String = "Button",
val containerColor: Color = Color.Blue,
)
class Test1Functions {
fun changeTextPropertiesText(test1Properties: Test1Properties): Color {
test1Properties.textProperties.color = Color.Blue
return test1Properties.textProperties.color
}
}
Everything seems fine, no errors occurring, but when pressing the button the intended functionality does not occur. The intended functionally being turning the text in the Text composable blue.
I'm also open to suggestions if you recommend another way of structuring your app.
Upvotes: 1
Views: 478
Reputation: 70
Use State for the composable to recompose. Here is a high-level changed code that fixes the exact issue that was posted (The main idea is to use State).
@Composable
fun Test1(
test1Functions: Test1Functions
){
val test1Properties by remember {
mutableStateOf( test1Functions.test)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Text(
text = test1Properties.textProperties.value.text,
color = test1Properties.textProperties.value.color,
fontSize = test1Properties.textProperties.value.fontSize,
fontWeight = test1Properties.textProperties.value.fontWeight
)
Button(
onClick = {test1Functions.changeTextPropertiesText(test1Properties)},
colors = ButtonDefaults.buttonColors(
containerColor = test1Properties.buttonProperties.containerColor
)
) {
Text(text = test1Properties.buttonProperties.text)
}
}
}
data class Test1Properties(
val textProperties: MutableState<TextProperties>,
val buttonProperties: ButtonProperties
)
data class TextProperties(
var text: String = "Hello World",
var color: Color = Color.Red,
val fontSize: TextUnit = 30.sp,
val fontWeight: FontWeight = FontWeight.Bold
)
data class ButtonProperties(
val onClick: () -> Unit = {},
val text: String = "Button",
val containerColor: Color = Color.Blue
)
class Test1Functions {
val test = Test1Properties(
textProperties = mutableStateOf(TextProperties()),
buttonProperties = ButtonProperties()
)
fun changeTextPropertiesText(test1Properties: Test1Properties) {
test.textProperties.value = test.textProperties.value.copy(
color = Color.Blue
)
}
}
You can read more about state at https://developer.android.com/develop/ui/compose/state
You can also polish code, like using to view model, proper state management, etc.
Upvotes: 0
Reputation: 14815
There are several issues with this approach.
First, this doesn't work as expected because every changed state that your composables should react to (like setting the text color) must be observable by the Compose runtime. That only works with MutableState
objects. But before you start wrapping all your data classes' properties in a MutableState, I would advise you to reconsider this entire approach.
What I think you want to do is have one centralized location where you can define default values for your composables. So the properties shouldn't even be mutable in the first place.
But even that would still defy a central principle of how Compose works: Each composable should be self-contained´, with only the variable being passed as a parameter. That enables a better reusability and simplifies the composable. If you would want to reuse Test1
you would first need to create a complex object that you can pass, while probably most of the data wouldn't be subject to be changed.
Instead, keep the parameter types as simple as possible: You will only need a String for the text. The fontSize and fontWeight will probably never change during runtime. As for the colors: These can change for a multitude of reasons: Signifying different button states (enabled/disabled, currently being activated), switch to a dark theme, and so on. But as luck has it, there is built-in support for all of that so that you do not have to pass color values around for yourself. What you want is to customize your Material Theme instead.
And even the text string shouldn't be defined as a literal, it should be read dynamically from a String Resource, depending on the language the user of your app selected.
I would actually entirely forego the data classes and do something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Test1()
}
}
}
}
@Composable
fun Test1(
displayText: String = stringResource(R.string.hello_world),
buttonText: String = stringResource(R.string.button),
initialColor: Color = MaterialTheme.colorScheme.primary,
) {
var textColor by remember { mutableStateOf(initialColor) }
val otherTextColor = MaterialTheme.colorScheme.tertiary
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize(),
) {
Text(
text = displayText,
color = textColor,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
)
Button(
onClick = { textColor = otherTextColor },
colors = ButtonDefaults.buttonColors(containerColor = Color.Blue),
) {
Text(text = buttonText)
}
}
}
When you create a new project in Android Studio, you can use an Empty Activity as a template. It contains a predefined, customized material theme that you can copy and adapt to your likings.
Upvotes: 0