DEV Community

Cover image for TikTok like navigation with Jetpack Compose and the ModalBottomSheetLayout in Android
Tristan Elliott
Tristan Elliott

Posted on • Updated on

TikTok like navigation with Jetpack Compose and the ModalBottomSheetLayout in Android

Table of contents

  1. What we are talking about
  2. Getting started
  3. GitHub
  4. YouTube

My app on the Google Playstore

GitHub code

YouTube version

Introduction

  • This series will be an informal demonstration of any problems I face or any observations I make while developing Android apps. Each blog post in this series will be unique and separate from the others, so feel free to look around.

What we are talking about

  • If you are like me and you have spent anytime on TikTok you may have noticed that when you are on the profile page, you can click on the little hamburger icon and a little modal pops up from the bottom
  • In this tutorial we will try our best at a semi recreation of the designs.

  • TikTok's modal navigation:

TikTok's modal navigation

  • Our recreation:

The copy of TikTok's modal navigation

  • Obviously ours is not going to be a one to one recreation. However, we will be recreating the basic functionality of the little modal popping up from the bottom:

Getting started

  • In Jetpack compose there are two main components to this tutorial:

1) Scaffold
2) ModalBottomSheetLayout

Scaffold

  • As the documentation states:

Compose provides convenient layouts for combining Material Components into common screen patterns. Composables such as Scaffold provide slots for various components and other screen elements.

-But all we really need to know is the Scaffold is what gives us the nice little hamburger menu:

val bottomModalState = rememberModalBottomSheetState(
        initialValue = ModalBottomSheetValue.Hidden,
        skipHalfExpanded = true
    )

Scaffold(
        backgroundColor = MaterialTheme.colors.primary,
        topBar = {
            TopAppBar(
                title = { Text("Calf Tracker") },
                navigationIcon = {
                    IconButton(
                        onClick = {
                            scope.launch {
                                bottomModalState.show()

                            }
                        }
                    ) {
                        Icon(Icons.Filled.Menu, contentDescription = "Toggle navigation drawer")
                    }
                }
            )
        },
    ){
// This is where the ModalBottomSheetLayout is going

}

Enter fullscreen mode Exit fullscreen mode
  • We will talk more about bottomModalState in the next section.

  • To get our nice looking TopBar we going to rely on the prebuilt TopAppBar given to us by the androidx.compose.material library. All we have to do is to provide a title(Really this is optional) and the hamburger icon with:

title = { Text("Calf Tracker") },
                navigationIcon = {
                    IconButton(
                        onClick = {
                            scope.launch {
                                bottomModalState.show()

                            }
                        }
                    ) {
                       //Hamburger Icon
                        Icon(Icons.Filled.Menu, contentDescription = "Toggle navigation drawer") 
                    }
                }

Enter fullscreen mode Exit fullscreen mode
  • However, If the prebuilt TopAppBar does not meet your requirements, you can create your own TopAppBar. Here is a custom one I created myself, with a search bar and scrollable chips:
    Custom Top Bar implementation

  • Full GitHub code found HERE and implementation below:

@Composable
fun CustomTopBar(chipTextList:List<String>,searchMethod:(String)->Unit){
    Column() {
        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = MaterialTheme.colors.primary,
            elevation = 8.dp
        ) {
            Column() {


                 SearchText(searchMethod= { tagNumber -> searchMethod(tagNumber) })
                    //CHIPS GO BELOW HERE
                    LazyRow(
                        modifier= Modifier
                            .fillMaxWidth()
                            .padding(horizontal = 18.dp, vertical = 8.dp),
                        horizontalArrangement = Arrangement.spacedBy(4.dp),
                    ) {
                        items(chipTextList){
                            Chip(it)
                        }
                    }
            }


        }

    }
}

Enter fullscreen mode Exit fullscreen mode

ModalBottomSheetLayout

  • As the documentation states: Modal bottom sheets present a set of choices while blocking interaction with the rest of the screen. They are an alternative to inline menus and simple dialogs, providing additional room for content, iconography, and actions.

  • All we need to know is that it is what gives us the faded background and the popup modal. Inside the Scaffold's content lambda we put:

ModalBottomSheetLayout(
            sheetState = bottomModalState,
            sheetContent = {
                ModalContents(
                    onNavigate = onNavigate,
                    bottomModalState = bottomModalState
                )
            }

        ){
            YourComposableFunctionHere()//what the modal covers up
        }

Enter fullscreen mode Exit fullscreen mode
  • The bottomModalState is what represents the state of the modal and determines if it should be shown or not. The initial starting state of the modal is represented by :
val bottomModalState = rememberModalBottomSheetState(
        initialValue = ModalBottomSheetValue.Hidden,
        skipHalfExpanded = true
    )

Enter fullscreen mode Exit fullscreen mode
  • Through the code block above we are telling the modal its initial state is Hidden. If you are wondering what skipHalfExpanded does, why don't you set it to false and find out ;)

  • The sheetContent is what is shown to the user when the modal pops up by bottomModalState.show().

  • This can be anything you want but my implementation looks like this:

data class ModalNavigation(
    val title:String,
    val contentDescription:String,
    val navigationDestination:Int,
    val icon:ImageVector,
    val key:Int,

)
val navItems = listOf(
    ModalNavigation(
        title ="Settings",
        contentDescription = "navigate to settings",
        navigationDestination = R.id.action_subscriptionFragment_to_settingsFragment,
        icon = Icons.Default.Settings,
        key =0
    ),
    ModalNavigation(
        title ="Calves",
        contentDescription = "navigate to calves screen",
        navigationDestination = R.id.action_subscriptionFragment_to_mainFragment22,
        icon = Icons.Default.Home,
        key =1
    )
)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ModalContents(
    onNavigate: (Int) -> Unit = {},
    bottomModalState:ModalBottomSheetState

){
    val scope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(MaterialTheme.colors.primary),
        contentAlignment = Alignment.Center


    ){
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = 128.dp)
        ) {

            items(navItems) { navItem ->
                Card(
                    modifier = Modifier
                        .padding(8.dp).clickable {
                            scope.launch {
                                bottomModalState.hide()
                                onNavigate(navItem.navigationDestination)
                            }
                        },
                    backgroundColor = MaterialTheme.colors.secondary,
                    elevation = 8.dp,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                        modifier = Modifier
                            .padding(vertical = 12.dp, horizontal = 4.dp),

                        ){
                        Icon(
                            imageVector = navItem.icon,
                            contentDescription = navItem.contentDescription,
                            modifier = Modifier.size(28.dp)
                        )
                        Text(navItem.title)
                    }
                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • If you are using Compose based navigation, then the navigationDestination can be changed to your destination string and the onNavigate function will be your compose based navigation function.

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (0)