Jetpack Compose Smart Recomposition - android

I'm doing experiments to comprehend recomposition and smart recomposition and made a sample
Sorry for the colors, they are generated with Random.nextIn() to observe recomposition visually, setting colors has no effect on recomposition, tried without changing colors either.
What's in gif is composed of three parts
Sample1
#Composable
private fun Sample1() {
Column(
modifier = Modifier
.background(getRandomColor())
.fillMaxWidth()
.padding(4.dp)
) {
var counter by remember { mutableStateOf(0) }
Text("Sample1", color = getRandomColor())
Button(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
counter++
}) {
Text("Counter: $counter", color = getRandomColor())
}
}
}
I have no questions here since smart composition works as expected, Text on top is not reading changes in counter so recomposition only occurs for Text inside Button.
Sample2
#Composable
private fun Sample2() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample2", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("๐Ÿ”ฅ Button1๏ธ")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("๐Ÿ Button 2๏ธ")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column(
modifier = Modifier.background(getRandomColor())
) {
println("๐Ÿš€ Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("โœ… Button 3๏ธ")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Column() {
println("โ˜•๏ธ Bottom Column")
Text(
text = "Sample2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
It also works as expected each mutableState is updating only the scope they have been observed in. Only Text that observes update2 and update3 is changed when either of these mutableStates are updated.
Sample3
#Composable
private fun Sample3() {
Column(
modifier = Modifier.background(getRandomColor())
) {
var update1 by remember { mutableStateOf(0) }
var update2 by remember { mutableStateOf(0) }
println("ROOT")
Text("Sample3", color = getRandomColor())
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = {
update1++
},
shape = RoundedCornerShape(5.dp)
) {
println("๐Ÿ”ฅ Button1๏ธ")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update2++ },
shape = RoundedCornerShape(5.dp)
) {
println("๐Ÿ Button 2๏ธ")
Text(
text = "Update2: $update2",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
Column {
println("๐Ÿš€ Inner Column")
var update3 by remember { mutableStateOf(0) }
Button(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 2.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),
onClick = { update3++ },
shape = RoundedCornerShape(5.dp)
) {
println("โœ… Button 3๏ธ")
Text(
text = "Update2: $update2, Update3: $update3",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
// ๐Ÿ”ฅ๐Ÿ”ฅ Reading update1 causes entire composable to recompose
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ˜•๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
}
Only difference between Sample2 and Sample3 is Text at the bottom is reading update1 mutableState which causing entire composable to be recomposed. As you can see in gif changing update1 recomposes or changes entire color schema for Sample3.
What's the reason for recomposing entire composable?
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ˜•๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}

To have smart recomposition scopes play a pivotal role. You can check Vinay Gaba's What is โ€œdonut-hole skippingโ€ in Jetpack Compose? article.
Leland Richardson explains in this tweet as
The part that is "donut hole skipping" is the fact that a new lambda
being passed into a composable (ie Button) can recompose without
recompiling the rest of it. The fact that the lambda are recompose
scopes are necessary for you to be able to do this, but not
sufficient
In other words, composable lambda are "special" :)
We wanted to do this for a long time but thought it was too
complicated until #chuckjaz had the brilliant realization that if the
lambdas were state objects, and invokes were reads, then this is
exactly the result
You can also check other answers about smart recomposition here, and here.
https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit. Column, Row and Box are inline functions and because of that they don't create scopes.
Created RandomColorColumn that take other Composables and its scope content: #Composable () -> Unit
#Composable
fun RandomColorColumn(content: #Composable () -> Unit) {
Column(
modifier = Modifier
.padding(4.dp)
.shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
.background(getRandomColor())
.padding(4.dp)
) {
content()
}
}
And replaced
Column(
modifier = Modifier.background(getRandomColor())
) {
println("โ˜•๏ธ Bottom Column")
Text(
text = "Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
with
RandomColorColumn() {
println("โ˜•๏ธ Bottom Column")
/*
๐Ÿ”ฅ๐Ÿ”ฅ Observing update(mutableState) does NOT causes entire composable to recompose
*/
Text(
text = "๐Ÿ”ฅ Update1: $update1",
textAlign = TextAlign.Center,
color = getRandomColor()
)
}
}
Only this scope gets updated as expected and we have smart recomposition.
What causes Text, or any Composable, inside Column to not have a scope, thus being recomposed when a mutableState value changes is Column having inline keyword in function signature.
#Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: #Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
If you add inline to RandomColorColumn function signature you will see that it causes whole Composable to recompose.
Compose uses call sites defined as
The call site is the source code location in which a composable is
called. This influences its place in Composition, and therefore, the
UI tree.
If during a recomposition a composable calls different composables
than it did during the previous composition, Compose will identify
which composables were called or not called and for the composables
that were called in both compositions, Compose will avoid recomposing
them if their inputs haven't changed.
Consider the following example:
#Composable
fun LoginScreen(showError: Boolean) {
if (showError) {
LoginError()
}
LoginInput() // This call site affects where LoginInput is placed in Composition
}
#Composable
fun LoginInput() { /* ... */ }
Call site of a Composable function affects smart recomposition, and having inline keyword in a Composable sets its child Composables call site same level, not one level below.
For anyone interested here is the github repo to play/test recomposition

Related

Android Text is too wide when it contains long words

I want to display a text with an icon left to the text. The text and icon should be centered horizontally. Here is a composable function for this:
Column(Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
// imagine this Box is an icon
Box(
Modifier
.size(48.dp)
.background(Color.Red)
)
Spacer(Modifier.width(8.dp))
Text(
text = "text ".repeat(3),
textAlign = TextAlign.Center,
)
}
}
It works fine with short words:
But adding long words to the text makes it too wide, and it seems that there is too much space between the icon and the text:
I've tried to add Modifier.width(IntrinsicSize.Min) to the text, and it actually solves the issue with long words:
But it breaks displaying short words:
I don't know how to make work both long and short words. Hope to get help here.
UPD:
The same result is for Android native views. Gist with xmls.
You can consider this one, all of the codes below are copy-and-paste-able.
#Composable
fun MyScreen() {
var text by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
modifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = { text = it}
)
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier.wrapContentSize()
) {
Spacer(modifier = Modifier.weight(.5f))
SomeComposable(text = text, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.weight(.5f))
}
}
}
#Composable
fun SomeComposable(
text: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
Box(
Modifier
.align(Alignment.CenterVertically)
.size(48.dp)
.background(Color.Red)
)
Spacer(Modifier.width(8.dp))
Text(
modifier = Modifier.width(IntrinsicSize.Min),
text = text,
textAlign = TextAlign.Center,
)
}
}
I just put a Spacer between the components and weighted them accordingly.
Output:
Solution proposed by my colleague.
#Composable
fun FlowText(text: String) {
FlowRow(
mainAxisSpacing = 4.dp,
mainAxisAlignment = MainAxisAlignment.Center,
crossAxisAlignment = FlowCrossAxisAlignment.Center,
) {
text.splitToSequence(' ').filter { it.isNotEmpty() }.forEach {
Text(text = it, textAlign = TextAlign.Center)
}
}
}
Demo: https://youtu.be/WXqvxlsJ3xM

jetpack compose removing elements from list of text fields

.
I want my code to remove elements from list of text fields properly.
Each element has an X button to remove it's text field.
If I start removing elements from the bottom it works but it doesn't work for removing random elements
I want to use forEachIndexed for displaing the list
Please help me with solving this problem. I've been trying to do this for some time but every trial is unsuccessful.
This is a piece of code that I've managed to write but removing elements doesn't work properly
val listOfWords = mutableStateListOf<String>()
#Composable
fun Main() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Words",
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 4.dp),
style = MaterialTheme.typography.h6
)
listOfWords.forEachIndexed { index, word ->
Input(word, 30, "Word", 1,
{newWord ->
listOfWords[index] = newWord
Log.d("text ",word)
},
{
listOfWords.removeAt(index)
}
)
}
IconButton(
onClick = {
listOfWords.add("")
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add"
)
}
}
}
#Composable
fun Input(
word: String,
maxChar: Int,
label: String,
maxLines: Int,
onEdit: (word: String) -> (Unit),
onRemove: () -> (Unit)
) {
var text by remember { mutableStateOf(word) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 0.dp, 8.dp, 0.dp)
) {
OutlinedTextField(
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
onEdit(text)
},
modifier = Modifier.fillMaxWidth(),
label = { Text(label) },
leadingIcon = {
Icon(Icons.Default.Edit, null)
},
trailingIcon = {
IconButton(onClick = {
onRemove()
}) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Back"
)
}
},
maxLines = maxLines
)
Text(
text = "${text.length} / $maxChar",
textAlign = TextAlign.End,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp)
)
}
}
The problem is here.
var text by remember { mutableStateOf(word) }
Without supplying a key to Input's remember, compose will not be able refresh/update your remaining Input's states during Main's re-composition every time an Input is removed.
You can use the word parameter as key for remember to re-calculate every composition pass (i.e when you add/remove or typed a value in the TextField), and your code should probably work as you expected.
var text by remember(word) { mutableStateOf(word) }
Have you tried doing the following instead?
listOfWords.forEachIndexed { index, word ->
... // rest of code
{
listOfWords.removeAt(index)
}

Passing Modifier.align as a parameter

I have a Composable as follows:
#Composable
private fun MoviePosterWithRating(movie: MovieModel) {
Box {
Image(<...>)
Box( //Rating circle
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(end = 8.dp, top = 220.dp)
.size(48.dp)
.background(Color.Black, shape = CircleShape)
.align(Alignment.TopEnd)
) {
CircularProgressIndicator(
progress = movie.score / 10,
color = percentageCircleColor(movie.score),
strokeWidth = 2.dp
)
Text(
text = "${movie.score.asPercentage()}%",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 13.sp,
modifier = Modifier.padding(4.dp)
)
}
}
I would like to extract the rating circle into it's own method so I can reuse it. However, I can't because of the align on modifier. I could pass the whole modifier in as a parameter, but I would just be passing the same padding, size and background colour every time. Is there a way that I could just pass in the .align part of the modifier?
The way you should do this is to have your composable accept a Modifier as parameter, that way you can pass it at the calling point, making your composable more flexible:
#Composable
fun RatingCircle(
modifier: Modifier = Modifier,
// other attributes
) {
Box(
modifier = modifier,
) {
// other composables
}
}
Then you call it like so
Box {
Image(<...>)
RatingCircle(
modifier = Modifier.align(/* alignment */)
)
}

How do I create a Jetpack Compose Column where a middle child is scrollable but all of the other children are always visible?

I am creating a Jetpack Compose Dialog that contains a Column where all of the elements should always be visible, except for the third element, which is a Text that should be scrollable if the text doesn't fit the screen space. I almost achieved this with a secondary scrollable Column just for that Text element. However, this implementation pushes the bottom child (a button) out of view if there is a lot of text. Here is my code:
#Composable
fun WelcomeView(
viewModel: WelcomeViewModel,
onDismiss: () -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(Spacing.extraLarge))
.background(Colors.backgroundBase)
.padding(all = Spacing.medium)
) {
Column {
IconView(
name = IconViewNames.RUNNING_SHOE,
size = IconViewSizes.LARGE,
color = Colors.primaryBase
)
Text(
viewModel.title,
style = Text.themeBillboard,
modifier = Modifier.padding(bottom = Spacing.medium)
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Text(
viewModel.message,
style = Text.themeHeadline,
modifier = Modifier.padding(bottom = Spacing.medium)
)
}
Button(
onClick = onDismiss,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(Spacing.medium))
.background(Colors.primaryBase)
) {
Text(
"Continue",
style = Text.themeHeadline.copy(color = textExtraLight),
modifier = Modifier.padding(all = Spacing.extraSmall)
)
}
}
}
}
}
#Preview
#Composable
fun PreviewWelcomeView() {
WelcomeView(
viewModel = WelcomeViewModel(
firstName = "Wendy",
htmlWelcomeMessage = PreviewTextFixtures.threeParagraphs
),
onDismiss = {}
)
}
This is what it (correctly) looks like when there is only one paragraph of text:
But this is what it looks like when there are three paragraphs. The text scrolls correctly, but notice the missing "Continue" button:
Use this for your middle (scrollable) composable
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(weight =1f, fill = false)
) {
Text("Your text here")
}
The key is to use fill = false.
Just apply to the scrollable Column the weight modifier.
Something like:
Column (verticalArrangement= Arrangement.SpaceBetween) {
Text(
"viewModel.title",
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f, false)
) {
Text("...")
}
Button()
}

Issue with defining weight to views in Jetpack compose

I want to make a simple jetpack compose layout, using weights. below is the code
Row() {
Column(
Modifier.weight(1f).background(Blue)){
Text(text = "Weight = 1", color = Color.White)
}
Column(
Modifier.weight(2f).background(Yellow)
) {
Text(text = "Weight = 2")
}
}
which will result in a layout like this
but what if my inner views are coming from some other Composable function, which is unaware that its going to be a child of a column or a row?
Row() {
// Calling Some composable function here
}
In that case i am not able to access the Modifier.weight(..) function, because it thinks that its not in the row or column scope, because it is an independent function.
You can add the modifier as parameter in your Composable.
Something like:
#Composable
fun myCard(modifier: Modifier = Modifier, name:String){
Box(modifier){
Text(name,textAlign = TextAlign.Center,)
}
}
In this way the myCard is unaware that its going to be a child of a Column or a Row.
Then in your implementation you can use:
Row(Modifier.padding(16.dp).height(50.dp)) {
myCard(Modifier.weight(1f).background(Color.Yellow),"Weight = 1")
myCard(Modifier.weight(2f).background(Color.Red),"Weight = 2")
}
You can prefix RowScope. or ColumnScope. to your function declaration
#Composable
fun RowScope.SomeComposable() {
// weights should work in here
Column(Modifier.weight(1f)){ /**/ }
}
Row(
modifier = mainRowModifier,
verticalAlignment = rowCenterVerticallyAlign
) {
Row(
verticalAlignment = rowCenterVerticallyAlign,
horizontalArrangement = rowContentArrangementCenter,
modifier = Modifier.weight(.5f, true).wrapContentHeight()
) {
Text(
text = "971",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
Icon(
painter = painterResource(id = resIdDropDownIcon),
contentDescription = null,
modifier = Modifier.padding(start = startPaddingDropdownIcon.dp)
)
}
Divider(
color = dividerColor,
modifier = dividerModifier
)
SimpleTextField(
modifier = Modifier.weight(2f),
value = "",
placeHolder = textFieldPlaceholder,
keyboardType = KeyboardType.Phone,
singleLine = true
)
Text(
text = optionalTextTxt,
modifier = Modifier.weight(.5f, true),
style = MaterialTheme.typography.bodySmall,
fontSize = optionalTextFontSize.sp
)
}
no weight is issue to main row, then i will assign subrow weight to 0.5 and for textField weight will be 2f and last textview weight is issue to 0.5f
then Output will be looks like

Categories

Resources