13 Classic Controls Ii How Uitable View List View Are Used in Flutter

13 Classic Controls II How UITableView-ListView Are Used in Flutter #

Hello, I’m Chen Hang.

In the previous article, we learned about the usage of three classic components in Flutter: text, image, and button. We also learned how to customize their styles based on different scenarios in actual development.

These basic elements, such as text, image, and button, need to be arranged and combined to form the UI views we see. When the arrangement of these basic elements exceeds the screen size (i.e., longer than one screen), we need to introduce list controls to display the complete content of the view and adaptively scroll based on the number of elements.

In Android, this requirement is achieved by ListView or RecyclerView, while in iOS, it is achieved by UITableView. In Flutter, the list control ListView is used to achieve this requirement.

ListView #

In Flutter, ListView arranges all its child widgets in one direction (vertical or horizontal), and is often used in scenarios where a continuous group of views needs to be displayed, such as contact lists, coupons, and merchant lists.

Let’s see how to use ListView. ListView provides a default constructor ListView, and we can conveniently include all the child widgets in the ListView by setting its children parameter.

However, this method requires all the child widgets to be created in advance, instead of creating them when they are actually needed to be displayed on the screen. So, there is an obvious downside, which is poor performance. Therefore, this method is only suitable for scenarios with a small number of elements in the list.

As shown below, I have defined a group of list item components and placed them in a vertically scrolling ListView:

ListView(
  children: <Widget>[
    // Set the title and icon of the ListTile component
    ListTile(leading: Icon(Icons.map),  title: Text('Map')),
    ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
    ListTile(leading: Icon(Icons.message), title: Text('Message')),
]);

Note: ListTile is a small widget unit provided by Flutter for quickly building list item elements. It is used to display text, icons, and other view elements in 1-3 lines (leading, title, subtitle) and is often used in conjunction with ListView. - The use of ListTile in the code above is to demonstrate the capabilities of ListView. The specific usage details of ListTile are not the focus of this article. If you want to learn more about it, you can refer to the official documentation.

The running effect is shown in the figure below:

Figure 1: ListView default constructor

In addition to the default vertical layout, ListView also supports horizontal layout by setting the scrollDirection parameter. As shown below, I defined a group of components with different background colors, set their width to 140, and put them in a ListView with horizontal layout, allowing them to scroll horizontally:

ListView(
    scrollDirection: Axis.horizontal,
    itemExtent: 140, // Width of each item
    children: <Widget>[
      Container(color: Colors.black),
      Container(color: Colors.red),
      Container(color: Colors.blue),
      Container(color: Colors.green),
      Container(color: Colors.yellow),
      Container(color: Colors.orange),
]);

The running effect is shown in the figure below:

Figure 2: Horizontal scrolling ListView

In this example, we created 6 child widgets all at once. But from the running effect in figure 2, we can see that due to the limited width and height of the screen, the user can only see 3 widgets at a time. In other words, whether to create all the child widgets in advance or not does not make any visual difference to the user.

Therefore, considering the performance issues caused by creating child widgets, a better approach is to abstract the method of creating child widgets and let ListView manage them centrally, and create them only when they need to be displayed.

Another constructor of ListView, ListView.builder, is suitable for scenarios with a large number of child widgets. This constructor has two key parameters:

  • itemBuilder is the method for creating list items. When the list scrolls to the corresponding position, ListView will call this method to create the corresponding child widgets.
  • itemCount represents the number of list items. If it is null, it means ListView is an infinite list.

Similarly, I will explain the usage of itemBuilder and itemCount through an example.

I defined a ListView with 100 list elements. In the method for creating list items, I set the value of index as the title and subtitle of ListTile. For example, the first list item will display “title 0 body 0”:

ListView.builder(
    itemCount: 100, // Number of elements
    itemExtent: 50.0, // Height of list items
    itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
);

Note that itemExtent is not a required parameter. However, for list item elements with a fixed height, I highly recommend that you set the value of this parameter in advance.

Because if this parameter is null, ListView will dynamically determine its own view height and the relative positions of the child widgets based on the result of creating the child widgets. When scrolling changes occur and there are many list items, such calculations will be very frequent.

But if itemExtent is set in advance, ListView can calculate the relative positions of each list item element and its own view height in advance, saving unnecessary calculations.

Therefore, in ListView, specifying itemExtent is more efficient than letting the child widgets determine their own heights.

Running this example, the effect is as shown below:

Figure 3: ListView.builder constructor

You may have noticed that our list is still missing separators. In ListView, there are two ways to support separators:

  • One is to dynamically create separators based on the value of index in itemBuilder, which means considering the separator as part of the list item.
  • Another is to use another constructor of ListView, ListView.separated, to set the style of the separator separately.

The first method is actually the combination of views, which we have mentioned several times in previous explanations, so I will not introduce it in more detail here. Next, I will demonstrate how to use ListView.separated to set separators.

Similar to abstracting the method for creating child widgets in ListView.builder, ListView.separated abstracts the method for creating separators as separatorBuilder, so that different styles of separators can be set based on the index.

As shown below, for scenarios where the index is even, I created a green separator, and for scenarios where the index is odd, I created a red separator:

// Use ListView.separated to set separators
ListView.separated(
    itemCount: 100,
    separatorBuilder: (BuildContext context, int index) => 
        index % 2 == 0 ? Divider(color: Colors.green) : Divider(color: Colors.red),
    itemBuilder: (BuildContext context, int index) =>
        ListTile(title: Text("title $index"), subtitle: Text("body $index"))// Create child widgets
)

The running effect is as shown below:

Figure 4: ListView.separated constructor

Well, I have finished sharing the common constructors of ListView with you. Next, I have prepared a table summarizing the common constructors of ListView and their applicable scenarios for your reference, to help you understand and remember them:

Figure 5: Common constructors of ListView and their applicable scenarios

CustomScrollView #

Okay, ListView implements the interaction model of scrollable widgets under a single view, and also includes UI display-related control logic and layout models. However, for certain special interaction scenarios, such as multiple effects linkage, nested scrolling, fine scrolling, and view following gesture operations, multiple ListViews need to be nested to achieve this. In this case, the scroll and layout models of each view are independent and separate, making it difficult to ensure consistent scrolling effects throughout the entire page.

So, how does Flutter solve the problem of inconsistent page scrolling effects when multiple ListViews are nested?

In Flutter, there is a dedicated widget called CustomScrollView, which is used to handle multiple widgets that require custom scroll effects. In CustomScrollView, these individually scrollable widgets are collectively referred to as Slivers.

For example, the Sliver implementation of ListView is SliverList, and the Sliver implementation of AppBar is SliverAppBar. These Slivers no longer maintain their own scroll states, but are managed by CustomScrollView, resulting in consistent scrolling effects.

Next, I will demonstrate how to use CustomScrollView through an example of scrolling parallax.

Parallax scrolling refers to the technique of moving multiple layers of backgrounds at different speeds to create a three-dimensional scrolling effect while maintaining a good visual experience. As a hot trend in mobile application interaction design, more and more mobile applications are using this technique.

Take a list with a cover image as an example. We want the cover image and the list to be linked together when the user scrolls the list, such that the cover image shrinks and expands based on the user’s scrolling gesture.

After analysis, we determine that we need two Slivers to achieve this requirement: SliverAppBar for the cover image and SliverList for the list. The specific implementation is as follows:

  • When creating the SliverAppBar, set the flexibleSpace parameter to the floating background of the cover image. The flexibleSpace allows the background image to be displayed below the AppBar, with the same height as the SliverAppBar.
  • When creating the SliverList, use the SliverChildBuilderDelegate parameter to create the list item elements.
  • Finally, combine them and pass them to the slivers parameter of CustomScrollView for unified management.

The specific example code is as follows:

CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(// SliverAppBar as the header image widget
      title: Text('CustomScrollView Demo'),// title
      floating: true,// set floating style
      flexibleSpace: Image.network("https://xx.jpg",fit:BoxFit.cover),// set floating background for the header image
      expandedHeight: 300,// header image widget height
    ),
    SliverList(// SliverList as the list widget
      delegate: SliverChildBuilderDelegate(
            (context, index) => ListTile(title: Text('Item #$index')),// list item creation method
        childCount: 100,// number of list elements
      ),
    ),
  ]);

When you run it, the parallax scrolling effect looks like this:

Image 6 CustomScrollView example

ScrollController and ScrollNotification #

Now, you should already know how to implement visual and interactive effects for a scrolling view. Next, I will share with you a more complex problem: in some cases, we want to obtain the scrolling information of the view and control it accordingly. For example, whether the list has scrolled to the bottom or top? How to quickly return to the top of the list? Whether the list scrolling has started or stopped?

For the first two questions, we can use ScrollController to listen to the scrolling information and control the scrolling accordingly. For the last question, we need to receive ScrollNotification to get the scrolling events. Now I will introduce them separately.

In Flutter, because the Widget is not the final visual element rendered on the screen (RenderObject is), we cannot obtain or set visual information related to the final rendering through the held Widget object, as we can in native Android or iOS systems. Instead, we must use the corresponding component controller to achieve it.

The component controller for ListView is ScrollControler, through which we can obtain the scrolling information of the view and update the scrolling position of the view.

Generally, obtaining the scrolling information of the view is often for controlling the state of the interface, so the initialization, listening, and disposal of ScrollController need to be synchronized with the state of StatefulWidget.

As shown in the code below, we have declared a list item with 100 elements. When the scroll view reaches a specific position, the user can click a button to return to the top of the list:

  • First, in the initialization method of the State, we create a ScrollController and register the scroll listener method callback through _controller.addListener. Based on the current scrolling position of the view, we determine whether to show the “Top” button.
  • Then, in the build method of the view, we associate the ScrollController object with the ListView, and register the corresponding callback method for the RaisedButton. When the button is clicked, _controller.animateTo is used to return to the top of the list with an animation.
  • Finally, in the disposal method of the State, we release the resources of the ScrollController.
class MyAPPState extends State<MyApp> {
  ScrollController _controller; // ListView controller
  bool isToTop = false; // Indicates whether the "Top" button needs to be enabled
  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(() {
      if (_controller.offset > 1000) {
        setState(() {
          isToTop = true;
        });
      } else if (_controller.offset < 300) {
        setState(() {
          isToTop = false;
        });
      }
    });
    super.initState();
  }

  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      RaisedButton(
        onPressed: (isToTop
            ? () {
                if (isToTop) {
                  _controller.animateTo(
                    .0,
                    duration: Duration(milliseconds: 200),
                    curve: Curves.ease,
                  );
                }
              }
            : null),
        child: Text("Top"),
      ),
      ...
      ListView.builder(
        controller: _controller,
        itemCount: 100,
        itemBuilder: (context, index) => ListTile(
          title: Text("Index : $index"),
        ),
      ),
      ...
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

The effect of ScrollController is shown below:

ScrollController

Figure 7. ScrollController example

After introducing how to listen to the scrolling information of ListView and how to control the scrolling using ScrollController, let’s take a look at how to receive ScrollNotification and perceive various scrolling events of ListView.

In Flutter, ScrollNotification is obtained through NotificationListener. Unlike ScrollController, NotificationListener is a Widget. To listen to scroll events, we need to add NotificationListener as the parent container of ListView to capture notifications in ListView. For these notifications, we need to implement the listening logic through the onNotification callback function:

Widget build(BuildContext context) {
  return MaterialApp(
    title: 'ScrollController Demo',
    home: Scaffold(
      appBar: AppBar(title: Text('ScrollController Demo')),
      body: NotificationListener<ScrollNotification>(
        onNotification: (scrollNotification) {
          if (scrollNotification is ScrollStartNotification) {
            print('Scroll Start');
          } else if (scrollNotification is ScrollUpdateNotification) {
            print('Scroll Update');
          } else if (scrollNotification is ScrollEndNotification) {
            print('Scroll End');
          }
        },
        child: ListView.builder(
          itemCount: 30,
          itemBuilder: (context, index) =>
              ListTile(title: Text("Index : $index")),
        ),
      ),
    ),
  );
}

Compared to ScrollController, which can only listen to scrolling information after being associated with a specific ListView, with NotificationListener, we can listen to any ListView in its child widgets. We not only obtain the current scroll position information of these ListViews, but also get the current scroll event information.

Summary #

In the scenario of handling a set of continuous and scrollable view elements, Flutter provides more powerful list components, ListView and CustomScrollView, compared to the native Android and iOS systems. They not only support the interaction model and UI control model of scrollable widgets under a single view, but also provide a unified management mechanism for scenarios that require nesting multiple scrollable widgets, ultimately achieving consistent scrolling effects. These powerful components enable us to not only develop visually rich interfaces, but also implement complex interactions.

Next, let’s briefly review today’s content to deepen your understanding and memory.

First, we got acquainted with the ListView component. It supports both vertical and horizontal scrolling and provides not only a default constructor for creating a small number of child views at once, but also the ListView.builder mechanism for creating child views on demand. It also supports custom dividers. To save performance, for list item views with fixed height, specifying itemExtent in advance is more efficient than letting the child widgets decide.

Then, I taught you about the CustomScrollView component. It introduces the concept of Sliver, which takes over the interaction and layout of nested scrollable views, making advanced interactions such as parallax scrolling easier.

Lastly, we learned about ScrollController and NotificationListener. The former is bound to ListView to listen to scroll information and perform corresponding scroll control, while the latter achieves scroll event capture by including ListView as a child widget.

I have put the three examples (parallax, ScrollController, ScrollNotification) shared today on GitHub. You can download them and run them in your project, and learn while referring to today’s knowledge points to experience some advanced usage of ListView.

Thought Questions #

Finally, I have two small assignments for you:

  1. In the ListView.builder method, the ListView creates widgets on demand based on whether the widgets will appear in the visible area. For certain scenarios, in order to avoid long rendering times for widgets (such as when downloading images), we need to create the widgets in the visible area and its surrounding area in advance. So, how can we achieve this in Flutter?
  2. Please use NotificationListener to implement the same functionality as shown in the ScrollController example in Figure 7.

Feel free to leave your thoughts in the comments section. I will be waiting for you in the next article! Thank you for listening, and please feel free to share this article with more friends to read together.