Search code examples
androidkotlinarchitectureandroid-jetpack-compose

Is decision-logic inside Android Composable functions a bad idea?


Regarding Android Jetpack-Compose it is common practice to put your logic-parts inside your view-model. Thats why I am using UI-States to abstract my businesslogic from what I display. But where should I put necessary "visual-decision-logic" at.

This question is intended as a generic question of architecture best-practice in android compose.

visual-decision-logic := what is to be displayed depending on UI-State

Example - allowing simple logic inside nested composables

/**
 * this is the topmost Composable which is called by the Activity
 */
@Composable
fun DashboardScreen(dashboardViewModel: DashboardViewModel = hiltViewModel()) {
    val dashboardLayoutState: DashboardLayoutState? by
    dashboardViewModel.dashboardLayoutState.collectAsStateWithLifecycle()

    val isLongpressHintDisplayed: Boolean by
    dashboardViewModel.isLongpressHintDisplayed.collectAsStateWithLifecycle(
        initialValue = false
    )

    DashboardComposable(
        isLongpressHintDisplayed,
        dashboardLayoutState
    )
}

@Composable
private fun DashboardComposable(
    isHintDisplayed: Boolean,
    dashboardLayoutState: DashboardLayoutState?
) {
    val pagerState = rememberPagerState(pageCount = { dashboardLayoutState.pages.size })
    HorizontalPager(
        state = pagerState
    ) { index ->

        // question is about this if statement/logic
        // also a longer when-statement could be here instead
        if (isHintDisplayed) {
            HintComposable()
        } else {
            DashboardPageComposable(
                pageIndex = index
            )
        }
    }
}

pro: straight forward coding, local-variables like pagerState are stored and used where needed con: nested components are not "good" testable because of "hidden" paths and may get worst in bigger compose-trees


Solution

  • It is perfectly fine to use if-else in a Composable to discern which Composables to display depending on the state. The ViewModel should encapsulate business logic as

    • connecting to data repositories
    • exposing data in state variables that can be used by Composables
    • offering functions to modify the state variables

    If you need some way to conditionally display a Composable, this can only be achieved using an if-else in a Composable. But if you instead use if-else in a Composable to modify state variables based on conditions, this would not be a good practice.

    Have a look at the Jetpack Compose Samples Repository. It is an official repository maintained by Google and thus incorporating the best practices. You will find many if-else snippets within Composables when browsing the source code:

    Conversations.kt

    if (index == messages.size - 1) {
        item {
            DayHeader("20 Aug")
        }
    } else if (index == 2) {
        item {
            DayHeader("Today")
        }
    }
    
    // ...
    
    if (isFirstMessageByAuthor) {
        // Last bubble before next author
        Spacer(modifier = Modifier.height(8.dp))
    } else {
        // Between bubbles
        Spacer(modifier = Modifier.height(4.dp))
    }
    

    ReplyAppBars.kt

    if (searchResults.isNotEmpty()) {
        LazyColumn(
            modifier = Modifier.fillMaxWidth(),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            // ...
        }
    } else if (query.isNotEmpty()) {
        Text(
            text = stringResource(id = R.string.no_item_found),
            modifier = Modifier.padding(16.dp)
        )
    } else {
        Text(
            text = stringResource(id = R.string.no_search_history),
            modifier = Modifier.padding(16.dp)
        )
    }
    

    CraneHome.kt

    When having more than three different branches, consider using the Kotlin when construct for better readability:

    when (craneScreenValues[page]) {
        CraneScreen.Fly -> {
            suggestedDestinations?.let { destinations ->
                ExploreSection(
                    // ...
                )
            }
        }
    
        CraneScreen.Sleep -> {
            ExploreSection(
                // ...
            )
        }
    
        CraneScreen.Eat -> {
            ExploreSection(
                // ...
            )
        }
    }
    

    Note that the conditions within the if and else if blocks are kept fairly simple. If you end up having very complex conditional expressions involving many different state variables from the ViewModel, then you might need to further encapsulate the state in the ViewModel by introducing a separate UIState class.