Collapsing Toolbar in Jetpack Compose | ‘LazyColumn’ version — Part 1

Glenn Sandoval
Kotlin and Kotlin for Android
16 min readMay 22, 2022

--

A collapsing toolbar mechanism built completely in Jetpack Compose.

You can go to any article of this guide by clicking on one of the links below:

Collapsing Toolbar in Jetpack Compose

  1. Problem, solutions and alternatives
  2. Codebase
  3. ‘Column’ version
  4. ‘LazyColumn’ version — Part 1
  5. ‘LazyColumn’ version — Part 2
  6. A closer look at the Toolbar — Part 1
  7. A closer look at the Toolbar — Part 2

⚠️ ATTENTION ⚠️

💬 If you want to skip descriptions, comments and explanations, follow this symbol 🔷 to go from instruction to instruction.

💬 Stop when you see this symbol 🔶 to check if you have followed the instructions correctly by comparing your code against the one I provide at that point.

💬 If you see this symbol ▶️, you can run the application to check your progress.

💬 If you still haven’t downloaded the codebase, wait no more and click on the following link:

Let’s get started

First things first. Let’s build a new composable that will work as the initial screen which joins the CollapsingToolbar and LazyCatalog composables:

🔷 Create a new package named screens inside the ui folder.
🔷 Create a new Kotlin file named Catalog.kt inside the screens package.

— Step 1 : New Screen

We need to define two heights for the collapsed and expanded states of the toolbar.

🔷 Inside Catalog.kt, declare both dimensions as follows:

Now with its height range declared, we can create the new screen.

🔷 Create a new composable function named Catalog with the following input parameters:

  • animals: List of animals used for populating the LazyCatalog.
  • columns: Number of items that will be displayed per row.
  • onPrivacyTipButtonClicked: Lambda that defines the event to be triggered when the ‘Privacy Tip’ button is clicked.
  • onSettingsButtonClicked: Lambda that defines the event to be triggered when the ‘Settings’ button is clicked.
  • modifier: Optional parameter applied to the main structure which joins the CollapsingToolbar and LazyCatalog composables.

The Catalog composable function should look like this:

Leave this function with a TODO() statement for now. Let’s make a necessary change to be able to run the application and see what’s going on when we have written new code.

🔷 Open the file MainActivity.kt and replace the code of the composable function CollapsingToolbarInComposeApp with the following:

Before we run the application, let’s add all the composables that we need in the body of the composable function Catalog. We are going to use a Box as container for the CollapsingToolbar and LazyCatalog composables.

🔷 Open the file Catalog.kt.
🔷 Copy the body of Catalog below and replace the TODO() statement with it.

We also need a NestedScrollConnection object which contains callbacks that will be called when the list dispatches scrolling events, so we can use these callbacks to make the toolbar participate in the nested scroll chain.

🔷 Add the following code before the Box block:

By implementing the onPreScroll callback, we let the toolbar be the first consuming/reacting to the scrolling event. We will finish the implementation of onPreScroll later, but for now all we need is assign the NestedScrollConnection object to the modifier of the parent composable, which in this case is the Box component.

🔷 Assign the nestedScrollConnection object to the modifier of the Box as follows:

🔗 If you need more details about how the NestedScrollConnection object works, take a look at its official documentation here.

🔶 This is what the code inside Catalog.kt should look like:

▶️ Now you can run the application. This is what we have so far:

Step 1

There are two things that must be changed to make the toolbar collapsible: its parameter progress —line 41— and its modifier property height —line 46 — . Neither should have a fixed value. For its offset, we are going to call its modifier function graphicsLayer, setting its translationY value.

Similarly, the LazyCatalog composable is receiving two fixed values: its parameter contentPadding —line 37 — and its offset through its modifier function graphicsLayer — line 35 — . These values shouldn’t be fixed since both depend not only on the toolbar state, but also on its type.

Since progress, height and offset are interrelated properties that depend on the LazyListState object, we are going to follow what the API Guidelines for Jetpack Compose suggests under the Compose API design patterns section, encapsulating all the data involved in the toolbar state management, in favor of its internal consistency.

Besides those three properties, there is one more thing that we must take into account in order to get a smooth scrolling effect. We need to consume part of the scrolling value which is received in the available parameter of onPreScroll. There are some operations that we will have to make in order to calculate how much the toolbar is going to consume, since onPreScroll is required to return that value. That way, the rest of the components involved in the nested scroll chain will behave just like we expect them to.

— Step 2 : Abstraction

We need an interface to manage the toolbar state, not only to encapsulate all the data involved, as I just mentioned, but to establish a contract about the behavior that every scroll flag implementation must provide, in order to make them interchangeable.

First, let’s organize all the code related to the toolbar state management that we are going to write, inside its own package.

🔷 Create a chain of packages that follows the structure management.states.toolbar inside the ui folder.
🔷 Create an interface named ToolbarState inside the toolbar package and add the following code:

Now every scroll flag that we decide to support will be an implementation of ToolbarState, so we can rely on this interface to set the toolbar state values accordingly. Also, since every scroll flag implementation will be in charge of holding its corresponding toolbar state, its state should be able of being restored in case of a configuration change. So, let’s create a function that not only returns a scroll flag implementation, but also preserves its internal state.

🔷 Open the file Catalog.kt and add the following code:

💬 That TODO() statement will be the last thing we are going to change after we’re done coding any scroll flag implementation. It will also be the only line of code that we will need to modify for applying any behavior that we have implemented.

This function receives an IntRange corresponding to the toolbar height range in pixels for its collapsed and expanded states. Every scroll flag implementation will require that range in order to establish the toolbar height limits internally.

🔷 Add the following code inside the composable function Catalog:

🔷 Replace the onPreScroll callback implementation with the following code:

🔷 Replace all the fixed parameters of CollpasingToolbar and LazyCatalog that we set before, with the ones provided by whichever scroll flag implementation we’re using:

Let’s leave a fixed value for the bottom padding — line 15 — for now. We’ll work on that later.

🔶 At this point, the code inside Catalog.kt should look like this:

It’s time to work on the different ToolbarState implementations.

— Step 3 : Common implementation

All four different scroll flags share some of their behavior, so we can approach their implementations encapsulating their common member variables and functions in a base abstract class.

🔷 Create a new abstract class named ScrollFlagState inside the toolbar package and make it implement the ToolbarState interface.

To establish its height range, this class should receive that information as a parameter.

🔷 Add the parameter heightRange: IntRange to its constructor:

🔷 Validate heightRange with a require statement inside an init block.

🔷 Add the following member variables for internal control:

🔷 Override its height property making use of its custom getter with the following calculation:

💬 rangeDifference = maxHeight — minHeight.

height takes a value between minHeight and maxHeight. When scrollOffset is ≥ rangeDifference, the toolbar is collapsed. When scrollOffset is 0, the toolbar is expanded.

🔷 Override its progress property making use of its custom getter with the following calculation:

💬 rangeDifference = maxHeight — minHeight.

progress takes a value between 0 and 1, which correspond to the toolbar collapsed and expanded states respectively.

🔷 Override its consumed property making use of its custom getter returning the value of _consumed which works as a backing property:

consumed corresponds to the length that the toolbar takes from the total scrolling length provided by the NestedScrollConnection object.

🔷 Override its scrollTopLimitReached property setting its value to true:

scrollTopLimitReached works as a flag that lets us know if the first item of the list is completely visible.

🔶 And that’s all for this class. Its complete code should look like this:

— Step 4 : A little more abstraction

The bottom padding that we have to add to the list depends on whether the toolbar has a fixed position or not. If it does, then a bottom padding with a size equal to its minimum height is required. Otherwise, the last elements of the list would remain completely, or at least partially, off-screen. On the other hand, if the toolbar position is not fixed, then we don’t need to add any padding to the list.

We need a way to find out if the toolbar position is fixed or not. We can do that in two different ways:

  1. Adding an immutable boolean property to the interface.
  2. Applying a little more abstraction, so we can tell if a concrete implementation is of certain type or not.

I like the latter approach better, so we need to create a new abstract class to classify any fixed toolbar that we decide to implement.

🔷 Create a new abstract class named FixedScrollFlagState inside the toolbar package and make it a subclass of ScrollFlagState:

All we need to do is set its property offset equal to 0f and make it final, so it can’t be overridden by any subclass.

Now we can replace the fixed bottom padding that we were passing in to LazyCatalog with one that depends on the ToolbarState implementation type.

🔷 Open the file Catalog.kt and replace the line that corresponds to the contentPadding parameter of LazyCatalog with the following:

🔶 This is what the code inside Catalog.kt should look like:

We finally reached the last step where we are going to code every concrete scroll flag implementation. Here is where all this abstraction makes sense, so you’ll see how easy implementing a new behavior can be.

— Step 5 : Concrete implementations

🔷 Create a new package named scrollflags inside the toolbar folder.

We are going to save every concrete scroll flag implementation inside this new package.

🔷 Continue by clicking on the scroll flag you’re interested in:

ScrollState

🔷 Create a new class named ScrollState and make it a subclass of ScrollFlagState:

🔷 Override its _scrollOffset property as follows:

_scrollOffset works as a backing property for scrollOffset.

🔷 Override its scrollOffset property as follows:

If the top of the list has been reached, _scrollOffset will be updated with a value between 0 and maxHeight. Otherwise, _scrollOffset will remain the same. Also, _consumed will be updated accordingly.

🔷 Override its offset property as follows:

💬 rangeDifference = maxHeight — minHeight.

offset takes a value between 0 and -minHeight. When scrollOffset is ≤ rangeDifference, offset is 0, which means that the toolbar is completely visible. When scrollOffset is > rangeDifference, offset takes a negative value, so the toolbar is partially or totally off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollOffset.

💬 That’s why we need to pass in the scroll offset/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire ScrollState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

Scroll behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

We’re not done yet

Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:

🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.

Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…

…or just take a look at the following image:

To make the toolbar enter the screen and expand after a fling gesture, we need to implement the onPostFling callback inside the NestedScrollConnection object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

EnterAlwaysState

🔷 Create a new class named EnterAlwaysState and make it a subclass of ScrollFlagState:

🔷 Override its _scrollOffset property as follows:

_scrollOffset will work as a backing property for scrollOffset.

🔷 Override its scrollOffset property as follows:

_scrollOffset will be updated with a value between 0 and maxHeight. _consumed will also be updated accordingly.

🔷 Override its offset property as follows:

💬 rangeDifference = maxHeight — minHeight.

offset takes a value between 0 and -minHeight. When scrollOffset is ≤ rangeDifference, offset is 0, which means that the toolbar is completely visible. When scrollOffset is > rangeDifference, offset takes a negative value, so the toolbar is partially or totally off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollOffset.

💬 That’s why we need to pass in the scroll offset/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire EnterAlwaysState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

EnterAlways behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

EnterAlwaysCollapsedState

🔷 Create a new class named EnterAlwaysCollapsedState and make it a subclass of ScrollFlagState:

🔷 Override its _scrollOffset property as follows:

_scrollOffset will work as a backing property for scrollOffset.

🔷 Override its scrollOffset property as follows:

💬 rangeDifference = maxHeight — minHeight.

If the top of the list has been reached, _scrollOffset will be updated with a value between 0 and maxHeight. Otherwise, _scrollOffset will be updated with a value between rangeDifference and maxHeight. Also, _consumed will be updated accordingly.

🔷 Override its offset property as follows:

💬 rangeDifference = maxHeight — minHeight.

offset takes a value between 0 and -minHeight. When scrollOffset is ≤ rangeDifference, the toolbar is completely visible. When scrollOffset is > rangeDifference, the toolbar is partially or totally off-screen.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollOffset.

💬 That’s why we need to pass in the scroll offset/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire EnterAlwaysCollapsedState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

EnterAlwaysCollapsed behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

We’re not done yet

Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:

🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.

Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…

…or just take a look at the following image:

To make the toolbar enter the screen and expand after a fling gesture, we need to implement the onPostFling callback inside the NestedScrollConnection object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

ExitUntilCollapsedState

🔷 Create a new class named ExitUntilCollapsedState and make it a subclass of FixedSrollFlagState:

🔷 Override its _scrollOffset property as follows:

_scrollOffset will work as a backing property for scrollOffset.

🔷 Override its scrollOffset property as follows:

💬 rangeDifference = maxHeight — minHeight.

If the top of the list has been reached, _scrollOffset will be updated with a value between 0 and rangeDifference. Otherwise, _scrollOffset will remain the same. Also, _consumed will be updated accordingly.

There’s still one thing left to do inside this class. Right now, its internal state can’t be restored in case of a configuration change. To solve this, we are going to implement a Saver. We need to save three properties: minHeight, maxHeight and scrollOffset.

💬 That’s why we need to pass in the scroll offset/position as argument to the constructor.

🔷 Add the following companion object block inside the class:

🔶 This is what the entire ExitUntilCollapsedState class should look like:

Now all we have to do is make the rememberToolbarState composable function return this scroll flag implementation wrapped in a rememberSaveable statement.

🔷 Open the file Catalog.kt and replace the TODO() statement with the following code:

This way, the toolbar state will survive recompositions. Also, by passing in a Saver implementation as argument, it will survive a configuration change.

🔶 The code of rememberToolbarState should look like this:

▶️ Now you can run the application to see the final result.

ExitUntilCollapsed behavior

Now if you are interested in another toolbar behavior, all you need to do is create a new concrete implementation and edit the return statement inside the rememberToolbarState composable function.

We’re not done yet

Although it seems like we’re done, there is a little detail that you probably didn’t notice. I want you to try one last thing:

🔷 Run the application and scroll down as much as you can.
🔷 Perform a fling gesture to scroll all the way up quickly.

Did you notice something wrong? If you didn’t, then try again, but this time pay attention to where and when it stops…

…or just take a look at the following image:

To make the toolbar expand after a fling gesture, we need to implement the onPostFling callback inside the NestedScrollConnection object. Although it seems easy and straightforward to implement, making it work properly is a little bit tricky since it involves coroutines and animations. That’s what we are going to do in the next article.

💬 If you found this article useful, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.

--

--

Glenn Sandoval
Kotlin and Kotlin for Android

I’m a software developer who loves learning and making new things all the time. I especially like mobile technology.