Flutter项目从Firebase实时数据库下载数据时陷入循环求助
Flutter Firebase 数据读取循环问题排查与修复
问题描述
将SwiftUI学习项目迁移到Flutter时,Firebase Realtime Database数据读取环节出现循环问题:逻辑是先匿名登录Firebase,下载"Tests"数据存到SharedPreferences;若当日已下载则读本地缓存。但调用ViewModel的authAndDownload()时程序陷入循环,反复执行该函数,数据下载成功后仍无法正常跳转页面。
问题根源
- FutureBuilder重复触发异步任务:
IntroView的build方法中直接传入introViewModel.authAndDownload(),每次Widget重建(比如ViewModel调用notifyListeners()、父组件刷新)都会重新执行这个异步函数,导致无限循环。 - 状态通知不完整:
authAndDownload()和downloadTests()完成后未及时调用notifyListeners(),视图无法感知状态变化。 - 异步任务无缓存:没有对
authAndDownload()的执行结果做缓存,导致每次重建都重复发起请求。
修复方案
1. 缓存异步任务避免重复执行
在ViewModel中缓存异步任务的Future实例,确保仅执行一次。
2. 完善状态通知逻辑
在数据加载完成、状态变更时调用notifyListeners(),让视图及时更新。
3. 将IntroView改为StatefulWidget
在initState中触发异步任务,避免build方法重复调用。
4. 规范数据序列化逻辑
确保Test类实现toJson和fromJson方法,保证SharedPreferences存储与解析正常。
修改后的代码示例
1. 更新IntroViewModel
class IntroViewModel extends ChangeNotifier { List<Test> tests = []; String? alertError; bool isLoading = false; final String storedTestsKey = "storedTests"; final String lastDownloadedKey = "LastDownloadedDate"; final FirebaseRepository firebaseRepository = FirebaseRepository(); Future<void>? _authAndDownloadFuture; Future<void> authAndDownload() async { if (_authAndDownloadFuture != null) return; isLoading = true; notifyListeners(); try { _authAndDownloadFuture = _performAuthAndDownload(); await _authAndDownloadFuture; } catch (error) { alertError = error.toString(); } finally { isLoading = false; notifyListeners(); } } Future<void> _performAuthAndDownload() async { await firebaseRepository.signInAnonymously(); if (!await isTestsDownloadedForToday()) { await downloadTests(); await saveLastDownloadedDate(); } else { await getStoredTests(); } } Future<void> downloadTests() async { try { Test todayTest = await firebaseRepository.readTodayTest(); List<Test> last30Tests = await firebaseRepository.readLast30Tests(); tests = [todayTest, ...last30Tests]; await storeDownloadedTests(testData: tests); notifyListeners(); } catch (error) { alertError = error.toString(); notifyListeners(); } } Future<bool> isTestsDownloadedForToday() async { final currentDate = firebaseRepository.getCurrentDateUTC(); final String? lastDownloadedDate = await getLastDownloadedDate(); final currentDateString = firebaseRepository.formatDateString(date: currentDate); return currentDateString == lastDownloadedDate; } Future<String?> getLastDownloadedDate() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString(lastDownloadedKey); } Future<void> saveLastDownloadedDate() async { final currentDate = firebaseRepository.getCurrentDateUTC(); final currentDateString = firebaseRepository.formatDateString(date: currentDate); final prefs = await SharedPreferences.getInstance(); await prefs.setString(lastDownloadedKey, currentDateString); } Future<void> storeDownloadedTests({required List<Test> testData}) async { try { SharedPreferences prefs = await SharedPreferences.getInstance(); String encodedData = json.encode(testData.map((e) => e.toJson()).toList()); await prefs.setString(storedTestsKey, encodedData); } catch (error) {} } Future<void> getStoredTests() async { try { final prefs = await SharedPreferences.getInstance(); final String? data = prefs.getString(storedTestsKey); if (data != null) { final List<dynamic> decodedTests = json.decode(data); tests = decodedTests.map((json) => Test.fromJson(json)).toList(); notifyListeners(); } } catch (error) { print("Unable to process decoding ($error)"); } } }
2. 修改IntroView为StatefulWidget
class IntroView extends StatefulWidget { const IntroView({super.key}); @override State<IntroView> createState() => _IntroViewState(); } class _IntroViewState extends State<IntroView> { @override void initState() { super.initState(); Provider.of<IntroViewModel>(context, listen: false).authAndDownload(); } @override Widget build(BuildContext context) { final SettingsViewModel settingsViewModel = Provider.of<SettingsViewModel>(context); final IntroViewModel introViewModel = Provider.of<IntroViewModel>(context); if (introViewModel.isLoading) { return const Center(child: CircularProgressIndicator()); } if (introViewModel.alertError != null) { return Center(child: Text(introViewModel.alertError!)); } return settingsViewModel.userOnboarded ? const MainTabView() : const OnboardingView(); } }
3. 完善Test类序列化
class Test { final String a; final String b; final String c; final String testDate; Test({required this.a, required this.b, required this.c, required this.testDate}); Map<String, dynamic> toJson() { return { 'a': a, 'b': b, 'c': c, 'testDate': testDate, }; } factory Test.fromJson(Map<String, dynamic> json) { return Test( a: json['a'], b: json['b'], c: json['c'], testDate: json['testDate'], ); } }
内容的提问来源于stack exchange,提问作者Petr G
相关产品推荐
相关产品推荐

