We have an app that relies heavily on fragments. Every screen is an activity that hosts a fragment that hosts a recycler view:
We want to start migrating the screens to compose by keeping the screen’s content and placing it in a compose environment. This means that our activities will call setContent { } instead of setContentView() and that our top and bottom bars will be written in compose. Also, a must have requirement is that the top bar has to react to the user’s scroll by collapsing and expanding.
Using Scaffold and AndroidFragment
Scaffold offers, among others, slots for both bars. It also provides wiring between the screens’ components so that we can easily add support for various interactions. In our case we want to leverage top bar’s enter always scroll behavior. AndroidFragment is a composable that takes care the instantiation and hosting of a fragment.
Putting them together we start we something like this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
As you can see the list renders fine but, even though we’ve setup the wiring, the top bar does not collapse/expand when we scroll.
NestedScrollConnection and NestedScrollDispatcher
Scrolling in compose is a bit different from the view systems’. When a scroll event happens, instead of just one composable handling it, multiple composables can participate and decide how much of that scroll they want to consume.
To achieve that, compose provides three components:
NestedScrollConnection which is for parents to listen and consume
NestedScrollDispatcher which is for children to dispatch events and
the .nestedScroll() modifier which is the way to integrate these to the hierarchy
This means that if we want a parent composable to react to scroll events we need to provide a connection to it. If we want one of its the children to emit those events we need to provide to it that same connection and a dispatcher. .nestedScroll internally creates a NestedScrollNode which is used to couple the connection and the dispatcher together.
So, for our case, we have to create a dispatcher, couple it with the scroll behavior’s connection and provide it to our recycler view. Then the view will use it to dispatch its scroll events.
fun RecyclerView.setNestedScrollDispatcher()
Looking at the dispatcher’s API we can see that it provides two methods to dispatch scroll events. The first one, dispatchPreScroll, will inform the that a scroll is about to take place and that it will consume a certain distance. The second one, dispatchPostScroll, will inform that a scroll took place, it consumed a certain distance and has left some for consumption.
In the compose world all that makes sense. The scrollable modifier handles a scroll delta and communicates it properly in pre and post events. In the view world we don’t have anything similar. We can implement the logic using gesture detectors and by intercepting touch event but we can start simpler with a OnScrollListener:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
When dispatching the y delta we need to negate its sign because RecyclerView and Compose use opposite sign conventions for scroll direction.
We coupled the connection with the dispatcher in the AndroidFragment because the created NestedScrollNode will try to find a parent node to dispatch its events too.
As you can see from the video the implementation works. The top bar collapses and expands accordingly. The only problem is that when the user starts scrolling slowly the top bar jiggles creating an unpleasant UX.
After adding some logs we can see that the scrolled dy is not always positive or negative through out the gesture:
NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=3 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=1 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=6 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=-3 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=6 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=-4 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=10 NestedScrollDispatcher gr.le0nidas.fragmentincompose D onScrolled: dy=-7
The slow movement is making the framework think that we are constantly trying to move a little bit up and immediately a little bit down. This causes the jiggle since the top bar toggles constantly between collapsing and expanding.
VerticalMovementDetector
We can prevent this by determining what kind of movement we have and then ignore any deltas that are not part of that movement.
To do that we need to have a window of n deltas and then see if the majority of them is positive or negative which will mean that the user scrolls down or up respectively. After knowing that we simply ignore the deltas that we don’t want.
A couple of things that help in the UX:
Until we fill that window we do not dispatch anything.
After filling it we make sure that we keep the last n deltas. That way we can determine the movement even if the user does one continuous gesture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Here is the entire code where we’ve added a sampling switch in order to keep things cleaner and avoid weird edge cases:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters