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

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的频繁重建或流订阅逻辑干扰了动画帧调度,具体细节:

  1. searchPageVmProvider的Future完成后,_mapWidget中的FutureBuilder会渲染StreamBuilder。而mapController是广播流,首次运行时发送数据,但后续刷新时,StreamBuilder因Widget树重建重复订阅,导致FutureBuilder的loading状态未被正确触发,进度条的AnimationController无法初始化动画。
  2. CircularProgressIndicator的动画依赖Flutter的SchedulerBinding触发帧更新,若Stream频繁发送事件或Widget重建逻辑阻塞事件循环,会导致动画帧被跳过,进度条变为静态方块。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 21:01:44