How is the paramter onScreenChange : (String) -> Unit passed to the function body : @Composable ((String) -> Unit) -> Unit?

Viewed 224

The Code A is from the main branch of the official sample project.

There are three subclass Overview, Accounts and Bills of the enum class RallyScreen in the project.

There is a function fun content(onScreenChange: (String) -> Unit) { body(onScreenChange) } which accept the paramater onScreenChange : (String) -> Unit in the class RallyScreen.

Although the statement is enum class RallyScreen(val body: @Composable ((String) -> Unit) -> Unit) {..}, the body = { OverviewBody() } in the class Overview, the body = { AccountsBody(UserData.accounts) } in the class Accounts and the body = { BillsBody(UserData.bills) } in the class Bills don't require to pass the paramter (String) -> Unit), why can Kotlin run well and navigate by Tab well when the App launch fun content(onScreenChange: (String) -> Unit) { body(onScreenChange)} ?

Code A

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                currentScreen.content(
                    onScreenChange = { screen ->
                        currentScreen = RallyScreen.valueOf(screen)
                    }
                )
            }
        }
    }
}


enum class RallyScreen(
    val icon: ImageVector,
    val body: @Composable ((String) -> Unit) -> Unit
) {
    Overview(
        icon = Icons.Filled.PieChart,
        body = { OverviewBody() }  
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
        body = { AccountsBody(UserData.accounts) } 
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
        body = { BillsBody(UserData.bills) } 
    );

    @Composable
    fun content(onScreenChange: (String) -> Unit) {
        body(onScreenChange)
    } 
}


@Composable
fun OverviewBody(
    onClickSeeAllAccounts: () -> Unit = {},
    onClickSeeAllBills: () -> Unit = {},
    onAccountClick: (String) -> Unit = {},
) {
   ...
}

@Composable
fun AccountsBody(
    accounts: List<Account>,
    onAccountClick: (String) -> Unit = {},
) {
    ...
}

@Composable
fun BillsBody(bills: List<Bill>) {
    ...
}

Added Content:

To Gabriel Pizarro: Thanks!

By the statement of clas Overview, the body needn't any parameter, it use default values, how is the onScreenChange parameter passed ?

Overview(
   icon = Icons.Filled.PieChart,
   body = { OverviewBody() }  //It needn't any parameter
)


@Composable
fun content(onScreenChange: (String) -> Unit) {
   body(onScreenChange)
} 

And more, the app can work well when I add two default parameters for fun OverviewBody(...) .

@Composable
fun OverviewBody(
    onClickSeeAllAccounts: () -> Unit = {},
    onClickSeeAllBills: () -> Unit = {},
    a1: (String) -> Unit = {},              // I add
    onAccountClick: (String) -> Unit = {},
    a2: (String) -> Unit = {},              //I add
) {
   ...
}
2 Answers

Maybe body = { OverviewBody() } should be body = { OverviewBody(onAccountClick = it) } in Overview class, right?

Actually, NO!!!

The lambda that body receives is different from OverviewBody.onAccountClick. In body's lambda, the String refers to a name of RallyScreen as you can see from its usage here. And in onAccountClick lambda, the String represents an accountName.

By the statement of class Overview, the body needn't any parameter, it use default values, how is the onScreenChange parameter passed ?

onScreenChange is not passed called anywhere and that's why navigation doesn't work.

You can make the SEE ALL buttons in Overview screen work using:

Overview(
    icon = Icons.Filled.PieChart,
    body = { 
        OverviewBody(
            onClickSeeAllAccounts = { it(Accounts.name) },
            onClickSeeAllBills = { it(Bills.name) },
        ) 
    }
)

The onAccountClick lambda in OverviewBody is tricky to handle. We need to show the SingleAccountBody composable when AccountRow is clicked. Right now there is no RallyScreen corresponding to that composable. So you will have to create a new enum value in RallyScreen something like this:

Overview(
    icon = Icons.Filled.PieChart,
    body = {
        OverviewBody(
            onClickSeeAllAccounts = { it(Accounts.name) },
            onClickSeeAllBills = { it(Bills.name) },
            onAccountClick = { accountName ->                   >>>>>>
                it(SingleAccount.name)                                |   
            }                                                         |
        )                                                             | 
    }                                                                 |
),                                                                    |
Accounts(                                                             |
    icon = Icons.Filled.AttachMoney,                                  | 
    body = {                                                          | 
        AccountsBody(UserData.accounts)                               |
    }                                                                 |
),                                                                    |
Bills(                                                                |
    icon = Icons.Filled.MoneyOff,                                     |
    body = { BillsBody(UserData.bills) }                              |
),                                                                    | 
SingleAccount(                                                        |
    icon = Icons.Filled.AttachMoney,                                  | 
    body = {                                                          |
        SingleAccountBody(UserData.getAccount(???accountName???)) <<<--
    }
);

Here we need to send that accountName received from Overview screen to SingleAccount screen to make the navigation work. But there is no simple way to do that with the current code structure and I don't want to complicate this answer much. In the final code of this codelab, this accountName has been passed using NavArguments.

Remember that each of the on variables is a callback. onScreenChange implicitly passes the variable it as String, but it is not necessary to use the variable within your callback.

  1. it is implicitly passed, but in the GitHub link you gave, the programmer decided to rename it to onScreenChange. That callback then was passed to OverviewBody, which overrides the default {}, as you said. What I don't understand about your question is where onAccountClick is coming from. That is not a parameter of OverviewBody, according to the GitHub code.

  2. content has a parameter for another callback, which you noticed. The body variable for each class in the enum is then invoked, and the callback parameter is passed to the invocation. In the future, that callback will only be used by OverviewBody. Remember that body(callback) is the same as body.invoke(callback).

I hope this helps. Please let me know if anything needs some more explanation.

For further reference:

Additional Content Edit:

  1. I see the parameters in your code, not the one on GitHub. My apologies for missing that.
  2. body is a variable function. This means that the variable function can be invoked, either by writing body(...) or body.invoke(...). The String parameter it, which is passed implicitly, is ignored and the default values are used, unless specified like you suggested (onAccountClick=it).
Related