Flutter中CircularProgressIndicator在StreamBuilder下动画失效问题
问题:StreamBuilder导致CircularProgressIndicator停止动画
我有两个Widget,均依赖同一个Future来显示CircularProgressIndicator,待Future完成后展示目标Widget。但CircularProgressIndicator无法正常动画,仅显示为小方块。仅在完整编译后的首次运行时动画正常,之后即使刷新应用也不再动画。我已测试其他动画Widget,确认它们均处于静态状态。
相关Widget代码如下:
@override Widget build(BuildContext context) { _mapLoadedNotifier = ref.read(mapLoadedProvider.notifier); _mapLoaded = ref.watch(mapLoadedProvider); theVm = ref.watch(searchPageVmProvider); return Row(children: [ _mapOverlay(),_mapWidget(),]); } Widget _mapOverlay() { return theVm.when( data: (store) { vm = store; if (!vm!.hasSearched) { vm!.isMapLoading = true; } vm!.ref = ref; return SearchFormBuilder( initState: vm!.initState, model: Search(search: SearchByTermSuggestion('')), builder: (context, formModel, child) { return Container( padding: PAD_M_TOP, alignment: Alignment.topLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: PAD_M), width: sizingInfo.maxWidthS, color: Colors.transparent, child: VpCombinationAutocompleteField( formControl: formModel.searchControl, labelText: '', hintText: 'Search', widgetRef: vm!.ref!, widgetOnSuggestionSelected: (Suggestion suggestion) => suggestion.onSelected()))); }); }, error: (error, s) => Text(error.toString()), loading: () { return const Center( child: SizedBox( height: 30, width: 30, child: Center(child: CircularProgressIndicator())), ); }); } Widget _mapWidget() { return FutureBuilder<SearchPageVm>( future: ref.watch(searchPageVmProvider.future), builder: ((context, snapshot) { if (!snapshot.hasData) { return const Center( child: SizedBox( height: 30, width: 30, child: Center(child: CircularProgressIndicator())), ); } vm = snapshot.data; return StreamBuilder<dynamic>( stream: vm!.map$, builder: (context, AsyncSnapshot snapshot) { if (snapshot.hasData) { vm!.vpMap = snapshot.data; if (!vm!.hasSearched) { vm!.isMapLoading = true; } vm!.ref = ref; } return _googleMap(vm!); }); })); }
移除StreamBuilder后,两个CircularProgressIndicator均能正常动画。这并非Riverpod的问题,使用普通Flutter FutureBuilder时也会遇到相同情况。尝试过多种替代方案,但StreamBuilder始终会导致FutureBuilder中的CircularProgressIndicator停止动画,需明确原因。
Provider代码如下:
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:vepo/src/_common/extensions/vegan_item_establishments.dart'; import '../../../_common/constants/presentation/colors.dart'; import '../../../_common/enums/vegan_item_type.dart'; import '../../../_common/services/theme_mode.dart'; import '../../../_common/services/user.dart'; import '../../../application/local/form_capabilities.dart'; import '../../../application/local/map_data/map.dart'; import '../../../application/local/suggestion/search_by_term/search_by_term_suggestion.dart'; import '../../../application/search_params/vegan_item/vegan_item_search_params.dart'; import '../../../domain/vegan_item_establishment/_common/vegan_item_establishment.dart'; import 'search.dart'; import 'search_results_provider.dart'; final searchPageVmProvider = FutureProvider<SearchPageVm>((ref) async { final userWithLocation = await ref.watch(userWithLocationProvider.future); final vm = SearchPageVm( userWithLocation: userWithLocation, ); vm.search(); return vm; }); class SearchPageVm { bool hasSearched = false; late GoogleMap? googleMapWidget; bool isMapLoading = true; VpMap? vpMap; WidgetRef? ref; ThemeMode? themeMode; Color? statusBarColor; String? searchText; Stream<VpMap>? map$; late final StreamController<VpMap> mapController = StreamController.broadcast(); late UserWithLocation _userWithLocation; Set<Marker> markers = <Marker>{}; Stream<String?>? searchTerm; GoogleMapController? googleMapController; SearchPageVm({required UserWithLocation userWithLocation}) { map$ = mapController.stream; _userWithLocation = userWithLocation; } @override Search get model => Search(search: SearchByTermSuggestion('')); Position get userPosition => _userWithLocation.userPosition; /// Add an array of search results to the map void addResults(Iterable<VgnItmEst> searchResults) { mapController.add(VpMap( position: _userWithLocation.userPosition, markers: searchResults.markers)); _animateCamera(searchResults); } @override void initState(BuildContext context, SearchForm formModel, [dynamic args]) { if (!hasSearched) { search(); hasSearched = true; } themeMode = ref?.read(themeModeServiceProvider); statusBarColor = themeMode == ThemeMode.light ? BACKGROUND_COLOR : DARK_THEME_BACKGROUND_COLOR_LIGHT; } /// Search for results and add them to the map Future<List<VgnItmEst>?> search( {String searchTerm = '', VgnItmType? category}) async { final searchResults = await _search(searchTerm: searchTerm, category: category); addResults(searchResults ?? []); return searchResults; } void _animateCamera(Iterable<VgnItmEst> searchResults) { googleMapController?.animateCamera(CameraUpdate.newCameraPosition( CameraPosition( target: LatLng( searchResults.first.establishment!.location!.latitude, searchResults.first.establishment!.location!.longitude), zoom: 13))); } Future<List<VgnItmEst>?> _search( {required String searchTerm, VgnItmType? category}) async { final result = await ref?.watch(searchResultsProvider(VgnItmSearchParams( searchTerm: searchTerm, page: 1, pageSize: 10, vgnItmDiscriminators: category != null ? [category.name] : null)) .future); return result; } } final searchResultsProvider = FutureProvider.family<List<VgnItmEst>, VgnItmSearchParams>( (ref, searchParams) async { List<VgnItmEst>? searchResults; final allVgnItmsGooglePlacesRepo = ref.read(allVgnItmEstsRepoProvider); searchResults = (await allVgnItmsGooglePlacesRepo.search(searchParams))?.data; return searchResults!; });
原因分析
核心问题在于StreamBuilder的频繁重建或流订阅逻辑干扰了动画帧调度,具体细节:
searchPageVmProvider的Future完成后,_mapWidget中的FutureBuilder会渲染StreamBuilder。而mapController是广播流,首次运行时发送数据,但后续刷新时,StreamBuilder因Widget树重建重复订阅,导致FutureBuilder的loading状态未被正确触发,进度条的AnimationController无法初始化动画。CircularProgressIndicator的动画依赖Flutter的SchedulerBinding触发帧更新,若Stream频繁发送事件或Widget重建逻辑阻塞事件循环,会导致动画帧被跳过,进度条变为静态方块。- Riverpod的
FutureProvider会缓存结果,后续刷新时FutureBuilder直接进入data状态,进度条没有机会重新初始化动画。
解决方案
方案1:优化流订阅逻辑,避免嵌套Builder
将流监听逻辑移到Widget的生命周期方法中,手动管理状态更新,避免FutureBuilder嵌套StreamBuilder:
// 转为StatefulWidget后,在initState中监听流 @override void initState() { super.initState(); ref.listen(searchPageVmProvider, (previous, next) { next.whenData((vm) { vm.map$?.listen((mapData) { setState(() { vm.vpMap = mapData; }); }); }); }); } // 重构_mapWidget Widget _mapWidget() { return ref.watch(searchPageVmProvider).when( data: (vm) { return _googleMap(vm); }, loading: () => const Center( child: SizedBox( height: 30, width: 30, child: CircularProgressIndicator(), ), ), error: (e, s) => Text(e.toString()), ); }
方案2:用Riverpod StreamProvider管理流
将ViewModel中的手动流改为Riverpod的StreamProvider,让框架管理流的生命周期:
// 新增StreamProvider final mapStreamProvider = StreamProvider<VpMap>((ref) async* { final vm = await ref.watch(searchPageVmProvider.future); yield* vm.map$!; }); // 使用该Provider重构_mapWidget Widget _mapWidget() { return ref.watch(searchPageVmProvider).when( data: (vm) { return ref.watch(mapStreamProvider).when( data: (mapData) { vm.vpMap = mapData; return _googleMap(vm); }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, s) => Text(e.toString()), ); }, loading: () => const Center( child: SizedBox( height: 30, width: 30, child: CircularProgressIndicator(), ), ), error: (e, s) => Text(e.toString()), ); }
方案3:强制重建进度条组件
给CircularProgressIndicator添加唯一Key,每次进入loading状态时生成新Key,强制Flutter重新创建组件并初始化动画:
// 在Widget类中定义Key变量 UniqueKey _progressKey = UniqueKey(); // 在loading状态中使用该Key loading: () { // 每次进入loading状态更新Key _progressKey = UniqueKey(); return Center( child: SizedBox( height: 30, width: 30, child: CircularProgressIndicator(key: _progressKey), ), ); }
内容的提问来源于stack exchange,提问作者BeniaminoBaggins
相关产品推荐
相关产品推荐

