You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Debug模式下已移除的Riverpod组件仍执行FamilyProvider选择器

问题:移除列表项时Riverpod FamilyProvider抛出"Bad state: No element"错误

问题描述

状态中包含用户可修改的按钮列表,添加新按钮正常,但移除按钮时,对应按钮组件仍会执行buttonStateFamilyProvider的选择器逻辑,此时目标按钮已从列表中移除,firstWhere因找不到元素抛出Error: Bad state: No element异常。

复现环境

  • 仅在Windows、Chrome的Debug构建版本中触发
  • Release模式及dartpad.dev中无法复现

复现步骤

  1. 运行Debug版本应用
  2. 点击任意按钮将其从列表中移除
  3. 立即触发上述错误

原因分析

点击按钮移除自身时,AppState更新后,MyHomePage会重建并移除对应的_ButtonWidget,但在组件从树中完全移除前,autoDispose的Family Provider仍会收到AppState更新通知,此时执行firstWhere找不到对应ID的按钮,直接抛出异常。

解决方案

核心修改:给firstWhere添加orElse避免抛出异常

修改Family Provider中的选择器逻辑,添加orElse返回null,同时在组件中处理null情况,避免报错;另外修正ButtonState的Equatable实现,确保状态判断准确。

修改后的完整代码

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

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final buttons = ref.watch(appStateProvider).buttons;

    print('Rebuild HomePage with $buttons');

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Click on any of the buttons to remove the widget'),
            for (final btn in buttons) _ButtonWidget(id: btn.id),
          ],
        ),
      ),
    );
  }
}

class _ButtonWidget extends ConsumerWidget {
  final int id;

  const _ButtonWidget({
    required this.id,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    print('Rebuild ButtonWidget $id');

    final buttonState = ref.watch(buttonStateFamilyProvider(id));

    // 若按钮已被移除,返回空组件避免UI报错
    if (buttonState == null) {
      return const SizedBox.shrink();
    }

    return ElevatedButton(
      onPressed: () {
        final appStateController = ref.read(appStateProvider.notifier);
        final newList = appStateController.state.buttons.toList()
          ..removeWhere((btn) => btn.id == id);

        appStateController.state = AppState(newList);
      },
      style: ElevatedButton.styleFrom(backgroundColor: Colors.blueGrey),
      child: Text(buttonState.name),
    );
  }
}

final appStateProvider = StateProvider<AppState>((ref) {
  return const AppState([
    ButtonState(1, 'Button 1'),
    ButtonState(2, 'Button 2'),
  ]);
});

final buttonStateFamilyProvider = Provider.family.autoDispose<ButtonState?, int>(
  (ref, id) {
    ref.onCancel(() {
      print('cancelled buttonStateFamilyProvider($id)');
    });

    print('Executing buttonStateFamilyProvider($id)');

    final result = ref.watch(
      appStateProvider.select(
        (state) => state.buttons.firstWhere(
          (btn) => btn.id == id,
          orElse: () => null, // 添加orElse避免找不到元素时抛出异常
        ),
      ),
    );

    print('buttonStateFamilyProvider($id) = $result');

    return result;
  },
);

class AppState with EquatableMixin {
  final List<ButtonState> buttons;

  const AppState(this.buttons);

  @override
  List<Object?> get props => [buttons];
}

class ButtonState with EquatableMixin {
  final String name;
  final int id;

  const ButtonState(this.id, this.name);

  // 修正:将id加入props,确保Equatable能正确判断按钮唯一性
  @override
  List<Object?> get props => [id, name];
}

关键修改说明

  1. Family Provider返回类型调整:改为ButtonState?,适配orElse返回null的情况
  2. 添加orElse参数:在firstWhere中指定orElse: () => null,避免找不到元素时抛出异常
  3. 组件空状态处理:在_ButtonWidget中判断buttonState是否为null,为空时返回空组件
  4. Equatable逻辑修正:将ButtonState的id加入props列表,确保不同ID的按钮被正确识别为不同对象

pubspec.yaml(无修改)

name: riverpod_widget_rebuild
description: A new Flutter project.

publish_to: 'none' 

version: 1.0.0+1

environment:
  sdk: '>=2.18.5 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  flutter_riverpod: ^2.3.0
  cupertino_icons: ^1.0.2
  equatable: ^2.0.5

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

内容的提问来源于stack exchange,提问作者Matthiee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 12:25:00