19

I am writing an android application pure in compose and I am using scaffold in every screen to implement topBar, bottomBar, fab, etc.

My question is should I be using scaffold in every screen or just in MainActivity? what are the best practices while using composables? Can I use scaffold inside of scaffold ?

I have researched a lot but didn't find answer anywhere even jetpack compose sample apps do not provide anything about best practices to build an app in jetpack compose. please can anyone help me.

My code looks like this

MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            PasswordManagerApp()
        }
    }
}

@Composable
fun PasswordManagerApp() {


    val mainViewModel: MainViewModel = hiltViewModel()
    val navController = rememberNavController()
    val systemUiController = rememberSystemUiController()
    val scaffoldState = rememberScaffoldState()
    val coroutineScope = rememberCoroutineScope()

    Theme(
        darkTheme = mainViewModel.storedAppTheme.value
    ) {

        Scaffold(
            scaffoldState = scaffoldState,
            snackbarHost = { scaffoldState.snackbarHostState }
        ) {

            Box(modifier = Modifier) {

                AppNavGraph(
                    mainViewModel = mainViewModel,
                    navController = navController,
                    scaffoldState = scaffoldState
                )

                DefaultSnackbar(
                    snackbarHostState = scaffoldState.snackbarHostState,
                    onDismiss = { scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() },
                    modifier = Modifier.align(Alignment.BottomCenter)
                )

            }


        }
    }
}

Screen 1:

@Composable
fun LoginsScreen(
    ...
) {


    ...

    Scaffold(
        topBar = {
            HomeTopAppBar(
                topAppBarTitle = LoginsScreen.AllLogins.label,
                onMenuIconClick = {},
                switchState = viewModel.switch.value,
                onSwitchIconClick = { viewModel.setSwitch(it) },
                onSettingsIconClick = {navigateToSettings()}
            )
        },
        scaffoldState = scaffoldState,
        snackbarHost = { scaffoldState.snackbarHostState },
        floatingActionButton = {
            MyFloatingBtn(
                onClick = { navigateToNewItem() }
            )
        },
        drawerContent = {
            //MyDrawer()
        },
        bottomBar = {
            MyBottomBar(
                navController = navController,
                currentRoute = currentRoute,
                navigateToAllLogins = navigateToAllLogins,
                navigateToAllCards = navigateToAllCards,
                navigateToAllOthers = navigateToAllOthers,
            )

        },
        floatingActionButtonPosition = FabPosition.End,
        isFloatingActionButtonDocked = false,

        ) {

        Box(modifier = Modifier.fillMaxSize()) {

            Column(
                verticalArrangement = Arrangement.Top,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .fillMaxSize()
                    .padding(bottom = 48.dp)
                    .verticalScroll(scrollState)
            ) {...}


        }


    }

Screen 2:

@Composable
fun CardsScreen(
    ...
) {

    ...

    Scaffold(
        topBar = {
            HomeTopAppBar(
                topAppBarTitle = CardsScreen.AllCards.label,
                onMenuIconClick = {},
                switchState = viewModel.switch.value,
                onSwitchIconClick = { viewModel.setSwitch(it) },
                onSettingsIconClick = {navigateToSettings()}
            )
        },
        floatingActionButton = {
            MyFloatingBtn(
                onClick = { navigateToNewItem() })
        },
        drawerContent = {
            //MyDrawer()
        },
        bottomBar = {
            MyBottomBar(
                navController = navController,
                currentRoute = currentRoute,
                navigateToAllLogins = navigateToAllLogins,
                navigateToAllCards = navigateToAllCards,
                navigateToAllOthers = navigateToAllOthers,
            )
        },
        floatingActionButtonPosition = FabPosition.End,
        isFloatingActionButtonDocked = false
    ) {

        Column(
            verticalArrangement = Arrangement.Top,
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .fillMaxSize()
                .padding(bottom = 48.dp)
                .verticalScroll(scrollState)
        ) {...}


           

}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • 2
    compose samples has a lot of `Scaffold` usage examples. There's no one right way. If your bars looks the same for all screens, place for the whole app. If you need something specific in each individual screen - place it there. – Phil Dukhov Sep 05 '21 at 05:18
  • @PhilipDukhov So I can use scaffold in all screens and it will not be kind of bad practice. – Sahibjadatalib Ansari Sep 05 '21 at 05:51
  • I would say it depends on how repetitive your code is. You can put the repetitive parts in your own `AppScaffold` and only pass in parameters that are different on different screens with arguments. – Phil Dukhov Sep 05 '21 at 06:02
  • 5
    @PhilipDukhov I kind of disagree with there's no one right way. In my testing, I've found that if the app is wrapped around a scaffold (`MyApp(){ProvideWindowInsets{MyTheme{MyScaffold(topBar=...)}}}`), it does graceful transitions between screens. However if I put it in individual screens (`fun MyScreen(){MyScaffold(topBar=...)}}`), I've noticed that the top bar kind of flashes when going from screen to screen and I imagine that's because it's "rebuilding" it each time. – user1795832 Oct 12 '21 at 18:20
  • For topAppBar you can have idea from this [link](https://stackoverflow.com/questions/71417326/jetpack-compose-topappbar-with-dynamic-actions/75846287#75846287) – Watermelon Apr 25 '23 at 02:44

2 Answers2

3

Like the others in comments, i didn't find any guidelines. It looks like everything is possible :

  • Nested Scaffolds
  • One Scaffold per app
  • One Scaffold per page

At first sight, you should choose the solution that match your needs.

But...

Keep in mind that one Scaffold per app could be the "better" solution because :

  • It's the only way to keep Snackbars alive when navigating.
  • It's the easiest way to have advanced transition animation between screens. I mean controlling animation component by component the way you want during transition (top bar, bottom nav, floating action button, page content...). As well, default NavHost transition animations look better with this solution.

With that solution, you'll have to deal with having different top app bar, floating action button... per page. That's something you can solve by having a shared view state for these elements.

@Immutable
data class ScaffoldViewState(
    @StringRes val topAppBarTitle: Int? = null,
    @StringRes val fabText: Int? = null,
    // TODO : ...etc (top app bar actions, nav icon...)
)

var scaffoldViewState by remember {
    mutableStateOf(ScaffoldViewState())
}

Scaffold(
    topBar = {
        TopAppBar(
            title = {
                scaffoldViewState.topAppBarTitle?.let {
                    Text(text = stringResource(id = it))
                }
            }
        )
    },
    floatingActionButton = {
        // TODO : a bit like for topBar
    }
) {
    NavHost {
        composable("a") {
            LaunchedEffect(Unit) { 
                scaffoldViewState = // TODO : choose the top app bar and fab appearance for this page
            }

            AScreen()
        }
        composable("b") {
            LaunchedEffect(Unit) { 
                scaffoldViewState = // TODO : choose the top app bar and fab appearance for this page
            }
                        
            BScreen()
        }
    }
}
DamienL
  • 577
  • 1
  • 5
  • 14
0

As it has been said in comments both approaches are valid. Sure, you can use Scaffold inside another Scaffold like in the sample Jetnews app by Google

If you want "nice" screen transitions while only content changes, then define the component in top level Scaffold (e.g. the Drawer menu is usually shared in the top level Scaffold). Toolbars / AppBars are easier to implement per screen (inner Scaffold), not only in Jetpack Compose, because usually they have different titles, action buttons, etc.

In a simple app without dedicated Toolbars only one Scaffold could be used.

Derek K
  • 2,756
  • 1
  • 21
  • 37
  • For the record, Jetnews doesn't use nested Scaffolds and I don't remember it has ever used it. – dominik Apr 21 '23 at 18:54
  • @dominik It uses, look at the HomeScreen (link in the answer) and the containing JetnewsApp composables - both have Scaffolds. – Derek K Apr 23 '23 at 09:56
  • Ok, I see. Funny because they use nested Scaffolds in the code labs you linked, but not in the code samples here: https://github.com/android/compose-samples - in the code samples Jetnews has three distinct Scaffolds (ArticleScreen, HomeScreen, InterestsScreen) but there's no parent Scaffold in the JetnewsApp. That's why I wrote they've never used nested Scaffolds. – dominik Apr 23 '23 at 10:30