How to control a Scaffold's FloatingActionButton onClick from child composable?
Asked Answered
T

1

6

Is it possible to set the value of a FloatingActionButton's onClick inside of a Scaffold from a screen composable from a NavHost, which is also inside the Scaffold? Here in my example, I want to be able to change the FAB's onClick to do some calculations from a value (name) inside the screen composable, without hoisting the value to the composable where the Scaffold is located (MyActivity()).

@Composable
fun MyActivity() {
    val navController = rememberNavController()

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* set this lambda from Screen1 */ }) { ... }
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = Screen.Screen1.route,
            modifier = Modifier.padding(paddingValues)
        ) {
            composable(route = Screen.Screen1.route) {
                Screen1()
            }
            ...
        }
    }
}

@Composable
fun Screen1() {
    var name by remember { mutableStateOf(TextFieldValue("") }

    TextField(value = name, onValueChange = { name = it })

    // Set FAB onClick to do some calculations on name without hoisting the variable
    setFabOnClick { name.calculate() }
}
Tatting answered 19/2, 2022 at 15:58 Comment(0)
B
13

I'm not sure why you don't want to put this value in the top view. If you do that in your example, updating the text will not cause recomposition of MyActivity, only Screen1 - Compose is smart enough to do that.

In any case, you can create a mutable state to store the handler and update it in Screen1. I use LaunchedEffect because updating the state is a side effect and should not be done directly from the view constructor, also there is no need to do it at every recomposition.

@Composable
fun Screen1(
    setFabOnClick: (() -> Unit) -> Unit,
) {
    var name by remember { mutableStateOf(TextFieldValue("")) }

    TextField(value = name, onValueChange = {name = it})

    LaunchedEffect(Unit) {
        setFabOnClick { println("$name") }
    }
}

@Composable
fun MyActivity() {
    val navController = rememberNavController()
    val (fabOnClick, setFabOnClick) = remember { mutableStateOf<(() -> Unit)?>(null) }

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = {
                fabOnClick?.invoke()
            }) {
                Icon(Icons.Default.ReportProblem, null)
            }
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = Screen.Screen1.route,
            modifier = Modifier.padding(paddingValues)
        ) {
            composable(route = Screen.Screen1.route) {
                Screen1(setFabOnClick = setFabOnClick)
            }
        }
    }
}
Bloodyminded answered 20/2, 2022 at 3:8 Comment(8)
Thank you so much for spending your time to answer my question. Will the code inside the onClick automatically update as the value of name changes?Tatting
@Tatting why don't you check it by yourself? your state variable value will be up to date. If you'll face some data being captured, which you don't expect, you can pass value as key to LaunchedEffectBloodyminded
Okay. I'll try it later. Thank youTatting
Looks like a nice solution, thanks for sharing. About the LaunchEffect, as the reference to the function doesn't change on recomposition (except when changing screen) and doesn't do heavy/long running work, shouldn't it be fine doing this without LaunchEffect?Ane
@stefan.at.wpf not using LaunchedEffect here is against Compose principles - composables should be free of side-effects for better performance. One of the reasons is that without LaunchedEffect each time name is updated it's gonna recompose MyActivity too, but it's better to always make your composables free of side-effects, even if in the particular case it looks like it's ok to just make a callBloodyminded
Thanks, convinved by applying the general principle of side effect free :-) Was thinking too much about this specific case.Ane
ughh spent 2 hrs this morning trying to figure this out, i was getting massive re-compositions...ty for this solution!Southwestwards
Thanks for putting up a solution, but it seems to break down when I have a mix of screens that do or don't have FABs. Seems I'd need to call setFabOnClick in every screen - passing a callback or null as the case may be. Or I need to have some corresponding code in MyActivity indicating whether a screen has a FAB or not; or rely on a race-condition-prone effect that clears fabOnClick every time the current destination changes, hoping that another effect sets it back only after it is cleared. Any neat tricks to make this scenario work well?Fredericksburg

© 2022 - 2024 — McMap. All rights reserved.