I'm working through Android Developers online tutorial for 1st Android apps.
I have a C coding background, but very limited object-oriented experience.
I am trying to sort out what's going on when I call a method with "modifier." vs "Modifier.". I assume the lowercase version calls the method on the object passed to it, whereas the uppercase version is somehow a new object local to the child that called it as a parameter. The lowercase version seems to affect previous methods applied to that "parent". Here's an example of both, with observed behavior. The only difference between the 2 examples is the case (uppercase vs lowercase) of "m" in that "modifier" call in the first Text() call.
The first example is desired behavior. In the first "Text" call, I use "Modifier"(uppercase). This preserves the centered vertical arrangement of the parent column. ("OneQuadrant" function is called 4 times, image below shows the preview pane)
@Composable
fun OneQuadrant(title: String, info: String, bColor: Color, modifier: Modifier)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxHeight()
.background(bColor)
.padding(16.dp)
)
{
Text(
text = title,
fontWeight = Bold,
modifier = Modifier.padding(bottom=16.dp)
)
Text(
text = info,
textAlign = TextAlign.Justify,
)
}
}
The 2nd example is undesired behavior. The first "Text" call uses "modifier" (lowercase). This evidently overrides the centered vertical arrangement in the parent column.
@Composable
fun OneQuadrant(title: String, info: String, bColor: Color, modifier: Modifier)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxHeight()
.background(bColor)
.padding(16.dp)
)
{
Text(
text = title,
fontWeight = Bold,
modifier = modifier.padding(bottom=16.dp)
)
Text(
text = info,
textAlign = TextAlign.Justify,
)
}
}
Q1: In the 2nd example, even if the lowercase version is acting on the parent, why would that undo the centered vertical arrangement? I see that bottom padding and centered are inconsistent, and probably the last call prevails. However, the Column also calls for padding after the Arrangement.Center statement, and that works OK.
Q2: If it is mapping back to the parent, why isn't the call within "Column" also mapping back to its parent, since it was passed as a parameter?
Q3: What is the right mental model here, to understand what's going on? I think I'm confused in part by what the underlying objects are. The composable functions have names that suggest objects, but they are really functions acting on some other object(s)... true?
I did read up on Kotlin "companion objects", but that didn't really help. I'm guessing this is somehow related to pass-by-reference behavior in Kotlin, but I cannot figure it out. Can you please explain, or refer me to a clear reference? Thank you!
In case it is useful, here is the function that calls OneQuadrant 4 times.
fun DisplayQuadrants(modifier: Modifier = Modifier) {
Column (modifier.fillMaxWidth()) {
Row (modifier.weight(1f)){
OneQuadrant(
title = stringResource(R.string.text_composable),
info = stringResource(R.string.displays_text),
bColor = Color(0xFFEADDFF),
modifier = modifier.weight(1f)
)
OneQuadrant(
title = stringResource(R.string.image_composable),
info = stringResource(R.string.creates_composable),
bColor = Color(0xFFD0BCFF),
modifier = modifier.weight(1f)
)
}
Row (modifier.weight(1f)){
OneQuadrant (
title = stringResource(R.string.row_composable),
info = stringResource(R.string.a_layout),
bColor = Color(0xFFD0BCFF),
modifier = modifier.weight(1f)
)
OneQuadrant(
title = stringResource(R.string.column_composable),
info = stringResource(R.string.vertical),
bColor = Color(0xFFF6EDFF),
modifier = modifier.weight(1f)
)
}
}
}
This "DisplayQuadrants" is called in MainActivity as follows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QuadrantsTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
DisplayQuadrants(modifier = Modifier)
}
}
}
}
}
To answer your questions, let's have a look at how Modifiers work internally.
According to the documentation, Modifiers basically are immutable lists:
Multiple modifiers can be chained together to decorate or augment a composable. This chain is created via the Modifier interface which represents an ordered, immutable list of single Modifier.Elements.
When we execute the code
1 val aModifier = Modifier.fillMaxHeight() // = [fillMaxHeight]
2 val bModifier = aModifier.wrapContentWidth() // = [fillMaxHeight, wrapContentWidth]
3 val cModifier = aModifier.fillMaxWidth() // = [fillMaxHeight, fillMaxWidth]
4 // aModifier = [fillMaxHeight]
then
aModifier
itself will not be modified in lines 2 and 3bModifier
will receive a new Modifier instance that has all Modifier.Elements copied from aModifier
and additionally the Modifier wrapContentWidth()
cModifier
will receive a new Modifier instance that has all Modifier.Elements copied from aModifier
and additionally the Modifier fillMaxWidth()
So in general, there will be no side effects within a Composable if you used the same aModifier
instance multiple times like this:
val aModifier = Modifier.fillMaxWidth()
Text(
modifier = aModifier.background(Color.Red) // = [fillMaxWidth, background]
text = "Text with red background"
)
Text(
modifier = aModifier.border(width = 2.dp, color = Color.Green) // = [fillMaxWidth, border]
text = "Text with green border"
)
The first Text will have fillMaxWidth
and background
Modifier.Elements.
The second Text will have fillMaxWidth
and border
Modifier.Elements.
The first Text does not affect the second Text, as aModifier
itself is never modified.
Now let's analyze your code.
Within your OneQuadrant
Composable, everything is fine. The Modifiers used there will not have side effects on each other.
The problematic part happens in the DisplayQuadrants
Composable:
OneQuadrant(
// ...
modifier = modifier.weight(1f)
)
You pass in a new Modifier instance that has the weight
Modifier.Element. Let's see how this affects the Composables within OneQuadrant
:
fun OneQuadrant(title: String, info: String, bColor: Color, modifier: Modifier)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier // = [weight, fillMaxHeight, background, padding]
.fillMaxHeight()
.background(bColor)
.padding(16.dp)
)
{
Text(
text = title,
fontWeight = Bold,
modifier = modifier.padding(bottom=16.dp) // = [weight, padding]
)
Text(
text = info,
textAlign = TextAlign.Justify,
)
}
}
As you can see, the title Text suddenly has a weight
and a padding
Modifier.Element.
The weight
Modifier makes the Text fill the maximum remaining height within the Column.
As a result, the verticalArrangement
does not look correct anymore, even though it technically still works as it should.
When you instead do
Text(
text = title,
fontWeight = Bold,
modifier = Modifier.padding(bottom=16.dp) // = [padding]
)
then there will be no weight
Modifier.Element present, and thus the Text Composable wraps its Text.
Notes
What the official documentation says about passing in a Modifier into a Composable:
It's a best practice to have all of your composables accept a modifier parameter, and pass that modifier to its first child that emits UI. Doing so makes your code more reusable and makes its behavior more predictable and intuitive.
So it is recommended to allow a Modifier parameter to each of your own Composables and then apply that Modifier to the top-level child Composable:
@Composable
MyComposable(modifier: Modifier = Modifier) { // use Modifier as default value if no modifier explicitly provided
Column(
modifier = modifier.background(Color.Green) // apply passed in Modifier here
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Hello World"
)
}
}
Now, if you want to apply a certain Modifier to that Composable, you call it like this:
MyComposable(
modifier = Modifier.fillMaxSize()
)
If you don't need to pass in a Modifier, you can just call
MyComposable()
and then Kotlin will use the default value for the modifier
parameter, which is an empty Modifier
.