Jaween
Jaween

Reputation: 374

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?

Upvotes: 0

Views: 2286

Answers (3)

Syed Laiq Afzal
Syed Laiq Afzal

Reputation: 84

I created a todo app using riverpod, so I used ref.watch(todoController.select((value) => value.length) to rebuild todo listView whenever todo list length changes, but for update I used ref.watch(todoController.select((value) => value[index]) to rebuild only that listTile whose item changes, it was working fine but when deleted any item was getting the invalid range error, so what i did is that in ListTile provider.select added a condition todoController.select((value) => value.length > index ? value[index]: null), and now not getting error on deleting, on update only listTile rebuild which needs to and on creating and delete whole listview rebuild w/o invalid range exception

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print('rebuild todo List');
    final todosItem = ref.watch(todoController.select((value) => value.length));
    return ListView.builder(
        itemCount: todosItem,
        itemBuilder: ((context, index) => TodoWidget(index: index,)
      ));
  }
}

class TodoWidget extends ConsumerWidget {
  final int index;
  const TodoWidget({required this.index, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final currentTodo =
        ref.watch(todoController.select((value) => value.length > index ? value[index]: null));
    print('rebuild Current todo');

    return InkWell(
      onDoubleTap: () => ref
          .read(todoController.notifier)
          .deleteTodo(currentTodo!.id),
      onTap: () => ref
          .read(todoController.notifier)
          .updateTodo('change', 'todo is changed', currentTodo!.id),
      child: ListTile(
        title: Text(currentTodo!.name),
        subtitle: Text(currentTodo.desc),
      ),
    );
  }
}

Upvotes: 1

TmKVU
TmKVU

Reputation: 3000

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),
          ),
        );
      },
    );
  }
}

Upvotes: 1

Josteve Adekanbi
Josteve Adekanbi

Reputation: 12703

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),
          ),
        );
      },
    );
  }
}

Upvotes: 0

Related Questions