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

基于Riverpod展示Sembast记录:添加单条数据时避免全列表加载动画

解决方案:避免Sembast流更新时列表被加载框替换

问题描述

我正在跟着Andrea Bizzotto的《Flutter Riverpod 2.0: The Ultimate Guide》教程,把Sembast数据存储和Flutter UI关联起来。目前我的仓库类用Sembast的Streams特性返回所有记录,这个方法会把整个集合以流的形式返回。控制器里有添加新项的方法,同时会用新集合更新状态。

功能大体正常:能往数据库加项,显示列表也会更新成包含新项的集合。但糟的是,保存/获取数据期间,整个列表会被CircularProgressIndicator替换。我知道原因——整个集合被替换了,加载期间无状态组件没有数据,所以在ConnectionState.waiting阶段没法显示内容。

我想保持列表显示,只添加(以后还要支持删除和编辑)单条数据。没找到这种交互的示例,有没有最佳实践或者演示这个用例的代码示例?


现有代码

仓库代码

@riverpod
ExerciseRepository exerciseRepository(
        ExerciseRepositoryRef ref) =
    ExerciseRepository(database: ref.watch(databaseProvider));

class ExerciseRepository implements Repository<Exercise> {
  final Database database;
  late final StoreRef<int, Map<String, dynamic>> _store;

  ExerciseRepository({required this.database}) {
    _store = intMapStoreFactory.store('exercise_store');
  }

  @override
  Future delete(int exerciseId) =>
      _store.record(exerciseId).delete(database);

  @override
  Stream<List<Exercise>> getAllEntitiesStream() {
    developer.log('getAllEntitiesStream', name: 'ExerciseRepository');
    return _store.query().onSnapshots(database).map(
          (snapshot) =>
          snapshot
              .map((definition) =>
              Exercise.fromJson(definition.value)
                  .copyWith(id: definition.key))
              .toList(growable: false),
        );
  }

  @override
  Future<int> insert(Exercise exercise) {
    developer.log('insert', name: 'ExerciseRepository');
    return _store.add(database, exercise.toJson());
  }

  @override
  Future update(Exercise exercise) {
    developer.log('update', name: 'ExerciseRepository');
    return _store
        .record(exercise.id)
        .update(database, exercise.toJson());
  }
}

@riverpod
Stream<List<Exercise>> getAllEntitiesStream(
    GetAllEntitiesStreamRef ref) {
  developer.log('getAllEntitiesStream',
      name: 'ExerciseRepository@riverpod');
  return ref.watch(exerciseRepositoryProvider).getAllEntitiesStream();
}

@riverpod
Future<int> insert(InsertRef ref,
    {required Exercise exercise}) {
  return ref
      .watch(exerciseRepositoryProvider)
      .insert(exercise);
}

@riverpod
Future delete(DeleteRef ref, {required int exerciseId}) {
  return ref
      .watch(exerciseRepositoryProvider)
      .delete(exerciseId);
}

@riverpod
Future update(UpdateRef ref, {required Exercise exercise}) {
  return ref
      .watch(exerciseRepositoryProvider)
      .update(exercise);
}

控制器代码(原实现)

@riverpod
class ExerciseController extends _$ExerciseController {
  @override
  FutureOr<Stream<List<Exercise>>> build() async {
    final definitionsRepository =
        ref.read(exerciseRepositoryProvider);
    return definitionsRepository.getAllEntitiesStream();
  }

  Future<int> createExercise(
      {required String name, required String exerciseType, required String note, ExerciseSetting? setting}) async {
    final definitionsRepository =
        ref.read(exerciseRepositoryProvider);
    final newExercise = Exercise(name: name, exerciseType: exerciseType, note: note, setting: setting);

    state = const AsyncLoading();
    var id = await definitionsRepository.insert(newExercise);

    state = await AsyncValue.guard(() async {
      return definitionsRepository.getAllEntitiesStream();
    });
    return id;
  }

  Future<Stream<List<Exercise>>> getExercises() async {
    final definitionsRepository =
        ref.read(exerciseRepositoryProvider);
    state = const AsyncLoading();
    var definitions = definitionsRepository.getAllEntitiesStream();
    state = await AsyncValue.guard(() async => definitions);
    return definitions;
  }
}

Provider代码

@riverpod
Database database(DatabaseRef ref) {
  throw Exception('Database not initialized');
}

UI代码(原实现)

body: Consumer(builder: (_, WidgetRef ref, __) {
  final workoutDefinitionsAsync = ref.watch(exerciseControllerProvider);

  return workoutDefinitionsAsync.when(
      data: (exercises) =>
          StreamBuilder<List<Exercise>>(
              stream: exercises,
              builder: (BuildContext context,
                  AsyncSnapshot<List<Exercise>> snapshot) {
                if (!snapshot.hasData) {
                  return const Center(child: CircularProgressIndicator());
                }
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return const Center(child: CircularProgressIndicator());
                  default:
                    var items = snapshot.data!;
                    return Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: ListView.builder(
                        shrinkWrap: true,
                        itemCount: items.length,
                        itemBuilder: (BuildContext context, int index) {
                          var entry = items[index];
                          return Card(
                            child: Padding(
                              padding: const EdgeInsets.all(8),
                              child: Column(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  Row(
                                    mainAxisAlignment:
                                        MainAxisAlignment.start,
                                    children: [
                                      Text(
                                        entry.name,
                                        style: Theme.of(context)
                                            .textTheme
                                            .titleMedium,
                                      ),
                                    ],
                                  ),
                                ],
                              ),
                            ),
                          );
                        },
                      ),
                    );
                }
              }),
      error: (e, st) => Text(e.toString()),
      loading: () => const Center(child: CircularProgressIndicator()));
})

优化方案

1. 核心问题分析

  • 控制器错误地将状态设为AsyncLoading()并重新获取流,导致UI触发全局加载
  • Sembast的onSnapshots流本身会自动监听数据库变化,无需手动刷新
  • UI层双重状态监听(控制器的AsyncValue+StreamBuilder)加剧了加载闪烁

2. 优化后的控制器

控制器只负责增删改操作,不再持有流状态:

@riverpod
class ExerciseController extends _$ExerciseController {
  @override
  FutureOr<void> build() {
    // 初始化无需返回内容,仅持有仓库引用
  }

  Future<int> createExercise({
    required String name,
    required String exerciseType,
    required String note,
    ExerciseSetting? setting,
  }) async {
    final repository = ref.read(exerciseRepositoryProvider);
    final newExercise = Exercise(
      name: name,
      exerciseType: exerciseType,
      note: note,
      setting: setting,
    );
    // 直接调用仓库方法,无需修改控制器状态
    return await repository.insert(newExercise);
  }

  Future<void> deleteExercise({required int exerciseId}) async {
    final repository = ref.read(exerciseRepositoryProvider);
    await repository.delete(exerciseId);
  }

  Future<void> updateExercise({required Exercise exercise}) async {
    final repository = ref.read(exerciseRepositoryProvider);
    await repository.update(exercise);
  }
}

3. 优化后的UI代码

直接监听仓库的流Provider,调整加载状态判断逻辑:

body: Consumer(builder: (_, WidgetRef ref, __) {
  final exercisesStream = ref.watch(getAllEntitiesStreamProvider);
  
  return exercisesStream.when(
    data: (stream) => StreamBuilder<List<Exercise>>(
      stream: stream,
      initialData: const [], // 设置初始空列表,避免首次加载空白
      builder: (context, snapshot) {
        final items = snapshot.data ?? [];
        
        // 仅当列表为空且处于等待状态时显示加载框
        if (items.isEmpty && snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        
        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: ListView.builder(
            shrinkWrap: true,
            itemCount: items.length,
            itemBuilder: (context, index) {
              final entry = items[index];
              return Card(
                child: Padding(
                  padding: const EdgeInsets.all(8),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Text(
                            entry.name,
                            style: Theme.of(context).textTheme.titleMedium,
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        );
      },
    ),
    error: (e, st) => Text(e.toString()),
    loading: () => const Center(child: CircularProgressIndicator()),
  );
})

4. 仓库代码保持不变

仓库的流监听逻辑已经正确,增删改操作会自动触发流的更新,无需修改。


优化效果

  • 增删改操作时,列表不会被加载框替换,而是无缝更新新数据
  • 首次加载时如果有历史数据,会直接显示,仅空状态下才显示加载框
  • 代码结构更清晰,符合Riverpod的职责分离原则

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 20:16:01