ListView widget in Flutter

Video thumbnail

A ListView is a Flutter widget used to build a scrollable list of widgets. It's frequently used in conjunction with the ScaffoldMessenger widget in Flutter to display data when interacting with the list items, where elements are added dynamically as the list is scrolled. ListView is a powerful tool for displaying large amounts of data in a Flutter application.

Flutter provides several types of ListView to meet different needs and use cases, such as ListView.builder, ListView.separated, and ListView.custom, among others.

In short, ListView is an essential component in building user interfaces in Flutter and is used to display a list of data efficiently and dynamically. A basic example of a ListView is shown below.

ListView.builder(
 itemCount: 100, // número de elementos en la lista
 itemBuilder: (BuildContext context, int index) {
   // construir el elemento de la lista
   return ListTile(
     title: Text('Elemento $index'),
   );
 },
),

In this example, we use the ListView.builder constructor to build a 100-item list. The itemCount parameter indicates the number of items to display in the list.

The itemBuilder parameter specifies how to build each item in the list. In this case, we use a ListTile for each item, with a text title showing the item's index.

This is just a simple example, but there are many possible ways to customize and use lists in Flutter, such as adding separators, headers, footers, and more.

We are going to know a fundamental widget for any type of modern application in which we want to display a set of data in an organized way through a scrollable list; this great widget is known as the ListView and is similar to Android Studio's RecylverView but more basic and without optimization for long lists but much easier to implement than on Android with Kotlin/Java.

The code to implement the same is as simple as this:

ListView( children: <Widget>[ Text("Item ListView 1"), Text("Item ListView 2"), Text("Item ListView 3"), Text("Item ListView 4") ], )

As you can see, it is simply a widget called ListView that, as is the classic or normal thing in this widget class, receives a list of widgets that can be anything, in this case the simplest thing in Flutter, which would be a widget of type text.

Reverse List + Vertical/Horizontal Scroll + lock scroll in ListView

There are many configurations that we can make on the ListView through the properties, to indicate that we want to apply scroll on the horizontal or vertical axis, we can also block the scroll if you need to do this for some reason or reverse a list; all this through a single property and therefore through a line of code.

We can configure multiple aspects of our list, for example if you want to reverse the items, for that we use the reverse property, if you want the scroll vertically:

scrollDirection: Axis.vertical

Or horizontal:

scrollDirection: Axis.horizontal

If we want to block the scroll in our list and it remains fixed in the first elements:

physics: NeverScrollableScrollPhysics(),

But of course a list of fixed elements is of absolutely no use to us, the idea is to make a dynamic list of elements; this is ideal if we connect to a Rest API.

Or a similar system, but to keep things simple, we'll create a function with a for loop that creates elements dynamically:

@override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: ListView(
        children: generateItem(),
        addAutomaticKeepAlives: false,
        scrollDirection: Axis.vertical,
        )
         // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
  List<Widget> generateItem(){
    final list = List<Widget>();
    for (var i = 0; i <= 100; i++) { 
      list.add(Text("Item ListView "+i.toString()));
    }
    return list;
  }

We can also place a divider between the elements or items of our ListView widget:

   for (var i = 0; i <= 100; i++) { 
      list.add(Text("Item ListView "+i.toString()));
      list.add(Divider());
    }

Problem with ListView widget

The problem with the ListView as we implement it, is that it is not optimized to work with very large lists, or dynamic lists; Let's remember that the ListView in basic Android were "replaced" to a certain extent by the RecyclerView which offer excellent performance for large lists of items, since they "recycle" the view items that we don't see and reuse them in the rest of the list that appears or consumes at the request of the user.

And with this we conclude the use of the ListView at least in its basic use, but remember that there are many more properties and variants of this list that we will see later.

Always remember to check the official documentation on Flutter ListView.

Animated ListView in Flutter

In Flutter, ListView is a widget that is used to display a set of items in a list. ListView is an extremely flexible widget and allows you to display a variety of formats as well as lists in landscape or portrait format, with custom or standard elements. Additionally, ListView is highly performing and ideal for large data sets, as it only loads the items that are currently displayed on the screen.

Create a listview

To create a ListView in Flutter, you first need a dataset on which to build the list. You can then use the ListView.builder widget to build the list dynamically as needed, giving it the necessary elements to display in the list as the user scrolls through it:

ListView.builder(
 itemCount: items.length,
 itemBuilder: (BuildContext context, int index) {
   return ListTile(
     title: Text(items[index]),
   );
 },
);

In this example, items is a list of items to display in the list, and for each item a ListTile will be created, containing a title that displays the item's text.

This is just a basic example, there are many other options and customizations available for the ListView in Flutter.

Animated

Animations are essential in any application we build today, equipping the application to show smooth changes or transitions between one state to another is normal; In the lists through the ListView in Flutter things change a bit, since, due to their behavior, the lists start the animation when an item appears on the screen, it becomes more complicated to carry out this task.

There are many ways to animate a listing; but, to keep it simple, we are going to use the following package:

https://pub.dev/packages/delayed_display

We install the package with:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  syncfusion_flutter_datepicker:
  delayed_display:

Which allows adding delays or delay to the widget that is embedded by the DelayedDisplay() widget; In addition to this, we can specify a simple "fade In" type animation which we can customize the direction of the animation by indicating the axis where the effect starts; for example, if we want it from left to right:

slidingBeginOffset: const Offset(-1, 0)

Or vice versa (right to left):

slidingBeginOffset: const Offset(1, 0)

From top to bottom:

slidingBeginOffset: const Offset(0, -1)

Or vice versa (from bottom to top):

slidingBeginOffset: const Offset(0, 1)

You can customize other aspects such as the animation curve among other details that you can review in the official documentation.

Finally, we will use the mentioned widget:

ListView.builder(
    itemCount: 50,
    itemBuilder: ((context, index) => DelayedDisplay(
    delay: const Duration(milliseconds: 5),
    slidingBeginOffset: const Offset(-1, 0),
    fadeIn: true,
    child: Card(***)
  • slidingBeginOffset, we specify the animation curve through the Offset, which receives the X and Y position, which are the factors taken into account to perform the displacement effect.
  • fadeIn, enables or disables the fade in effect.
  • delay, indicates the delay in the animation.

Remember that this post is part of my complete course on Flutter.

Expandable ListView in Flutter

Many times when we create our applications on Android, iOS, an application in general, we are interested in creating a multi-level list or a nested list, that is, having the list, if we click or touch it it expands and another list appears or at least , more information, this is somewhat complicated to create if we use native Android or iOS but with Flutter we are in luck and we have a widget that allows us to do this behavior.

The Expandable ListView is a very interesting and useful Flutter widget in many cases since it allows you to create drop-down lists with hidden content that is shown when you click on it, it's that simple.

This functionality is especially useful when you need to display additional information in a list without taking up too much screen space.

Getting started with the Exandable ListView in Flutter

The Expandable ListView is used in conjunction with the ListView widget, which is responsible for displaying the list items. Using the Expandable ListView, we can hide or show additional content when clicking or tapping on a list item.

One of the most notable features of the Expandable ListView is its ability to expand and contract in an animated manner. This creates an engaging user experience and improves the usability of the app.

To implement the Expandable ListView, some simple steps must be followed. First, we create a list of elements that we want to display in our application. Next, we use the ListView.builder widget to build the list and the Expandable ListView.builder widget to add the drop-down functionality.

Inside the Expandable ListView.builder widget, we define how each list item will behave when expanded or collapsed. We can customize the additional content that will be displayed when expanded, such as images, text or even other widgets.

In addition to the basic expand and collapse functionality, the Expandable ListView also offers customization options. We can adjust the appearance of the headers and icons used to indicate the status of each list item.

The Expandable ListView in Flutter is a powerful tool for creating dynamic and attractive user interfaces. It allows developers to effectively display additional information in a list without overwhelming the user with too much information at once.

Let's see an example

ExpansionTile(
  title: Text('Título del elemento'),
  children: <Widget>[
    ListTile(
      title: Text('Element 1'),
    ),
    ListTile(
      title: Text('Element 2'),
    ),
    ListTile(
      title: Text('Element 3'),
    ),
  ],
)

The ExpansionTile has similar features to the ListTile such as the title, subtitle, you can use the help option of VSC or Android Studio to see more information.

Here I give you another example in which I show how to create a listing listing which was the original objective of this post:

ListView.builder(
          shrinkWrap: true,
          itemCount: widget.tutorialModel.tutorialSectionsModel.length,
          itemBuilder: (context, index) {
            return ExpansionTile(
              title: Text('TEXT'),
              subtitle: Text('TEXT'),
              children: [
                ListView.builder(
                  shrinkWrap: true,
                  itemCount: 10,
                  itemBuilder: (context, index2) {
                    return ListTile(
                      title: Row(
                        children: [
                          Checkbox(
                            value: true,
                            onChanged: (value) {},
                          ),*
                    );
                  },
                )
              ],
            );
          },
        )

As you can see, since the ExpansionTile receives a children or set of widgets, we can place a column and another Listview as a single element and with this, we have a Listview of Listview in Flutter.

Create a Static List of Widgets in a Wrap/Row based on a List of Objects in Flutter

I want to show you how you can create a list. I'm not referring to the ListView or ListView.builder type of list, which is the one we're seeing here on screen:

ListView.builder(
 itemCount: bookResponseModel.books.length,
 itemBuilder: (_, int position) {
   return _itemBook(position, context);
 }
)

ListView.builder and its Equivalent

ListView.builder is somewhat reminiscent of RecyclerView in native Android. What it does is recycle the items that are not visible on the screen to show them again when necessary.

For example, if we scroll down the list, the elements that hide at the top are reused further down, only changing the information they display. This is more efficient than creating a lot of widgets in memory that are never seen.

Note: I'm not going to delve into this; let's just remember that ListView.builder is intended for dynamic and large lists.

Static Lists: Using Wrap instead of ListView.builder

For static lists, such as a category list that doesn't require dynamic scrolling or element recycling, using ListView.builder is not recommended. This would be inefficient because we don't need to constantly check if an item is visible or hidden.

In this case, you can build a list using a Wrap, which automatically adapts the content and works as an equivalent to Row and Column combined:

Wrap(
 spacing: 1,
 alignment: WrapAlignment.start,
 crossAxisAlignment: WrapCrossAlignment.start,
 children: categories
     .map((c) => TextButton(
           child: Text(
             c.title,
             style: TextStyle(
                 color: _categoryId == c.id
                     ? Colors.purple
                     : Theme.of(context).colorScheme.secondary,
                 fontWeight: _categoryId == c.id
                     ? FontWeight.bold
                     : FontWeight.w500),
           ),
           onPressed: () {
             if (_categoryId == c.id) {
               _categoryId = 0;
             } else {
               _categoryId = c.id;
             }
             setState(() {});
             _filter();
           },
         ))
     .toList(),
),
ElevatedButton(
 onPressed: () {
   Navigator.of(context).pop();
 },
 child: Text(LocaleKeys.close.tr()),
),
  • Wrap automatically places the widgets in rows and columns.
  • `children` receives a list of widgets, which in this case is generated from a list of objects.

With the map method, we convert each object (for example, each category) into a widget, in this case a TextButton.

Example of Dynamic List with List.generate

We can also create widgets from a list dynamically using List.generate:

List.generate(100, (index) {
 return Center(
   child: Text(
     'Item $index',
     style: Theme.of(context).textTheme.headlineSmall,
   ),
 );
});
  • This allows generating a list of widgets based on any number of elements.
  • It is useful for large lists where you want to create widgets programmatically.

If you want, I can make a summarized and visual version with a diagram of Wrap vs ListView.builder, so you can understand much faster how and when to use each one.

Filters in Flutter: How to Filter Data

I'm going to explain something I consider very interesting: the use of filters to filter data in Flutter.

In this case, as you can see on the screen, this component can be placed anywhere. If you've been following the application a bit, you'll already know that in the posts section, where I have it placed at the top, the space is reduced and showing so much information on a single screen can complicate organization.

That's why I prefer to take advantage of the characteristics of mobile devices: placing the filter in an accessible button, usually at the bottom for greater comfort when using it with the thumb. For now, it is placed here and works correctly.

Local Filter: Book Example

Notice that here we are working with books. I have a limited number of records, between 15 and 30 books, so the sorting is local, and extremely fast. Later, I'll show you what it would be like if the filters were performed remotely, on a server.

Switch for Language

Here I use a Switch to filter by language:

ListTile(
 leading: const Icon(Icons.language, color: Colors.purple),
 title: const Text('Only in Spanish'),
 trailing: Switch.adaptive(
     value: _onlyLang == 'spanish',
     activeColor: Colors.purple,
     onChanged: (value) {
       _onlyLang = 'spanish';
       _filter();
       setState(() {});
     }),
),
ListTile(
 leading: const Icon(Icons.language, color: Colors.purple),
 title: const Text('Only in English'),
 trailing: Switch.adaptive(
     value: _onlyLang == 'english',
     activeColor: Colors.purple,
     onChanged: (value) {
       _onlyLang = 'english';
       _filter();
       setState(() {});
     }),
),
  • _onlyLang stores the selected language as a String (spanish or english).
  • When changing the switch, it automatically updates using `setState()`.

Switch for Price

    To filter by price (free or paid), I use another switch:

ListTile(
 leading: const Icon(Icons.exposure_zero, color: Colors.purple),
 title: Text(LocaleKeys.free.tr()),
 trailing: Switch.adaptive(
     value: _freePay == 'free',
     activeColor: Colors.purple,
     onChanged: (value) {
       _freePay = (_freePay == 'free') ? '' : 'free';
       setState(() {});
       _filter();
     }),
),
ListTile(
 leading: const Icon(Icons.payments, color: Colors.purple),
 title: Text(LocaleKeys.pay.tr()),
 trailing: Switch.adaptive(
     value: _freePay == 'pay',
     activeColor: Colors.purple,
     onChanged: (value) {
       _freePay = (_freePay == 'pay') ? '' : 'pay';
       setState(() {});
       _filter();
     }),
),
  •  `_freePay` controls three states: free, paid, or none.    
  • The logic is handled with conditionals that allow toggling between the values correctly.    

    Filter by Categories

    For categories, I use a Wrap that adapts the buttons based on the available space:

Wrap(
 spacing: 1,
 alignment: WrapAlignment.start,
 crossAxisAlignment: WrapCrossAlignment.start,
 children: categories
     .map((c) => TextButton(
           child: Text(
             c.title,
             style: TextStyle(
                 color: _categoryId == c.id
                     ? Colors.purple
                     : Theme.of(context).colorScheme.secondary,
                 fontWeight: _categoryId == c.id
                     ? FontWeight.bold
                     : FontWeight.w500),
           ),
           onPressed: () {
             _categoryId = (_categoryId == c.id) ? 0 : c.id;
             setState(() {});
             _filter();
           },
         ))
     .toList(),
),
  •  `_categoryId` stores the selected category.    

    Each button updates the state and executes the `_filter()` function to show only the corresponding elements.

    Widget for Filtering Individual Elements

    For each element (book), I use a widget that checks if it should be displayed:

Widget _itemBook(int position, BuildContext context) {
 final book = bookResponseModel.books[position];
 if (!book.show) {
   return const SizedBox();
 }
 String image = '';
 if (book.post.image != '') {
   image = '${BlogHelper.baseUrlImage}${book.post.path}/${book.post.image}';
 }
 return Column(
   children: [
     // Book content
   ],
 );
}
  • The `show` property determines whether the element is shown or hidden.    
  • This avoids directly modifying the array and is more efficient than continuously deleting or adding elements.    

    Remote Filter: Post Example

    When working with many posts, the local filter is not efficient. In that case, a remote filter is performed:

    Every time the user selects a filter, a request is sent to the server.

    The server returns the paginated list according to the selected filters.

    This prevents loading all records at once, improving efficiency.

_dataList() async {
 bookResponseModel = await AcademyHelper.getMyBooks(appModel.userToken);
}

    This function retrieves the filtered data from the server.

    The filtering and pagination logic is applied directly in the backend.

    Conclusion

  •         Local filters are fast and suitable for small lists.    
  •     Remote filters are necessary when working with large amounts of data.    
  •     Language, price, and category filters can be combined efficiently.    
  •     The implementation with Wrap, Switch, and conditionals allows for clear and modular control of element visualization.    

From ListView to GridView in Flutter, first steps

ListView

We have a list, the name doesn't matter here, it's a list of anything we ask for it and this is to know the number of elements that we are going to build, then here we have the item builder in which we receive the context, I'm not interested in this case, and the position, and here finally a widget that builds our item, in this case what we have here, this is not very difficult and it's the same one that I'm using here, simply here some conditions to know if the image exists:

ListView.builder(
 itemCount: tutorialResponseModel.tutorials.length,
 itemBuilder: (_, int position) {
 return _itemTutorial(position, context);
 }),

And the rest is up to you, a column to glue our elements together, which in this case is the title, some spacing, and the image. Here I used ClipRRect to round the corners. Otherwise, there's nothing strange about it. And a little button to go to the action, to buy or whatever. We can expand this without any changes to either element, whether it's a ListView or a GridView:

Widget _itemTutorial(int position, BuildContext context) {
    final appModel = Provider.of<AppModel>(context, listen: false);
    final tutorial = tutorialResponseModel.tutorials[position];
    if (!tutorial.show) {
      return const SizedBox();
    }
    String image = '';
    if (tutorial.image != '') {
      image = '${AcademyHelper.baseUrlImage}${tutorial.path}/${tutorial.image}';
    }
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Text(
          tutorial.title.toUpperCase(),
          style: Theme.of(context).textTheme.displayMedium,
        ),
        const SizedBox(
          height: 15,
        ),
        image != ''
            ? ClipRRect(
                borderRadius: BorderRadius.circular(15),
                child: Image.network(
                          image,
                          errorBuilder: (context, error, stackTrace) =>
                              const SizedBox(),
                          loadingBuilder: (context, child, loadingProgress) {
                            if (loadingProgress == null) return child;
                            return Container(
                              width: double.infinity,
                              height: 120,
                              color: Colors.grey,
                            );
                          },
                        ),
                      ))
            : const SizedBox(),
        const SizedBox(
          height: 15,
        ),
        Padding(
          padding: const EdgeInsets.only(right: 15, left: 15),
          child: Text(
            truncate(tutorial.description),
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ),
        const SizedBox(
          height: 15,
        ),
         Text(
                    '${tutorial.priceOffers}\$',
                    style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                        color: Colors.green, fontWeight: FontWeight.bold),
                  ),

GridView

Just as I was showing you then what we have here in the GridView since we know what we have here in the ListView:

GridView.builder(
                 shrinkWrap: true,
                 physics:
                     const BouncingScrollPhysics(), // evita problema de doble scroll
                 padding: const EdgeInsets.all(8),
                 itemCount: tutorialResponseModel.tutorials.length,
                 gridDelegate:
                     //     SliverGridDelegateWithFixedCrossAxisCount(
                     //   crossAxisCount: countItem,
                     // ),
                     SliverGridDelegateWithFixedCrossAxisCount(
                         crossAxisCount: countItem,
                         crossAxisSpacing: 8.0,
                         mainAxisExtent: 520,
                         mainAxisSpacing: 8.0),
                 itemBuilder: (_, position) {
                   return _itemTutorial(position, context);
                 })

And we can compare this to make it a little more efficient as indicated here this would be a little optional but in this case as in several views I have double scroll, that is to say on the one hand in this view I think I don't have it but if this I am using as a parent element a single child scroll View and this also has its own scroll then to avoid precisely a problem with the double scroll:

physics: const BouncingScrollPhysics(), // evita problema de doble scroll

This is to define the size of our gripView cell:

mainAxisExtent: 520,

It would be from the item element how you want to see it some spacing:

crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0

So that they do not appear all stuck together, this would be your personal matter and here we pass the position so that it builds based on the list that we obviously have defined at the class level, it builds the element just as I showed you here, the part I think that stands out the most is the Main Access extended which is to indicate the size of the cell:

mainAxisExtent: 520,

Note that, regardless of the available space, it's recommended to define a minimum size for each cell or row. This ensures that all rows are correctly aligned and prevents design problems.

If we assign a size smaller than necessary, we can experience overflow issues, as the cell or container where we place our elements will be too small. This can cause the content to shift upwards, which is obviously undesirable.

Adjusting the Size

To avoid these problems, it's advisable to experiment with the minimum size assigned to each cell. This is optional, but if you don't define it, you may encounter the problem mentioned earlier: disorganized content, excessively small cells, and an unprofessional appearance.

Optional Parameters

There are additional parameters you can configure, depending on your needs. For example, some settings are optional, and you can find more information online about what other parameters can be applied.

If you don't define a minimum size, it won't cause an error, but the content will appear compressed and disorganized.

By defining a minimum size, we solve the overflow problem and ensure that the row presentation is consistent and readable.

In short, assigning a minimum size to each cell is a best practice to maintain structure and prevent content scrolling issues.

Next step, learn how to use the FutureBuilder widget in Flutter.

I agree to receive announcements of interest about this Blog.

Let's learn how to create a list in Flutter using the ListView widget: To display data using the ListView widget, I'll also show you how to use the GridView system in Flutter starting from a ListView, animate them, and the expand option.

| 👤 Andrés Cruz

🇪🇸 En español