使用 ReorderableList 解決 Bloc 在 ListView 中頻繁的關閉與創建問題

最近在開發 Flutter app 時遇到了一個問題:當 ListView 中,每個 child 都使用 BlocProvider,例如:

class MyListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocSelector<RootCubit, RootState, List<SomeObj>>(
      selector: (state) => state.items,
      builder: (_, items) {
        return ListView.builder(
          itemCount: items.length,
          itemBuilder: (_, index) {
            return BlocProvider(
              create: (_) => MyCubit(),
              child: ListTile(
                key: ValueKey(items[index]),
                title: Text(items[index].title),
              ),
            );
          },
        );
      },
    );
  }
}

如果列表的順序變更,child cubit 將會頻繁地被關閉與重新創建。此時會導致所有的 child 都不能再 emit state,否則會發生錯誤:

[ERROR: flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Cannot emit new states after calling close
#0  BlocBase.emit (package:bloc/src/bloc_base.dart:97:9)
#1  MyCubit.fetchData.<anonymous closure> (package: myapp/features/hello/presentation/blocs/cubit.dart:203:13)
#2  GreetingRepository.fetchData(package:myapp/features/hello/infrastructure/repositories.dart:167:20)
‹asynchronous suspension>
#3  GreetingRepository.fetch.<anonymous closure (package:myapp/features/hello/infrastructure/repositories.dart:60:9)
<asynchronous suspension>

即便在每個 child 上增加了唯一的 key,也無法解決這個問題。

解決方案

為了解決這個問題,可以將 ListView 替換為 ReorderableListViewReorderableListView 提供了一個可以自由拖曳調整順序的功能,並且在指定每個 child 的 key 後,它不會隨便重新創建或關閉 cubit。 這樣,就能保持 cubit 狀態的穩定性,不再受到順序變更的影響。

以下是修正後的程式碼:

class MyReorderableListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocSelector<RootCubit, RootState, List<SomeObj>>(
      selector: (state) => state.items,
      builder: (ctx, items) {
        return ReorderableListView.builder(
          onReorder: (oldIndex, newIndex) {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            ctx.read<RootCubit>().moveItem(oldIndex, newIndex);
          },
          itemCount: items.length,
          itemBuilder: (_, index) {
            return BlocProvider(
              key: ValueKey(items[index]), // 保證每個 child 有唯一的 key
              create: (_) => MyCubit(),
              child: ListTile(
                title: Text(items[index].title),
              ),
            );
          },
        );
      },
    );
  }
}

後記

我不太確定如果今天狀態管理是使用 ProviderRiverpod,是否也會有相同的問題。 因為一般 flutter_bloc 的 state 基本都是 immutable,是否是因為這個原因,讓 ListView 在順序變更時,判定 child 需要重新創建。 但是,如果有的話,這個解決方案也能夠解決。