Riverpod select() 在重建列表视图子项之前运行

Riverpod select() runs before list view children are rebuilt

我正在使用 Riverpod (package:flutter_riverpod v1.0.3) 来管理我的 Flutter 应用程序中的状态。我想要一个基于模型中的项目构建的小部件列表。每个列表项 Widget 使用 provider.select 在其索引处选择相应的模型项。请参阅以下示例应用程序:

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

然而,当删除元素时,select 函数是 运行,但似乎 ListView 的项目计数尚未更新以匹配模型的项目数数。这导致列表中的最后一个 Widget 没有对应的模型项,因此我们得到一个错误:

[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)

如何在没有这个问题的情况下构建列表及其子项?

在这里使用 select 没有任何意义,您没有优化重建,因为无论如何列表中的项目数发生变化时都会重建。

你在这里能做的最好的事情就是只观察一次状态并将其分配给一个变量:

final fruitList = ref.watch(provider);

然后只需使用变量获取长度并构建列表项。

因此生成的代码将是:

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

试试这个:

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