基于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
相关产品推荐
相关产品推荐

