'Riverpod select() runs before list view children are rebuilt

I am using Riverpod (package:flutter_riverpod v1.0.3) to manage state in my Flutter app. I would like to have a list of Widgets built based on the items in a model. Each list item Widget uses provider.select to pick the corresponding model item at its index. See the following example app:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const MyApp());
}

final provider = StateNotifierProvider<FruitStateNotifier, List<String>>((ref) {
  return FruitStateNotifier(['apricot', 'blueberry', 'cherry']);
});

class FruitStateNotifier extends StateNotifier<List<String>> {
  FruitStateNotifier(List<String> fruits) : super(fruits);

  void update(List<String> fruits) => state = fruits;
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const ProviderScope(
      child: MaterialApp(
        home: Scaffold(
          body: FruitList(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
      itemCount: ref.watch(provider.select((p) => p.length)),
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('$index) ${ref.watch(provider.select((p) => p[index]))}'),
          trailing: IconButton(
            onPressed: () {
              final fruits = ref.read(provider);
              final newFruits = List.of(fruits)..removeAt(index);
              ref.read(provider.notifier).update(newFruits);
            },
            icon: const Icon(Icons.delete),
          ),
        );
      },
    );
  }
}

However, when deleting an element, the select functions are run, but it seems that the ListView's item count has not yet been updated to match the model's item count. This causes the last Widget in the list to have no corresponding model item, and so we get an error:

[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: An exception was thrown while building StateNotifierProvider<FruitStateNotifier, List<String>>#82be7.
Thrown exception:
RangeError (index): Invalid value: Not in inclusive range 0..1: 2
Stack trace:
#0      List.[] (dart:core-patch/growable_array.dart:281:36)
#1      FruitList.build.<anonymous closure>.<anonymous closure>
#2      _ProviderSelector._select.<anonymous closure>
#3      ResultData.map
#4      _ProviderSelector._select
#5      _ProviderSelector._selectOnChange
#6      _ProviderSelector.listen.<anonymous closure>
#7      _rootRunBinary (dart:async/zone.dart:1450:47)
#8      _CustomZone.runBinary (dart:async/zone.dart:1342:19)
#9      _CustomZone.runBinaryGuarded (dart:async/zone.dart:1252:7)
#10     ProviderElementBase._notifyListeners.<anonymous closure>
#11     ResultData.map
#12     ProviderElementBase._notifyListeners
#13     ProviderElementBase.setState
#14     StateNotifierProvider.create.listener
#15     StateNotifier.state=
#16     FruitStateNotifier.update
#17     FruitList.build.<anonymous closure>.<anonymous closure>
#18     _InkResponseState._handleTap
#19     GestureRecognizer.invokeCallback
#20     TapGestureRecognizer.handleTapUp
#21     BaseTapGestureRecognizer._checkUp
#22     BaseTapGestureRecognizer.handlePrimaryPointer
#23     PrimaryPointerGestureRecognizer.handleEvent
#24     PointerRouter._dispatch
#25     PointerRouter._dispatchEventToRoutes.<anonymous closure>
#26     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:539:8)
#27     PointerRouter._dispatchEventToRoutes
#28     PointerRouter.route
#29     GestureBinding.handleEvent
#30     GestureBinding.dispatchEvent
#31     RendererBinding.dispatchEvent
#32     GestureBinding._handlePointerEventImmediately
#33     GestureBinding.handlePointerEvent
#34     GestureBinding._flushPointerEventQueue
#35     GestureBinding._handlePointerDataPacket
#36     _rootRunUnary (dart:async/zone.dart:1442:13)
#37     _CustomZone.runUnary (dart:async/zone.dart:1335:19)
#38     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1244:7)
#39     _invoke1 (dart:ui/hooks.dart:170:10)
#40     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:331:7)
#41     _dispatchPointerDataPacket (dart:ui/hooks.dart:94:31)
#0      _fallbackOnErrorForProvider
#1      _ProviderSelector.listen
#2      ProviderContainer.listen
#3      ConsumerStatefulElement.watch.<anonymous closure>
#4      _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:453:23)
#5      ConsumerStatefulElement.watch
#6      FruitList.build.<anonymous closure>
#7      SliverChildBuilderDelegate.build
#8      SliverMultiBoxAdaptorElement._build
#9      SliverMultiBoxAdaptorElement.createChild.<anonymous closure>
#10     BuildOwner.buildScope
#11     SliverMultiBoxAdaptorElement.createChild
#12     RenderSliverMultiBoxAdaptor._createOrObtainChild.<anonymous closure>
#13     RenderObject.invokeLayoutCallback.<anonymous closure>
#14     PipelineOwner._enableMutationsToDirtySubtrees
#15     RenderObject.invokeLayoutCallback
#16     RenderSliverMultiBoxAdaptor._createOrObtainChild
#17     RenderSliverMultiBoxAdaptor.insertAndLayoutChild
#18     RenderSliverList.performLayout.advance
#19     RenderSliverList.performLayout
#20     RenderObject.layout
#21     RenderSliverEdgeInsetsPadding.performLayout
#22     RenderSliverPadding.performLayout
#23     RenderObject.layout
#24     RenderViewportBase.layoutChildSequence
#25     RenderViewport._attemptLayout
#26     RenderViewport.performLayout
#27     RenderObject.layout
#28     RenderProxyBoxMixin.performLayout
#29     RenderObject.layout
#30     RenderProxyBoxMixin.performLayout
#31     RenderObject.layout
#32     RenderProxyBoxMixin.performLayout
#33     RenderObject.layout
#34     RenderProxyBoxMixin.performLayout
#35     RenderObject.layout
#36     RenderProxyBoxMixin.performLayout
#37     RenderObject.layout
#38     RenderProxyBoxMixin.performLayout
#39     RenderObject.layout
#40     RenderProxyBoxMixin.performLayout
#41     RenderObject.layout
#42     RenderProxyBoxMixin.performLayout
#43     RenderCustomPaint.performLayout
#44     RenderObject.layout
#45     RenderProxyBoxMixin.performLayout
#46     RenderObject.layout
#47     RenderProxyBoxMixin.performLayout
#48     RenderObject.layout
#49     RenderProxyBoxMixin.performLayout
#50     RenderObject.layout
#51     RenderProxyBoxMixin.performLayout
#52     RenderObject.layout
#53     MultiChildLayoutDelegate.layoutChild
#54     _ScaffoldLayout.performLayout
#55     MultiChildLayoutDelegate._callPerformLayout
#56     RenderCustomMultiChildLayoutBox.performLayout
#57     RenderObject.layout
#58     RenderProxyBoxMixin.performLayout
#59     RenderObject.layout
#60     RenderProxyBoxMixin.performLayout
#61     _RenderCustomClip.performLayout
#62     RenderObject.layout
#63     RenderProxyBoxMixin.performLayout
#64     RenderObject.layout
#65     RenderProxyBoxMixin.performLayout
#66     RenderObject.layout
#67     RenderProxyBoxMixin.performLayout
#68     RenderObject.layout
#69     RenderProxyBoxMixin.performLayout
#70     RenderObject.layout
#71     RenderProxyBoxMixin.performLayout
#72     RenderObject.layout
#73     RenderProxyBoxMixin.performLayout
#74     RenderObject.layout
#75     RenderProxyBoxMixin.performLayout
#76     RenderObject.layout
#77     RenderProxyBoxMixin.performLayout
#78     RenderObject.layout
#79     RenderProxyBoxMixin.performLayout
#80     RenderOffstage.performLayout
#81     RenderObject.layout
#82     RenderProxyBoxMixin.performLayout
#83     RenderObject.layout
#84     _RenderTheatre.performLayout
#85     RenderObject.layout
#86     RenderProxyBoxMixin.performLayout
#87     RenderObject.layout
#88     RenderProxyBoxMixin.performLayout
#89     RenderObject.layout
#90     RenderProxyBoxMixin.performLayout
#91     RenderObject.layout
#92     RenderProxyBoxMixin.performLayout
#93     RenderCustomPaint.performLayout
#94     RenderObject.layout
#95     RenderProxyBoxMixin.performLayout
#96     RenderObject.layout
#97     RenderProxyBoxMixin.performLayout
#98     RenderObject.layout
#99     RenderProxyBoxMixin.performLayout
#100    RenderObject.layout
#101    RenderProxyBoxMixin.performLayout
#102    RenderObject.layout
#103    RenderView.performLayout
#104    RenderObject._layoutWithoutResize
#105    PipelineOwner.flushLayout
#106    RendererBinding.drawFrame
#107    WidgetsBinding.drawFrame
#108    RendererBinding._handlePersistentFrameCallback
#109    SchedulerBinding._invokeFrameCallback
#110    SchedulerBinding.handleDrawFrame
#111    SchedulerBinding.scheduleWarmUpFrame.<anonymous closure>
#112    _rootRun (dart:async/zone.dart:1418:47)
#113    _CustomZone.run (dart:async/zone.dart:1328:19)
#114    _CustomZone.runGuarded (dart:async/zone.dart:1236:7)
#115    _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1276:23)
#116    _rootRun (dart:async/zone.dart:1426:13)
#117    _CustomZone.run (dart:async/zone.dart:1328:19)
#118    _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1260:23)
#119    Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
#120    _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
#121    _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
#122    _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

How can I build the list and its children without this issue?



Solution 1:[1]

Using select here does not really make sense, you are not optimizing rebuilds, since it will be rebuilt when the number of items in the list changes anyway.

The best you can do here is just watch the state once and assign it to a variable:

final fruitList = ref.watch(provider);

Then just use the variable to get the length and build the list items.

So the resulting code will be:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final fruitList = ref.watch(provider);
    return ListView.builder(
      itemCount: fruitList.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('$index) ${fruitList[index]}'),
          trailing: IconButton(
            onPressed: () {
              final newFruits = List.of(fruitList)..removeAt(index);
              ref.read(provider.notifier).update(newFruits);
            },
            icon: const Icon(Icons.delete),
          ),
        );
      },
    );
  }
}

Solution 2:[2]

Try this:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final list = ref.watch(provider);
    return ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('$index) ${list[index]}'),
          trailing: IconButton(
            onPressed: () {
              final fruits = ref.read(provider);
              final newFruits = List.of(fruits)..removeAt(index);
              ref.read(provider.notifier).update(newFruits);
            },
            icon: const Icon(Icons.delete),
          ),
        );
      },
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Josteve