Reputation: 374
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
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
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
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