DEV Community

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

Posted on • Edited 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yaymwf0h8ble7tiqmsn6.png)


- Full GitHub code found [HERE](https://github.com/thePlebDev/CalfTracker/blob/95fa3382ce28ee75e3a5223af1338eb0b101c9a8/app/src/main/java/com/elliottsoftware/calftracker/presentation/components/main/MainView.kt#L726) and implementation below:
Enter fullscreen mode Exit fullscreen mode

@Composable
fun CustomTopBar(chipTextList:List,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:
Enter fullscreen mode Exit fullscreen mode

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 :
Enter fullscreen mode Exit fullscreen mode

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

- 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:
Enter fullscreen mode Exit fullscreen mode

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](https://twitter.com/AndroidTristan).
Enter fullscreen mode Exit fullscreen mode

Top comments (0)