https://kotlinlang.org logo
#compose
Title
# compose
t

Tobias Gronbach

03/23/2022, 10:24 AM
How can I avoid recomposition of certain areas of a screen? In my example I use an AndroidView (to show diagrams) and also I display some Dialogs depending on the ScreenState. Everytime a dialog is shown or hidden the AndroidView gets recomposed. How can I avoid it? Is it possible to scope recomposition only to specific states?
m

myanmarking

03/23/2022, 12:13 PM
post code please
in my experience, recomposition of non-common part of the screen like yours is related to non-skippable composables and or lambdas accessing non-stable state. So its hard to guess without any code
r

Ronald Wolvers

03/23/2022, 5:21 PM
@Tobias Gronbach do you display a dialog in Compose (e.g.
AlertDialog()
) or from your
AndroidView()
? If
AndroidView()
is creating the dialogs then what comes to mind is to use the compose variant. You would have to propagate state to the composable somehow though, so you could use a
StateFlow
and call
collectAsState()
on it, which will give you exact control over the recomposition. I guess that's a lot of work but that's the approach I use a lot and does get the job done so to speak.
That's if I even understood correctly 😃
And actually not so much work if you use a view model, but can lead to having a lot of state flows.
t

Tobias Gronbach

03/23/2022, 5:50 PM
Thank you for your answers and thoughts. For the code is a bit long I removed some parts.
Copy code
@Composable
fun AnalyseScreen(
    viewModel: AnalyseViewModel = hiltViewModel(),
) {

    val chartDataPackage by viewModel.chartDataPackageFlow.collectAsState()
    val screenState by viewModel.screenState.collectAsState(AnalyseScreenState())

    val menuBackgroundColor = Color(0xFF957B1B)
    val menuBarHeight = 50.dp

    var isMenuOpen by remember { mutableStateOf(false) }
    var showTimeSelectionDialog by remember { mutableStateOf(false) }

    if (showTimeSelectionDialog) {
        // ... calls Dialog 
    }

    if (screenState.showDiagramSelectionDialog) {
        //... calls Dialog
    }

    Column(
        modifier = Modifier.background(Color(0xFF423F34)),
        verticalArrangement = Arrangement.SpaceBetween
    ) {

        Row(
            modifier = Modifier.fillMaxWidth().height(200.dp)
        ) {
            Chart(chartDataPackage) // opens AndroidView
        }

        Row(modifier = Modifier
            .height(menuBarHeight)
            .fillMaxWidth()
            .background(menuBackgroundColor)
            .padding(5.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically) {

            // .... more composables
        }

        if (isMenuOpen) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(0.dp, 0.dp, 0.dp, menuBarHeight),
                contentAlignment = Alignment.BottomEnd
            ) {
                // Menu
            }
        }
    }
}
As soon as a dialog is shown / hidden or Menu is shown / hidden the Chart also gets recomposed and I don't want that.
r

Ronald Wolvers

03/23/2022, 6:33 PM
Understood! I think the reason that the chart gets recomposed is because the composition literally goes back to the point where
remember[...]()
was called and then after the
if
switches simply hits the part of the function again where the chart is drawn. If you would put the dialogs logic after drawing the chart, I'm pretty sure it won't get recomposed.
That is, as far as it's possible to put it after the chart logic
What I said could also very well be false as the dialogs overlap the chart I assume and a recomposition happens anyway, but I'd say definitely worth trying to put the logic after it
t

Tobias Gronbach

03/23/2022, 8:07 PM
Thank you for your thoughts! I put the logic behind the drawing of the chart but nothing changes. The Dialog is drawn above the chart, so maybe your guess is right. But I guess there must be some kind of a solution. The Charts are animated and each time a dialog or menu pops up the animation starts again. That's a bad user experience.
This is Chart-Composable (there the correct Chart is selected)
Copy code
@Composable
private fun <T> Chart(chartDataPackage: ChartDataPackage<T>) {
    when (chartDataPackage.diagram) {
        Diagram.OverviewChart -> {
            val stats = chartDataPackage.data as List<StatisticsData>
            StatsOverviewChart(stats.first())
        }
   
        Diagram.BarChart_XNameValence_YAmount,
        -> {
            val chartData = chartDataPackage.data as List<BarEntry>
            val meta = chartDataPackage.meta
            meta?.let {
                BarChartCompose(chartData, it)
            }
        }
   
        Diagram.PieChart_XNameValence_YAmount,
        -> {
            val meta = chartDataPackage.meta
            val chartData = chartDataPackage.data as List<PieEntry>
            meta?.let {
                PieChartCompose(chartData, meta)
            }
        }
}
This is PieChart with AndroidView as example
Copy code
@Composable
fun PieChartCompose(chartData: List<PieEntry>, meta: ChartMetaData) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            PieChart(context)
        },
        update = { pieChart ->
         
            pieChart.apply {

		// ... PieChart Settings
}}})
r

Ronald Wolvers

03/23/2022, 8:51 PM
One way you could rule out whether it's Compose recomposing as opposed to the
View
being re-drawn is putting a
LaunchedEffect()
in the
Chart()
composable and then seeing if that gets executed. If it doesn't then it must have something to do with
AndroidView()
of which I don't know the internals. How Compose decides whether or not to re-draw an old school
View
is territory probably best avoided until there's no other option, lest you would have to re-do the whole thing in Compose 😬
*executed when opening or closing a dialog
t

Tobias Gronbach

03/23/2022, 9:04 PM
Not sure if I do it right (i'm still a beginner). I added the following in the Chart-Composable:
Copy code
LaunchedEffect(chartDataPackage) {  //used chartDataPackage as Key
        println("Recomposition Launched Effect")
 }

 println("Recomposition") // added this as well outside of launchedeffect
Output is: "Recomposition Launched Effect" when diagramm opened first time "Recompostion" after show/hide of dialogs and menus If I interpret this correctly I would say it's Compose recomposing
r

Ronald Wolvers

03/23/2022, 9:16 PM
Yeah, that's what I meant by "Compose recomposing" indeed! Next I would probably put print statements up in your top level composable as well as down to where you use
AndroidView()
. Just wanted to double check too that the dialogs don't touch the state?
Also a beginner! Never not been one 😆
t

Tobias Gronbach

03/23/2022, 9:23 PM
I'm very thankful you give me ideas how to approach the problem! If I open Menu, that's the Composition-Order AnalyseScreen Recomposition (Top Level) Chart is recomposed (Chart Composable) PieChart Composition (Composable that contains AndroidView)
r

Ronald Wolvers

03/23/2022, 9:26 PM
The first print statement is before the first
collectAsState()
call?
If so, you could try passing in the `StateFlow`s as arguments instead of the view model, which is what I'm guessing @myanmarking meant by unstable arguments
t

Tobias Gronbach

03/23/2022, 9:30 PM
I also changed TopLevel Composable to :
Copy code
@Composable
fun AnalyseScreen(
    viewModel: AnalyseViewModelCompose = hiltViewModel(),
) {

    val chartDataPackage by viewModel.chartDataPackageFlow.collectAsState()

    println("AnalyseScreen Recomposition")
    Box(
        modifier = Modifier.fillMaxSize().padding(0.dp, 0.dp, 0.dp, 50.dp)
    ) {
        Chart(chartDataPackage)
    }


  .... // rest of states and composables
}
r

Ronald Wolvers

03/23/2022, 9:32 PM
And if you also put a print statement before the line that says
val chartDataPackage (...)
?
If that does not get executed then something somewhere must be changing the data package
t

Tobias Gronbach

03/23/2022, 9:34 PM
New Output: AnalyseScreen before Collection of State AnalyseScreen Recomposition Chart is recomposed PieChart Composition
r

Ronald Wolvers

03/23/2022, 9:34 PM
Weird!
Okay yeah I would put the `StateFlow`s directly as arguments!
t

Tobias Gronbach

03/23/2022, 9:38 PM
I will try that tomorrow. For today I have enough. Thank you so, so much Ronald for your thoughts and help! I'm grateful you took that time! Have a nice evening!
🌉 1
🙇‍♂️ 1
r

Ronald Wolvers

03/23/2022, 9:39 PM
And then if the print statement between the beginning of the function and the call to
collectAsState()
is happening is still called, it must be something that
AndroidView()
is doing where it recomposes the whole thing
Sounds good. My pleasure! Keep us posted 😊
t

Tobias Gronbach

03/23/2022, 9:39 PM
Will defintely do!
r

Ronald Wolvers

03/23/2022, 9:40 PM
You have a nice evening as well and thanks! 🌉
t

Tobias Gronbach

03/23/2022, 9:42 PM
@myanmarking also thank you very much for your help! I'm not sure if you can see what you need to know in the code examples i posted. If not I'm glad to provide more information! There are no other Compose-Functions called just a bunch of Standard Composables.
plus one 1
m

myanmarking

03/23/2022, 9:49 PM
Do you have any lambdas in the screen? Are the composables top level classes ?
Thse are important questikns regarding rwcomposition. But i suspect the cause is ChartDataPackage not being stable. Please check that
t

Tobias Gronbach

03/24/2022, 9:58 AM
Guys, it's finally working! It feels so good! Let me tell you what I've done and what I think now . I also read this article from Zack Klippenstein, which was claryfing and finally helped me solve the issue: https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78 First I Changed my Top-Level Composable
Copy code
@Composable
fun AnalyseScreen(
    viewModel: AnalyseViewModelCompose = hiltViewModel(),
) {
    Surface {
        println("AnalyseScreen Recomposition")
        Box(
            modifier = Modifier.fillMaxSize().padding(0.dp, 0.dp, 0.dp, 50.dp)
        ) {
            Chart(viewModel)
        }

        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.BottomEnd
        ) {
            ChartMenu(viewModel)
        }
    }
}
As you can see I took the content (a lot of nested Composables) below the Chart and added it to a seperate @Composable-Annotated-Function. I passed to both @Composable-Functions (Chart and ChartMenu) my viewModel (which could be done in other ways as well). In Zachs article he writes "Functions are a natural delimiter for re-executable chunks of code, because they already have well-defined entry and exit points.". So I thought this was a good idea and suddenly it worked as expected BUT I still didn't understand why? Because I thought the content of the ChartMenu was also before wrapped in Columns and Rows (and wondered if this aren't good entry and exit points), etc. and so I still wondered why a Menu Popup or Dialog Popup would force the whole Screen to recompose. Then again in the article Zach writes "You might be surprised to find out (and I often forget) that common layouts like Column, Row, and Box are all inline functions." and "Because the body of inline composable functions are simply copied into their call sites, such functions do not get their own recompose scopes." That got me interested and I changed the function declaration from
Copy code
fun ChartMenu(viewModel : AnalyseViewModelCompose)
to
Copy code
inline fun ChartMenu(viewModel : AnalyseViewModelCompose)
... and guess what? Right, again it didn't work! So it's really important to understand recompose scopes and realize that Column, Row, Box etc. are inline functions without own recompose scopes! I hope I got it right and I hope you could also learn something! Thanks again so much for your kind help, i really appreciate that!
🌠 1
👌 1
r

Ronald Wolvers

03/24/2022, 10:35 AM
That's awesome! Very happy to hear that it works and thanks for sharing! That inlining might come in handy 🤔
😀 1
338 Views