I'm using the Provider package to provide a BLoC object (hand-written, not using bloc or flutter_bloc packages) to my Flutter app. I have a few asynchronous calls that need to be made to initialize the BLoC properly (for example, settings and other saved info from SharedPreferences). So far, I've written those async calls into a few separate functions that are called inside my BLoC's constructor:
class MyBloc {
MySettings _settings;
List<MyOtherStuff> _otherStuff;
MyBloc() {
_loadSettings();
_loadOtherStuff();
}
Future<void> _loadSettings() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
// loads settings into _settings...
}
Future<void> _loadOtherStuff() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
// loads other stuff into _otherStuff...
}
}
I want to guarantee that _loadSettings() and _loadOtherStuff() complete before we get too far into the app so that the code that depends on settings/other stuff has the right info loaded (for example, I want settings to be loaded before I go out to make some network calls, initialize notifications, etc.).
As far as I understand, constructors can't be asynchronous, so I can't await on the constructor. I've tried giving my BLoC an init() function (or something similar) that calls _loadSettings() and/or _loadOtherStuff(), but I'm having a hard time finding a good place to put it.
Where should I be putting these calls? Or am I just misunderstanding async/await?
You can use a stream to listen for completion.
class MyBloc {
MySettings _settings;
List<MyOtherStuff> _otherStuff;
final _completer = StreamController<Void>.broadcast();
Stream<void> get completer => _completer.stream;
MyBloc() {
allInit();
}
allInit()async{
await _loadSettings();
await _loadOtherStuff();
_completer.sink.add(null);
}
Future<void> _loadSettings() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
// loads settings into _settings...
return;
}
Future<void> _loadOtherStuff() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
// loads other stuff into _otherStuff...
return;
}
}
And then you use a streamBuilder
return StreamBuilder(
stream: myBloc.completer,
builder: (context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) return Text('loading...');
//stuff to run after init
},
I also ended up using some help from Future.wait():
Future.wait([_loadSettings(), _loadOtherStuff()]).then((_) => _doMoreStuff());
Which makes sure that the first two are done before we move on.
Related
I am trying to get user email,save to shared preferences and use as collection name in another file.
my code to save
Future<void> saveEmail() async {
var sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.setString("email", _emailKontroller.text);}
no problem here, I can save data to sharedPreferences and read data from another file.
my code to read
#override
void initState() {
// TODO: implement initState
void initilaizeEmail() async {
var sharedPreferences = await SharedPreferences.getInstance();
_email = sharedPreferences.getString("email");
print(_email);
}
initilaizeEmail();
setState(() {});
}
output
I/flutter ( 3274): a#a.com
where I use as parameter my sharedPreferences Data:
query: FirebaseFirestore.instance
.collection("test")
.doc("$_email")
.collection("class 0"),
// to fetch real-time data
isLive: false,
I can not see anything on screen but, if I delete
_email
and type "a#a.com" manually everything works.What is the problem?
The problem is that initilaizeEmail is an async method, and you're not waiting for its result. To fix this:
await initilaizeEmail();
I also recommend fixing the name of the method to be initializeEmail. While it won't change the behavior, spelling mistakes tend distract from other problems.
I solved my problem with using
Future Builder
For context I'm using Getx state management for flutter and i need to call list.bindStream(availabilityStream()) on my Rx<List<Availability>> object.
here is my availabilityStream method
static Stream<List<Availability>> availabilityStream() {
return FirebaseFirestore.instance
.collection('availability')
.where('language',
isEqualTo: GetStorageController.instance.language.value)
.snapshots()
.map((QuerySnapshot query) {
List<Availability> results = [];
for (var availablity in query.docs) {
availablity["cluster"].get().then((DocumentSnapshot document) {
if (document.exists) {
print("Just reached here!");
//! Ignore doc if cluster link is broken
final model = Availability.fromDocumentSnapshot(
availabilityData: availablity, clusterData: document);
results.add(model);
}
});
}
print("result returned");
return results;
});
}
the cluster field on my availability collection is a reference field to another collection. The problem here is i need to await the .get() call to my firestore or the function returns before the data gets returned. I can't await inside the map function or the return type of Stream<List> changes. so how can i await my function call here?
using the advice i got from the comments I've used Stream.asyncMap to wait for all my network call futures to complete.
Here is my updated Repository
class AvailabilityRepository {
static Future<Availability> getAvailabilityAndCluster(
QueryDocumentSnapshot availability) async {
return await availability["cluster"]
.get()
.then((DocumentSnapshot document) {
if (document.exists) {
//! Ignore doc if cluster link is broken
final model = Availability.fromDocumentSnapshot(
availabilityData: availability, clusterData: document);
return model;
}
});
}
static Stream<List<Availability>> availabilityStream() {
return FirebaseFirestore.instance
.collection('availability')
.where('language',
isEqualTo: GetStorageController.instance.language.value)
.snapshots()
.asyncMap((snapshot) => Future.wait(
snapshot.docs.map((e) => getAvailabilityAndCluster(e))));
}
}
How i think this works is that the normal .map function returns multiple promises form the getAvailabilityAndCluster() method then all of the processes that execute asynchronously are all put to Future.wait() which is one big promise that waits all the promises inside it to complete. Then this is passed onto .asyncMap() which waits for the Future.wait() to complete before continuing with its result.
I want to build a contactScreen for my flutter app with an array downloaded from firebase. The array is correctly downloaded, but while building the app the array stays empty. With some tests (the print-statements) I figured out, that the app builds the screen and at the same time download the data (with getUserData()). So the download is not fast enough. (After a reload everything works fine).
#override
void initState() {
super.initState();
getUserData();
print(contacts);
}
getUserData() async {
var userData = await FirebaseFirestore.instance
.collection('users')
.doc(currentUser)
.get();
print(userData.data()!['contacts']);
contacts = userData.data()!['contacts'];
}
Is it possible to download the data before the build method every time? I can't use the straembuilder because it's already in use for another download.
create a variable bool isLoading = true
getUserData() {
//await retrieve data
//after data is retrieved
setState(() {
isLoading = false;
});
}
In your build method:
isLoading ? Container(child: Text('Loading...')) : YourScreen()
I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.
I've encountered a weird issue where if I yield* from my provider in my flutter app, the rest of the code in the function doesn't complete.
I'm using the BLoC pattern, so my _mapEventToState function looks like this:
Stream<WizardState> _mapJoiningCongregationToState(
int identifier, int password) async* {
_subscription?.cancel();
_subscription= (_provider.doThings(
id: identifier, password: password))
.listen((progress) => {
dispatch(Event(
progressMessage: progress.progressText))
}, onError: (error){
print(error);
}, onDone: (){
print('done joiining');
});
}
Then in the provider/service... this is the first attempt.
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings(
{int id, int password}) async* {
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
yield* _progressStream.stream;
}
The yield statement returns, but only after both awaited functions have completed. This makes complete sense to me, obviously I wouldn't expect the code to complete out of order and somehow run the yield* before waiting for the 'await's to complete.
In order to "subscribe" to the progress of this service though, I need to yield the stream back up to the caller, to write updates on the UI etc. In my mind, this is as simple as moving the yield* to before the first await. Like this.
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings(
{int id, int password}) async* {
yield* _progressStream.stream;
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
}
But, then setting breakpoints on the later _progressStream.add calls show that these never get called. I'm stuck on this, any idea what it could be? I know it has something to do with how I have mixed Futures and Streams.
The yield* awaits the completion of the stream it returns.
In this case, you want to return a stream immediately, then asynchronously feed some data into that stream.
Is anything else adding events to the stream controller? If not, you should be able to just do:
#override
Stream<JoinCongregationProgress> doThings({int id, int password}) async* {
await Future.delayed(Duration(seconds:2));
yield JoinCongregationProgress(progressText: "kake1...");
await Future.delayed(Duration(seconds:2));
yield JoinCongregationProgress(progressText: "kake5!!!...");
}
No stream controller is needed.
If other functions also add to the stream controller, then you do need it. You then have to splut your stream creation into an async part which updates the stream controller, and a synchronous part which returns the stream. Maybe:
final StreamController<Progress> _progressStream = StreamController<JoinCongregationProgress>();
#override
Stream<JoinCongregationProgress> doThings({int id, int password}) {
() async {
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake1..."));
await Future.delayed(Duration(seconds:2));
_progressStream.add(JoinCongregationProgress(progressText: "kake5!!!..."));
}(); // Spin off async background task to update stream controller.
return _progressStream.stream;
}