The Scroll Parallax : Flutter Edition

The Scroll Parallax : Flutter Edition

Nesting multi-scroll widgets the easy way

Whether you are a flutter novice building your interest in Dart, a hardened developer with published apps on the play store (or anywhere in the middle), you are more than once, in your coding journey going to face something I call the 'scroll parallax'.

Let's consider a situation where you are asked to mimic this interface using flutter for a project. (The enclosed graphic is from John Himic's MultiScroll, one of the other cooler things online.)

Now, the intuitive solution would be to add either of these two things inside a Column widget, and wrap the other within a ListView. So this is exactly what I did.

Why this doesn't work (and how I found out)

Yes, the above solution is the best and worst way to deal with the situation at hand. To understand this clearly, let's take a look at how Flutter handles which UI components to show onscreen, and how they get rendered on the device window.

Constraints go down. Sizes go up. Parent sets position.

Anyone well versed with Google's official flutter documentation knows that the above line governs the entirety of development with the tech stack. In simple words, the parent widget must always decide the size and position of its child widgets. This rule is followed throughout the content chain, with the said child widget deciding the constraints for its own children, and so on.

This can be translated to the following takeaways:

  1. A widget cannot decide its size. (outside the constraints set by the parent)

  2. A widget cannot decide its position.

Since the parent’s size and position, in its turn, also depends on its own parent, it’s impossible to precisely define the size and position of any widget without taking into consideration the tree as a whole. So, if a child wants a different size from its parent and the parent doesn’t have enough information to align it, then the child’s size (and the widget itself) might be ignored.

After a basic understanding of the layout rule, let's try to see why the ListView/Column solution in the above example doesn't work.

  1. Both ListView and Column are used for storing multiple widgets. Both of them can easily be given scroll properties. So when we include such similar widgets inside the same parent, Flutter gets confused about where the size and scrolling for one ends, and the other begins. I call this 'The Scroll Parallax'.

  2. Most importantly, this is not a problem definitive to ListView or ColumnView, this occurs every single time two widgets (that may possess scrolling ability in the future) are placed under the same parent.

Now, this is exactly the kind of problem that no tutorial mentions, yet one which can ruin hours and hours of productivity. If you've come here after hours of debugging and frustration, I've attached my own account of misery with this same parallax. But before we reminisce, let's understand how to solve this.

So, how can you avoid the scrolling parallax?

Flutter only has a problem with widgets where the parent doesn't define any constraints for the children it encloses. There are a variety of solutions to defeat this, including the very obvious one. But I've concocted a list of the simplest ways to handle multi-scroll components in Flutter.

  1. Using SizedBox/Container

    Since we're concerned with only the size component of the scroll-widget, a naive and simple solution would be to wrap said widget within a SizedBox, or a Container (with well-defined height and width).

     void main() {
       runApp(MaterialApp(
         home: Scaffold(
           body: Center(
             child: Column(
               children: [
                 SizedBox( // or container
                   height: 100,
                   width: 100, 
                   child: ListView(),
                 )
               ],
             ),
           ),
         ),
       ));
     }
    
  2. Using Flexible

    The Flexible widget gives the child of a Row, Column or Flex, the ability to occupy only as much space as it requires. Meaning, if we wrap two string items, say, "Parabola" and "ParanormalEpisode" using Flexible, the space they occupy would differ as follows.

    Using this approach, we can implement multi-scrolling widgets with Flexible.

    (Note that FlexFit.loose is the property of Flexible which is responsible for this arrangement. It is also the default set value for fit, and can be changed according to requirement)

     void main() {
       runApp(MaterialApp(
         home: Scaffold(
           body: Center(
             child: Column(
               children: [
                 Flexible(
                   fit: FlexFit.tight,
                   child: ListView(),
                 )
               ],
             ),
           ),
         ),
       ));
     }
    
  3. Using Expanded

    Similarly, Expanded is a subset of Flexible and also controls the space allotted to app widgets. It forces the child to take up all available space within the parent widget.

    (Setting the fit to FlexFit.tight in Flexible can also be used to achieve the same configuration)

    Basically, in the given snippet, the ListView will occupy the entire space devoted to the Column widget. (since it is the only child)

     void main() {
       runApp(MaterialApp(
         home: Scaffold(
           body: Center(
             child: Column(
               children: [
                 Expanded(
                   child: ListView(),
                 )
               ],
             ),
           ),
         ),
       ));
     }
    

The Debugging Walk of Shame

(Props for staying till the end!)

My primary encounter with 'The Scroll Parallax' takes me back to March 2022, my first ever hackathon, holding memories of my first ever flutter project.

Our app was a simple mental health awareness solution that my teammates and I had gathered over two cups of coffee. Naturally, being then-beginners in the art of development, we placed laser focus on the way our product would look to the third eye, and spent hours searching through complex visual designs on the web.

In the end, we agreed upon the PageView widget as our opening screen, to create a scrollable bar, holding the different kinds of moods and feelings our app would cater to. At first, it seemed like the perfect solution, easy to implement and pretty to look at. That same night, after we finished up the Figma for our solution, I sat down to work on that piece of code, ultimately falling into one of the longest potholes of my building journey. The debugger highlighted that not one line in my 2000-word codebase had an error, but the app rendered on the device was a blank white screen. My mind ran around in circles. By 4 A.M., I had executed whatever solutions my sleep-deprived body could think of, devoured the whole of everything StackOverflow had to offer on the topic (which was nothing). In the end, I discarded the entire screen and made to-do with whatever similar widget I could find (No, my team members still don't know).

This way the chapter progressed smoothly, and some eight months later I bagged my first app development internship. Yet, god was hiding his favorite soldier at the back of the guild. For defining my presence at the company, I was asked to recreate the user interface of a Figma design under a deadline of three hours. From an initial look at the design statement, one single word found its way out of the thought bubble, PageView. And we're back to ground zero.

Fast forward to one year, I was stuck on that project for way longer than 3 hours but gained most of my not-famous-must-be-famous flutter concepts through that experience. Now, the kiddie bone inside my mind tickles at the thought of helping others find answers to questions the internet hasn't found yet. Maybe, I'll always jump at the thought of adding multiple scrolling to my projects. Maybe, I'll find a better widget to counter the parallax, or maybe I'll find an even cooler paradox to patent under this blog. Likewise, this story will be one of my worst and most favorite programming jumbos of all time (yet).