Flutter ListView Advanced Operations | Riverpod

Created At: 2023-03-18 22:10:05 Updated At: 2023-03-20 02:26:09

Here we will learn how to optimize Flutter listView operations. We will see how to minimize number of build calls using Riverpod. Make sure you install Riverpod package for state management and optimize ListView build calls. We will also see how to reduce build calls number for the whole page.

ListView page and items rebuilds problems

To understand the problems, we will use a ListView.builder. We will see it creates unnecessary page builds

In this example we will randomly generate names using Faker package. Make sure you install it

flutter pub get faker

In our home_page.dart file we will have StateProvider like below. It will return a provider for list of names using faker package.

final listProvider = StateProvider<List<String>>((_) {
  return List.generate(5, (_) => Faker().person.firstName());
});

Here you see listProvider is our provider of list.

Now let's create a class name HomePage and we will put the below code to it.

class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print("...page build...");
    return Scaffold(
      appBar: AppBar(),
      body: ListView.builder(
          itemCount: ref.watch(listProvider).length,
          itemBuilder: (_, index){
            print("...list view build...");
            final list = ref.read(listProvider);
            final item = list[index];
            return ElevatedButton(
                onPressed: (){
                  print("...list item build...$item");
                  int index = ref.watch(listProvider).indexWhere((element) => element==item);
                  list[index]=Faker().person.firstName();
                  ref.watch(listProvider.notifier).state=[...list];
                }, child: Text(item)
            );
          }
      ),
    );
  }
}

This could be a very typical way of doing ListView rendering. But it comes with a hidden cost. This rebuilds the whole page and all the widgets. But this is unnecessary and expensive.

If you see the video carefully, you will see that, each time I click on button, the whole page gets rebuild. The items inside also gets rebuild.

Solutions

To stop these rebuilds we need to restructure of our code. 

First we will seperate the ListView.builder from the main page which is inside Scaffold.

Second we will seperate the items from ListView.builder.

Find a way to make more widgets const

Generate index independently 

We will use select() function

Seperate ListView.builder

Let's seperate it. We will create a new class name it _ListView and put the ListView.builder inside this class.

class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print("...page build...");
    return Scaffold(
      appBar: AppBar(),
      body: const _ListView(),
    );
  }
}
class _ListView extends ConsumerWidget {
  const _ListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(listProvider).length,
        itemBuilder: (_, index){
          print("...list view index $index...");
          final list = ref.read(listProvider);
          final item = list[index];
          return ElevatedButton(
              onPressed: (){
                print("...list item build...$item");
                int index = ref.watch(listProvider).indexWhere((element) => element==item);
                list[index]=Faker().person.firstName();
                ref.watch(listProvider.notifier).state=[...list];
              }, child: Text(item)
          );
        }
    );
  }
}

See the code after seperating ListView.builder from Scaffold. After separating it, and put a const before _ListView(), we have reduced unnecessary page builds.

See the video carefully, and you will find that, page build log is not printing anymore. 

But it still builds all the items as you click on a button. How to prevent that?

Prevent rebuilds of Items

To prevent the rebuilds of items, we need to introduce Riverpod select function and Provider along with StateProvider. We already have StateProvider, we will use Provider for index. We will see about index very soon.

First let's separate the items from _ListView() class, for this we will create a new class name _ListItem() and put all the button related code there. I mean move the items to _ListItem() class.

After separating it looks like this

class _ListView extends ConsumerWidget {
  const _ListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(listProvider).length,
        itemBuilder: (_, index){
          print("...list view index $index...");
          final list = ref.read(listProvider);

          return _ListItem(list[index]);
        }
    );
  }
}

class _ListItem extends ConsumerWidget {
  final String item;
  const _ListItem(this.item) ;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final list = ref.read(listProvider);
    return ElevatedButton(
        onPressed: (){
          print("...list item build...$item");
          int index = ref.watch(listProvider).indexWhere((element) => element==item);
          list[index]=Faker().person.firstName();
          ref.watch(listProvider.notifier).state=[...list];
        }, child: Text(item)
    );
  }
}

But it still rebuilds the items even if you click on one item. To prevent this rebuild, we need make _ListItem(list[index]) constant. To make it constant we need to get rid of list[index] from the constructor.  That means we need to pass the index to _ListItem() class somehow. 

ProviderScope overrides

To pass index, we may use Riverpod's ProviderScope with overrides property. We will generate index inside ListView.builder and change the index in the next loop. 

Since our ElevatedButton gets called in each call inside ListView.builder, the index would be provided correctly to each of the ListView.builder child.

We will use Riverpod's Provider to do it. We will create a provider using Provider and it will generate index per ListView.builder call.

final listProvider = StateProvider<List<String>>((_) {

  return List.generate(5, (_) => Faker().person.firstName());
});

final indexProvider = Provider<int>((_) {
  return 0;
});

We will use indexProvider from Riverpod's overrides property inside Providerscope. To override it, we need to use the code inside another Providerscope. Now it looks like this

ListView.builder(
        itemCount: ref.watch(listProvider).length,
        itemBuilder: (_, index){
          print("...list view index $index...");
          final list = ref.read(listProvider);

          return ProviderScope(
              overrides: [indexProvider.overrideWith((ref) => index)],
              child: _ListItem(list[index])
          );
        }
    )

Now we would be able to remove list[index] make _ListItem() constructor constant. We will directly grab the generated index inside _ListItem().

Using select function

We need to make change for itemCount. Instead of doing ref.watch(listProvider).length we need to do ref.watch(listProvder.select((e)=>e.length)). This will make sure not rebuild the items until length of the items have changed.

Riverpod's select() function let's you select a certain property of the object or list. With this select() function, your page or items will only get build, if the certain property changes.

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(listProvider.select((value) => value.length)),
        itemBuilder: (_, index){
          print("...list view index $index...");
          .........................................................
          ........................................................   
          ........................................................
        }
    );
  }

Getting the index

Since indexProvider would get called in every ListView.builder function, we would be able to grab the index insdie _ListItem() build method. Let's take a look

  final int index = ref.read(indexProvider);
  final item = ref.watch(listProvider)[index];

This index we get synchronously from ListView.builder. 

Since we have separated ListView.builder, made widgets const and used select() function to specify when to rebuild ListView, then our ListView is totally optimized and one fo the main reason is Riverpod.

Complete code

final listProvider = StateProvider<List<String>>((_) {

  return List.generate(5, (_) => Faker().person.firstName());
});

final indexProvider = Provider<int>((_) {
  return 0;
});

class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print("...page build...");
    return Scaffold(
      appBar: AppBar(),
      body: const _ListView(),
    );
  }
}
class _ListView extends ConsumerWidget {
  const _ListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(listProvider.select((value) => value.length)),
        itemBuilder: (_, index){
          print("...list view index $index...");
          //final list = ref.read(listProvider);

          return ProviderScope(
              overrides: [indexProvider.overrideWith((ref) => index)],
              child: const _ListItem()
          );
        }
    );
  }
}

class _ListItem extends ConsumerWidget {

  const _ListItem() ;

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    final index = ref.read(indexProvider);
    final item = ref.watch(listProvider)[index];
  
    return ElevatedButton(
        onPressed: (){
          final list = ref.read(listProvider);
          print("...list item build...$item");
         // int index = ref.watch(listProvider).indexWhere((element) => element==item);
          list[index]=Faker().person.firstName();
          ref.watch(listProvider.notifier).state=[...list];
        }, child: Text(item)
    );
  }
}


Comment

Add Reviews