'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 |
