Flutter BLoC:在父 Widget 更新时维护子状态

Posted

技术标签:

【中文标题】Flutter BLoC:在父 Widget 更新时维护子状态【英文标题】:Flutter BLoC: Maintaining Child State when Parent Widget Updates 【发布时间】:2021-02-11 02:31:13 【问题描述】:

我现在有一个应用程序可以创建计时器列表。它使用两个 BLoC,一个用于单个计时器,一个用于整个列表。您可以查看以下列表的示例:

在图像中,您可以看到索引 0 和 2 尚未启动,而索引 1 已启动。我的问题是,每当我从列表中添加或删除项目时,所有计时器都会重置。

当父窗口部件被重绘时,有没有办法让它们的状态保持不变?

这是列表代码:

这里的 Item Tile 包含计时器的 BLoC:

  class HomePage extends StatefulWidget 
  static const TextStyle timerTextStyle = TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
  );

  final Stream shouldTriggerChange;
  HomePage(@required this.shouldTriggerChange);
  @override
  _HomePageState createState() => _HomePageState();


class _HomePageState extends State<HomePage> 
  StreamSubscription streamSubscription;

  @override
  initState() 
    super.initState();
    streamSubscription = widget.shouldTriggerChange.listen((data) 
      createTimer(context, data);
    );
  

  void createTimer(BuildContext context, timer_type timer) 
    BlocProvider.of<ListBloc>(context).add(Add(
        id: DateTime.now().toString(),
        duration: timer == timer_type.timer ? 100 : 0,
        timer: timer,
        timerTextStyle: HomePage.timerTextStyle));
  

  @override
  didUpdateWidget(HomePage old) 
    super.didUpdateWidget(old);
    // in case the stream instance changed, subscribe to the new one
    if (widget.shouldTriggerChange != old.shouldTriggerChange) 
      streamSubscription.cancel();
      streamSubscription = widget.shouldTriggerChange
          .listen((data) => createTimer(context, data));
    
  

  @override
  dispose() 
    super.dispose();
    streamSubscription.cancel();
  

  @override
  Widget build(BuildContext context) 
    return BlocBuilder<ListBloc, ListState>(
      builder: (context, state) 
        if (state is Failure) 
          return Center(
            child: Text('Oops something went wrong!'),
          );
        
        if (state is Loaded) 
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              state.timers.isEmpty
                  ? Center(
                      child: Text('no content'),
                    )
                  : Expanded(
                      child: ListView.builder(
                        itemBuilder: (BuildContext context, int index) 
                          return ItemTile(
                            index: index,
                            timer: state.timers[index],
                            onDeletePressed: (id) 
                              BlocProvider.of<ListBloc>(context)
                                  .add(Delete(id: id));
                            ,
                          );
                        ,
                        itemCount: state.timers.length,
                      ),
                    ),
            ],
          );
        
        return Center(
          child: CircularProgressIndicator(),
        );
      ,
    );
  

最初我认为这是一个关键问题,因为列表没有正确删除,但我相信我已经按照示例中显示的方式实现了键,但问题仍然存在。

class ItemTile extends StatelessWidget 
  final key = UniqueKey();
  final Function(String) onDeletePressed;
  final int index;
  final Timer timer;
  ItemTile(
    Key key,
    @required this.index,
    @required this.timer,
    @required this.onDeletePressed,
  ) : super(key: key);
  @override
  Widget build(BuildContext context) 
    return Container(
      child: Row(
        children: [
          Text('$index.toString()'),
          BlocProvider(
            create: (context) => TimerBloc(ticker: Ticker(), timer: timer),
            child: Container(
              height: (MediaQuery.of(context).size.height - 100) / 5,
              child: Row(
                children: [
                  Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Padding(
                        padding: EdgeInsets.symmetric(vertical: 10.0),
                        child: Center(
                          child: BlocBuilder<TimerBloc, TimerState>(
                            builder: (context, state) 
                              final String minutesStr =
                                  ((state.duration / 60) % 60)
                                      .floor()
                                      .toString()
                                      .padLeft(2, '0');
                              final String secondsStr = (state.duration % 60)
                                  .floor()
                                  .toString()
                                  .padLeft(2, '0');
                              return Text(
                                '$minutesStr:$secondsStr',
                              );
                            ,
                          ),
                        ),
                      ),
                      BlocBuilder<TimerBloc, TimerState>(
                        buildWhen: (previousState, currentState) =>
                            currentState.runtimeType !=
                            previousState.runtimeType,
                        builder: (context, state) => TimerActions(),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          timer.isDeleting
              ? CircularProgressIndicator()
              : IconButton(
                  icon: Icon(Icons.delete, color: Colors.red),
                  onPressed: () 
                    onDeletePressed(timer.id);
                  ,
                ),
        ],
      ),
    );
  

这是在列表 BLoC 中传递的计时器模型:

class Timer extends Equatable 
  final timer_type timer;
  final int duration;
  final String id;
  final bool isDeleting;
  const Timer(
    @required this.id,
    @required this.timer,
    @required this.duration,
    this.isDeleting = false,
  );
  Timer copyWith(timer_type timer, int duration, String id, bool isDeleting) 
    return Timer(
      id: id ?? this.id,
      duration: duration ?? this.duration,
      timer: timer ?? this.timer,
      isDeleting: isDeleting ?? this.isDeleting,
    );
  

  @override
  List<Object> get props => [id, duration, timer, isDeleting];
  @override
  String toString() =>
      'Item  id: $id, duration: $duration, timer type: $timer, isDeleting: $isDeleting ';

任何帮助将不胜感激。

谢谢!

【问题讨论】:

【参考方案1】:

每次重建列表时都会创建一个新的UniqueKey,这会强制删除状态

因此,要修复它,您必须将键与计时器相关联,例如通过将计时器包装在包含计时器和键的类中,如下所示:

class KeyedTimer 
  final key = UniqueKey();
  Timer timer;

所以现在您将拥有一个 KeyedTimer 对象列表而不是计时器列表,并且您可以在列表的 itembuilder 中使用键

【讨论】:

首先感谢您的回复!为了清楚起见,我更新了帖子并包含了整个代码。我试图在 ItemTile 文件中放置一个唯一键,但它仍然每次都被重绘。我是否需要以某种方式将其传递给父母或孩子?我尝试在 listtile 构造函数上启用和禁用 super 关键字。 我能够通过将项目磁贴转换为有状态小部件并将全局键传递给 _ItemTileState 类来保持项目磁贴的状态,但现在它只是删除列表中的最后一个元素,无论我要删除哪个元素。 就像我在答案中所说,您必须将密钥与计时器相关联。 因此将密钥放入您的计时器模型中,这样它就不会在每次构建时都重新创建。但是,我看到每个计时器已经有一个唯一的 id,因此您可以使用 ValueKey(timer.id 而不是将密钥添加到计时器类 是的,我能够解决这个问题,但现在它正在重置已删除项目下方的所有计时器。

以上是关于Flutter BLoC:在父 Widget 更新时维护子状态的主要内容,如果未能解决你的问题,请参考以下文章

flutter_bloc 的 BlocBuilder 能否避免重建 Widget 的未更改部分?

Dart/Flutter 中 BLoC 模式的最佳实践

Flutter Bloc - Flutter Bloc 状态未更新

Flutter BloC 模式:基于另一个 BloC 的流更新 BloC 流

Flutter_bloc 从没有 UI 事件的 firestore 获取更新的数据

Flutter bloc:更新状态不会重新调用 BlocBuilder