I'm rewriting one of my projects I use for learning from SwiftUI to Flutter and I'm stuck at one part where I read data from Firebase Realtime Database. What I do is that I anonymously authorize to Firebase and download "Tests" from Firebase. Once downloaded, I store them in the shared preferences. If the tests were downloaded already that day, instead if downloading them again, I'm loading them from shared preference. The thing is that one I call the function in my ViewModel to fetch the tests, I'm stuck in the loop and even though I use asynchronous functions and await the download from Firebase it for some reason always returns me back to authAndDownload() function. The thing is that after some time, I properly download the test but still being stuck in the loop and not showing the correct screen to user (based on condition on the view).
Here is the Firebase Repository code:
class FirebaseRepository {
final DatabaseReference databaseRef = FirebaseDatabase.instance.ref();
final String path = 'tests';
late String? userID;
Future<void> signInAnonymously() async {
if (FirebaseAuth.instance.currentUser != null) {
userID = FirebaseAuth.instance.currentUser!.uid;
print('User already logged in with userID: $userID');
} else {
try {
UserCredential userCredential =
await FirebaseAuth.instance.signInAnonymously();
userID = userCredential.user!.uid;
print('User logged in anonymously with UID $userID');
} catch (error) {
rethrow;
}
}
}
Future<Test> readTodayTest() async {
String todayDateString = formatDateString(date: DateTime.now().toUtc());
DatabaseReference testRef = databaseRef.child(path).child(todayDateString);
try {
DataSnapshot snapshot = await testRef.get();
if (snapshot.exists) {
String key = snapshot.key!;
DataSnapshot aSnapshot = snapshot.child('a');
DataSnapshot bSnapshot = snapshot.child('b');
DataSnapshot cSnapshot = snapshot.child('c');
return Test(
a: aSnapshot.value as String,
b: bSnapshot.value as String,
c: cSnapshot.value as String,
testDate: key,
);
} else {
throw Exception("No test found for today");
}
} catch (error) {
rethrow;
}
}
Future<List<Test>> readLast30Tests() async {
List<Test> tests = [];
DatabaseReference testRef = databaseRef.child(path);
try {
DataSnapshot snapshot = await testRef.limitToLast(30).get();
if (snapshot.exists) {
print("Snapshot has data");
for (final testSnapshot in snapshot.children) {
final key = testSnapshot.key!;
final aSnapshot = testSnapshot.child("a");
final bSnapshot = testSnapshot.child("b");
final cSnapshot = testSnapshot.child("c");
Test test = Test(
testDate: key,
a: aSnapshot.value as String,
b: bSnapshot.value as String,
c: cSnapshot.value as String,
);
tests.add(test);
}
} else {
print("Snapshot has no data");
}
return tests;
} catch (error) {
rethrow;
}
}
DateTime getCurrentDateUTC() {
DateTime currentDate = DateTime.now();
final currentDateUTC = currentDate.toUtc();
return currentDateUTC;
}
String formatDateString({required DateTime date}) {
return DateFormat('yyMMdd').format(date);
}
}
This is my ViewModel I use to get the data and store them:
class IntroViewModel extends ChangeNotifier {
List<Test> Tests = [];
String? alertError;
final String storedTestsKey = "storedTests";
final String lastDownloadedKey = "LastDownloadedDate";
final FirebaseRepository firebaseRepository = FirebaseRepository();
Future<void> authAndDownload() async {
try {
await firebaseRepository.signInAnonymously();
if (await isTestsDownloadedForToday() == false) {
await downloadTests();
await saveLastDownloadedDate();
} else {
await getStoredTests();
}
} catch (error) {
alertError = error.toString();
notifyListeners();
}
}
Future<void> downloadTests() async {
try {
Test todayTest = await firebaseRepository.readTodayTest();
List<Test> last30Tests = await firebaseRepository.readLast30Tests();
Tests = [todayTest, ...last30Tests];
await storeDownloadedTests(TestData: Tests);
} 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);
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)");
}
}
}
And this is the initial view where I decide which screen should be displayed:
class IntroView extends StatelessWidget {
const IntroView({super.key});
@override
Widget build(BuildContext context) {
final SettingsViewModel settingsViewModel =
Provider.of<SettingsViewModel>(context);
final IntroViewModel introViewModel = Provider.of<IntroViewModel>(context);
return FutureBuilder<void>(
future: introViewModel.authAndDownload(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (settingsViewModel.userOnboarded) {
return MainTabView();
} else {
return OnboardingView();
}
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
}
I've spent few hours debugging the problem and I'm sure I'm just overlooking something really obvious. Any help will be really appreciated!
OK the answer was obviously a simple one - I should check docs more often. As mentioned in official docs (https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html) the future must have been obtained earlier, e.g. during State.initState,... which was the problem in the case above.
I've changed the widget to be Stateful and assigned future in initState and suddenly all works correctly. This is example how this can be done.
class _IntroViewState extends State<IntroView> {
Future<void>? _authAndDownloadFuture;
IntroViewModel? _introViewModel;
@override
void initState() {
_introViewModel = Provider.of<IntroViewModel>(context, listen: false);
_authAndDownloadFuture = _introViewModel?.authAndDownload();
super.initState();
}
...
}