What is Scaffold in Android Jetpack Compose?

Jetpack Compose Scaffold

In this article, we will learn about Scaffold layout in Jetpack Compose with the help of an example.

Prerequisites:

What is Scaffold in Android?

Scaffold allows you to implement the basic Material Design layout structure. You can add top and bottom app bars, Navigation Drawer, and Floating Action Button. The Scaffold layout put all these things together and maintains the state and layout structure for us.

For this article, let’s make this UI:

Jetpack Compose Scaffold Example

In the above UI, the snackbar is displayed with a gray background first time. It is a problem with the recorder, not the code. You won’t see it when you run the app on your phone.

Let’s start coding.

First, create an empty Jetpack Compose project and remove the Greeting and DefaultPreview composables. Create a composable called MyScaffoldLayout outside the MainActicity and call it in the onCreate() method:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                   MyScaffoldLayout()
                }
            }
        }
    }
}

@Composable
fun MyScaffoldLayout() {

}

Create a separate composable for each component (outside the MainActivity class). It makes the code more readable.

@Composable
fun MyTopAppBar() {

}

@Composable
fun MyNavigationDrawer() {

}

@Composable
fun MySnackbar() {

}

@Composable
fun MyFAB() {

}

@Composable
fun MyBottomBar() {

}

Scaffold Layout:

Jetpack Compose provides Scaffold() composable. Here is how it looks:

@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    isFloatingActionButtonDocked: Boolean = false,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    content: @Composable (PaddingValues) -> Unit
)

It takes a lot of parameters. Don’t get overwhelmed. They are easy to understand.

modifier – To modify the layout.

scaffoldStateState of the scaffold. The default value is provided by rememberScaffoldState().

contentColor – The color of the content in the scaffold body.

content – The content of the screen. Since it is the last parameter, we can move it out of the parentheses.

We will look at the others while we are adding the corresponding components.

In our MyScaffoldLayout() composable, we need two things – scaffold state and coroutine scope.

Scaffold State:

It represents the state of the scaffold. It contains both the drawer and snackbar state. We can get the ScaffoldState object from rememberScaffoldState().

@Composable
fun rememberScaffoldState(
    drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
)

drawerState – For maintaining the drawer state. The default value is closed.

snackbarHostState – For maintaining the snackbar sate.

val scaffoldState = rememberScaffoldState()

Coroutine Scope:

Changing the drawer and snackbar state should be done in the background thread. So, we need a coroutine scope. The rememberCoroutineScope() composable returns the CoroutineScope object.

val coroutineScope = rememberCoroutineScope()

We also need Context to display the Toast message. In Jetpack Compose, we can get it from the LocalContext.current property.

val contextForToast = LocalContext.current.applicationContext

Create the Scaffold and pass the state.

@Composable
fun MyScaffoldLayout() {
    val scaffoldState = rememberScaffoldState()
    val coroutineScope = rememberCoroutineScope()
    val contextForToast = LocalContext.current.applicationContext

    Scaffold(scaffoldState = scaffoldState) {

    }
}

Let’s add the Top App Bar.

TopAppBar:

Go to our MyTopAppBar() and add a parameter for handling the navigation icon click event.

@Composable
fun MyTopAppBar(onNavigationIconClick: () -> Unit) {

}

Jetpack Compose provides TopAppBar composable.

@Composable
fun MyTopAppBar(onNavigationIconClick: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = "Scaffold Example"
            )
        },
        navigationIcon = {
            IconButton(onClick = {
                onNavigationIconClick()
            }) {
                Icon(
                    imageVector = Icons.Outlined.Menu,
                    contentDescription = "navigation"
                )
            }
        }
    )
}

For the icon, you may have to add Material Icon Dependency in the your Gradle file.

We have created the TopAppBar with the title “Scaffold Example” and set the navigationIcon. Let’s assign it to the topBar parameter of the Scaffold in the MyScaffoldLayout().

Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
        MyTopAppBar {
            // We need to handle navigation icon
            // click events here
        }
    }) {

}

Run it. You will see the top app bar.

Top App Bar

When we tap on the navigation icon, the drawer should be opened. Call the open() method by using the scaffoldState object:

scaffoldState.drawerState.open() – To open the drawer.

scaffoldState.drawerState.close() – Top close the drawer.

We should call these methods in the background thread because they are suspend functions.

Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
        MyTopAppBar {
            // open the drawer
            coroutineScope.launch {
                scaffoldState.drawerState.open()
            }
        }
    }) {

}

Next, let’s add the drawer content. In our MyNavigationDrawer(), add coroutineScope and scaffoldState parameters. We need them to close the drawer on the button click.

@Composable
fun MyNavigationDrawer(
    coroutineScope: CoroutineScope,
    scaffoldState: ScaffoldState
) {

}

Let’s create a simple layout with a text and button.

@Composable
fun MyNavigationDrawer(
    coroutineScope: CoroutineScope,
    scaffoldState: ScaffoldState
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // text
        Text(text = "Your UI Here")

        // gap between text and button
        Spacer(modifier = Modifier.height(height = 32.dp))

        // button
        Button(onClick = {
            // close the drawer
            coroutineScope.launch {
                scaffoldState.drawerState.close()
            }
        }) {
            Text(text = "Close Drawer")
        }
    }
}

Assign the composable to the drawerContent parameter of our Scaffold.

Scaffold(
    // previous parameters here
    drawerContent = {
        MyNavigationDrawer(
            coroutineScope = coroutineScope,
            scaffoldState = scaffoldState
        )
    }) {

}

Now, run the app. The navigation drawer should work without any problem.

Navigation Drawer

Scaffold offers other parameters to customize the drawer:

drawerGesturesEnabled – whether or not drawer (if set) can be interacted with via gestures.

drawerShape – The shape of the drawer sheet. See the available shapes in the Jetpack Compose.

drawerElevation – The size of the shadow below the sheet.

drawerBackgroundColor – The background color.

drawerContentColor – The color of the content to use inside the drawer sheet.

drawerScrimColor – The overlay color that covers content when the drawer is open.

Bottom App Bar:

Add a Context parameter to the MyBottomBar() (for the Toast).

@Composable
fun MyBottomBar(contextForToast: Context) {

}

Jetpack Compose provides BottomNavigation API. You can read about it here.

@Composable
fun MyBottomBar(contextForToast: Context) {
    // items list
    val bottomMenuItemsList = prepareBottomMenu()

    var selectedItem by remember {
        mutableStateOf("Home")
    }

    BottomNavigation {
        // this is a row scope
        // all items are added horizontally

        bottomMenuItemsList.forEach { menuItem ->

            // adding each item
            BottomNavigationItem(
                // initially home icon should be selected
                selected = (selectedItem == menuItem.label),
                onClick = {
                    selectedItem = menuItem.label
                    Toast.makeText(
                        contextForToast,
                        menuItem.label, Toast.LENGTH_SHORT
                    ).show()
                },
                icon = {
                    Icon(
                        imageVector = menuItem.icon,
                        contentDescription = menuItem.label
                    )
                })
        }
    }
}

private fun prepareBottomMenu(): List<BottomMenuItem> {
    val bottomMenuItemsList = arrayListOf<BottomMenuItem>()

    // add menu items
    bottomMenuItemsList.add(BottomMenuItem(label = "Home", icon = Icons.Filled.Home))
    bottomMenuItemsList.add(BottomMenuItem(label = "Profile", icon = Icons.Filled.Person))
    bottomMenuItemsList.add(BottomMenuItem(label = "Cart", icon = Icons.Filled.ShoppingCart))
    bottomMenuItemsList.add(BottomMenuItem(label = "Settings", icon = Icons.Filled.Settings))

    return bottomMenuItemsList
}

data class BottomMenuItem(val label: String, val icon: ImageVector)

Assign the MyBottomBar() to the bottomBar parameter of the Scaffold.

Scaffold(
       // previous parameters here
        bottomBar = {
            MyBottomBar(contextForToast = contextForToast)
        }) {

    }

Run the app. You will see the bottom bar.

Bottom App Bar

Floating Action Button:

Go to our MyFAB() composable and add a Context parameter.

@Composable
fun MyFAB(contextForToast: Context) {
    
}

We can add FAB with the prebuilt FloatingActionButton() composable.

@Composable
fun MyFAB(contextForToast: Context) {
    FloatingActionButton(onClick = {
        Toast.makeText(contextForToast, "FAB", Toast.LENGTH_SHORT).show()
    }) {
        Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
    }
}

Go to our Scaffold layout and add it to the floatingActionButton parameter.

Scaffold(
        // previous parameters here
        floatingActionButton = { MyFAB(contextForToast = contextForToast) }
    ) {

    }

Scaffold also offers 2 other parameters:

floatingActionButtonPosition – Position of the floating action button on the screen. Available options are FabPosition.Center and FabPosition.End.

isFloatingActionButtonDocked – Whether the FAB should overlap with the bottomBar. Ignored if there’s no bottomBar or no floatingActionButton.

Scaffold(
        // previous parameters here
        floatingActionButton = { MyFAB(contextForToast = contextForToast) },
        floatingActionButtonPosition = FabPosition.End,
        isFloatingActionButtonDocked = false
    ) {

    }

Here is how it looks:

Jetpack Compose Scaffold Floating Action Button

Snackbar:

Add snackbarHostState parameter to our MySnackbar() composable.

@Composable
fun MySnackbar(snackbarHostState: SnackbarHostState) {

}

Jetpack Compose provides Snackbar API. The problem with it is that it has no mechanism for hiding and showing it. Also, it doesn’t follow material design guidelines. If we use it, we end up writing a lot of code ourselves.

Fortunately, Jetpack Compose also provides SnackbarHost() API. It follows material guidelines and offers a lot of features. We just need to pass the Snackbar() and the API will take care of everything.

Benefits of Using the SnackbarHost:

  • It shows and dismisses the snackbar with a nice animation.
  • If you post multiple snackbars at a time, they are added to the queue and shown one after the other.
  • You can show and dismiss the snackbar using the prebuilt methods.
  • The snackbar will be aligned at the bottom of the screen.

SnackbarHost:

@Composable
fun SnackbarHost(
    hostState: SnackbarHostState,
    modifier: Modifier = Modifier,
    snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) }
)

hostState – This is the state of this component to display or hide the Snackbar accordingly. We can access it from the scaffoldState.snackbarHostState property.

modifier – Optional modifier.

snackbar() – This is the Snackbar that is displayed on the screen. Look at the argument it takes – SnackbarData. It contains information like the text to be displayed and the action button label.

Let’s look at the Snackbar():

@Composable
fun Snackbar(
    modifier: Modifier = Modifier,
    action: @Composable (() -> Unit)? = null,
    actionOnNewLine: Boolean = false,
    shape: Shape = MaterialTheme.shapes.small,
    backgroundColor: Color = SnackbarDefaults.backgroundColor,
    contentColor: Color = MaterialTheme.colors.surface,
    elevation: Dp = 6.dp,
    content: @Composable () -> Unit
) 

modifierModifier for snackbar layout.

action – An action button.

actionOnNewLine – If the action button should be put on a separate line. It is useful when you display a long text.

shape – The snackbar’s shape.

backgroundColor – The background color of the snackbar.

contentColor – The color of the snackbar’s content.

elevation – The shadow below the snackbar.

content – The content of the snackbar.

Here is our MySnackbar():

@Composable
fun MySnackbar(snackbarHostState: SnackbarHostState) {

    SnackbarHost(
        hostState = snackbarHostState,
        snackbar = { data ->
            Snackbar(
                modifier = Modifier.padding(all = 8.dp),
                action = {
                    Text(
                        modifier = Modifier.clickable {
                            snackbarHostState.currentSnackbarData?.performAction()
                        },
                        text = data.actionLabel!!,
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Bold
                    )
                }
            ) {
                Text(text = data.message)
            }
        }
    )
}

In the above code, the data contains the values related to snackbar. data.message returns the snackbar’s text and data.actionLabel returns the action button text. We will pass these values while displaying the snackbar.

Go to our Scaffold and assign MySnackbar() to the snackbarHost parameter.

Scaffold(
        // previous parameters here
        snackbarHost = { state -> MySnackbar(snackbarHostState = state) }
    ) {

    }

Let’s write the app UI. Create a button and display the snackbar on the click.

Scaffold(
    // previous parameter here
    ) {
        // rest of the app's UI
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                coroutineScope.launch {
                    val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                        message = "Hey! I'm Snackbar",
                        actionLabel = "Action",
                        duration = SnackbarDuration.Short
                    )

                    // check the result
                    when (snackbarResult) {
                        SnackbarResult.ActionPerformed -> {
                            Toast.makeText(contextForToast, "ActionPerformed", Toast.LENGTH_SHORT)
                                .show()
                        }
                        SnackbarResult.Dismissed -> {
                            Toast.makeText(contextForToast, "Dismissed", Toast.LENGTH_SHORT)
                                .show()
                        }
                    }
                }
            }) {
                Text(text = "Show Snackbar")
            }
        }
    }

In the above code, we have created a Column layout and added a button to it. In the onClick block, we have launched a coroutine because we have to show the snackbar in a background thread.

The showSnackbar() method takes 3 parameter:

message – The text to be displayed on the snackbar.

actionLabel – The action button text.

duration – The duration to control how long the snackbar will be shown. There are 3 possible values: SnackbarDuration.Short, SnackbarDuration.Long and SnackbarDuration.Indefinite (shows the snackbar until explicitly dismissed or action is clicked).

The function returns SnackbarResult. Using it, we can find if the user clicked on the action button or dismissed the snackbar.

Run the app:

Scaffold Snackbar

Tip: Look at the content lambda parameter of the Scaffold() composable. You have the access to PaddingValues. You can use it to get the height of the bottom bar and then set extra padding to your content.

Scaffold(
    
) {
    Log.i("TAG", "BottomPadding = ${it.calculateBottomPadding()}")
}

It prints:

I/TAG: BottomPadding = 56.0.dp

Here is the complete code of our UI:

import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MyScaffoldLayout()
                }
            }
        }
    }
}

@Composable
fun MyScaffoldLayout() {

    val scaffoldState = rememberScaffoldState()

    val coroutineScope = rememberCoroutineScope()

    val contextForToast = LocalContext.current.applicationContext

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            MyTopAppBar {
                // open the drawer
                coroutineScope.launch {
                    scaffoldState.drawerState.open()
                }
            }
        },
        drawerContent = {
            MyNavigationDrawer(
                coroutineScope = coroutineScope,
                scaffoldState = scaffoldState
            )
        },
        bottomBar = {
            MyBottomBar(contextForToast = contextForToast)
        },
        floatingActionButton = { MyFAB(contextForToast = contextForToast) },
        floatingActionButtonPosition = FabPosition.End,
        isFloatingActionButtonDocked = false,
        snackbarHost = { state -> MySnackbar(snackbarHostState = state) }
    ) {
        // rest of the app's UI
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                coroutineScope.launch {
                    val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                        message = "Hey! I'm Snackbar",
                        actionLabel = "Action",
                        duration = SnackbarDuration.Short
                    )

                    when (snackbarResult) {
                        SnackbarResult.ActionPerformed -> {
                            Toast.makeText(contextForToast, "ActionPerformed", Toast.LENGTH_SHORT)
                                .show()
                        }
                        SnackbarResult.Dismissed -> {
                            Toast.makeText(contextForToast, "Dismissed", Toast.LENGTH_SHORT)
                                .show()
                        }
                    }
                }
            }) {
                Text(text = "Show Snackbar")
            }
        }
    }
}

@Composable
fun MyTopAppBar(onNavigationIconClick: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = "Scaffold Example"
            )
        },
        navigationIcon = {
            IconButton(onClick = {
                onNavigationIconClick()
            }) {
                Icon(
                    imageVector = Icons.Outlined.Menu,
                    contentDescription = "navigation"
                )
            }
        }
    )
}

@Composable
fun MyNavigationDrawer(
    coroutineScope: CoroutineScope,
    scaffoldState: ScaffoldState
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // text
        Text(text = "Your UI Here")

        // gap between text and button
        Spacer(modifier = Modifier.height(height = 32.dp))

        // button
        Button(onClick = {
            // close the drawer
            coroutineScope.launch {
                scaffoldState.drawerState.close()
            }
        }) {
            Text(text = "Close Drawer")
        }
    }
}

@Composable
fun MyFAB(contextForToast: Context) {
    FloatingActionButton(onClick = {
        Toast.makeText(contextForToast, "FAB", Toast.LENGTH_SHORT).show()
    }) {
        Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
    }
}

@Composable
fun MySnackbar(snackbarHostState: SnackbarHostState) {

    SnackbarHost(
        hostState = snackbarHostState,
        snackbar = { data ->
            Snackbar(
                modifier = Modifier.padding(all = 8.dp),
                action = {
                    Text(
                        modifier = Modifier.clickable {
                            snackbarHostState.currentSnackbarData?.performAction()
                        },
                        text = data.actionLabel!!,
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Bold
                    )
                }
            ) {
                Text(text = data.message)
            }
        }
    )
}

@Composable
fun MyBottomBar(contextForToast: Context) {
    // items list
    val bottomMenuItemsList = prepareBottomMenu()

    var selectedItem by remember {
        mutableStateOf("Home")
    }

    BottomNavigation {
        // this is a row scope
        // all items are added horizontally

        bottomMenuItemsList.forEach { menuItem ->

            // adding each item
            BottomNavigationItem(
                selected = (selectedItem == menuItem.label),
                onClick = {
                    selectedItem = menuItem.label
                    Toast.makeText(
                        contextForToast,
                        menuItem.label, Toast.LENGTH_SHORT
                    ).show()
                },
                icon = {
                    Icon(
                        imageVector = menuItem.icon,
                        contentDescription = menuItem.label
                    )
                })
        }
    }
}

private fun prepareBottomMenu(): List<BottomMenuItem> {
    val bottomMenuItemsList = arrayListOf<BottomMenuItem>()

    // add menu items
    bottomMenuItemsList.add(BottomMenuItem(label = "Home", icon = Icons.Filled.Home))
    bottomMenuItemsList.add(BottomMenuItem(label = "Profile", icon = Icons.Filled.Person))
    bottomMenuItemsList.add(BottomMenuItem(label = "Cart", icon = Icons.Filled.ShoppingCart))
    bottomMenuItemsList.add(BottomMenuItem(label = "Settings", icon = Icons.Filled.Settings))

    return bottomMenuItemsList
}

data class BottomMenuItem(val label: String, val icon: ImageVector)

This is all about Scaffold layout in Jetpack Compose. I hope you have learned something new. If you have any doubts, comment below.

Leave a Comment