I want to initialize a variable with a value that I have in my local sql database. This is the code:
class _SettingsState extends State<Settings>{
String morning = 'empty';
#override
void initState() {
super.initState();
initAsync();
}
void initAsync() async{
morning = await DatabaseHelper.instance.queryOrario(1);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Text(morning),
),
);
}
The problem is that when I first arrive on this page, I have this error:
A non-null String must be provided to a Text widget.
referred to the line: child: Text(morning),
Then, when I hot reload the page, the code works fine. Why? What am I doing wrong? There is a way to do what I want to do?
(I'm assuming that morning's default value of 'empty' is something you added to your code before posting it here, and it's actuall default value is null. Otherwise, it would make no sense for you to be getting the error you are getting.)
The problem is that your widget is initializing and building before the future has a chance to finish. By the time you have assigned a value for morning, the widget has already been built. As such, when the widget does build, morning is still null. And Flutter heavily frowns on giving a null value to a Text widget.
Instead of depending on a race condition (where two parts of your code are racing to complete first and you are hoping they will complete in a particular order), use a FutureBuilder so you can safely execute futures and have your widget update itself when they are complete:
class _SettingsState extends State<Settings>{
#override
void initState() {
super.initState();
}
Future<String> initAsync() async {
String result = await DatabaseHelper.instance.queryOrario(1);
return result;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<String>(
future: initAsync(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return Text(snapshot.data);
},
),
);
}
}
Related
I would like to retrieve the image from the cache after looking it up the first time but since my URL is getting stored in a future it seems that I'm awaiting the future every time which in turn is causing the ProgressIndecator to show even when the image is cached. Which kinda defeats the purpose of caching.
Flutter: CachedNetworkImage not caching image -
What TesteurManiak stated in this post seems to make sense to whats happening in my situation
Here is my code
class ImageLayerWidget extends StatelessWidget {
ImageLayerWidget({Key key, this.layer}) : super(key: key);
final AsyncMemoizer<models.Image> _memorizer = AsyncMemoizer();
final models.ImageLayer layer;
Future<models.Image> _fetchData() async {
return this._memorizer.runOnce(() async {
return await StorageFile<models.Image>(path: layer.path).getDatatoRam();
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: FutureBuilder(
future: _fetchData(),
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.hasData) {
return Image(
image: CachedNetworkImageProvider(snap.data.path),
);
}
return LinearProgressIndicator();
},
),
),
);
}
}
I am using an AsyncMemoizer to cache my image model class which holds the URL retrieved from ref.getDownloadURL();. This didn't really seem to help. I want it to only show the progress indicator on the first fetch then cache the image so the user doesn't see the indicator again.
I've tried so many things. Can anyone help?
Update:
As GrahamD suggested I also tried this
class ImageLayerWidget extends StatefulWidget {
ImageLayerWidget({Key key, this.layer}) : super(key: key);
final models.ImageLayer layer;
#override
_ImageLayerWidget createState() => _ImageLayerWidget();
}
class _ImageLayerWidget extends State<ImageLayerWidget> {
Future<models.Image> _future;
Future<models.Image> _fetchData() async {
return await StorageFile<models.Image>(path: widget.layer.path)
.getDatatoRam();
}
#override
void initState() {
_future = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snap) {
if (snap.hasData) {
return Image(
image: CachedNetworkImageProvider(snap.data.path),
);
}
return LinearProgressIndicator();
},
),
),
);
}
}
Unfortunately, it does not solve the problem. The LinearProgressIndicator always shows when returning to the same image. I also tried putting _future inside the stateful part of the widget but also didn't help. It's as if the snapshot is resolving the future every time. I even propagated this style of state up the widget tree. Still no fix. I'll keep trying things but I think it's the way I'm using ChachedNetworkImage with futures.
I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using streams to get the list...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
And, I read the Firebase Data in this way...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Total list was loaded - ${alarmaList.length}");
}); //I'm using this await to be sure that the full list was loaded, so I can order and process it later
return alarmaList;
}
How can I display a List from Firebase in "true" Real Time?
To properly manage the state of asynchronously loaded data, you should:
Start loading/listening to the data in initState()
Set the data into the state (with setState()) when you receive it or it is updated.
Then render it from the state in the build method.
So in your code that'd be something like:
final List<AlarmaModel> alarmaList = new List(); // this is now a field in the `State` object
#override
void initState() {
super.initState();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp);
setState(() {
alarmaList = alarmaList;
})
});
}
#override
Widget build(BuildContext context) {
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
If you only want to repaint once you've gotten a complete update from the database, you can put the call to setState() in a value listener, just use onValue in that instead of once(), as you also want to catch the updates.
I have stumbled upon a weird problem.
The following function gets a Firestore document and returns it so other functions can access it's data
Future getCountRequests() async {
try{
return await _countReference.document('Requests').get();
} catch(e){
print(e.toString());
}
}
And this is the function in question that uses it's data.
int _countRequest() {
int toReturn;
CounterService().getCountRequests().then(
(doc) {
//print('Item in question: $doc, information from document ${doc.data}');
//this line prints correctly Instance of DocumentReference and {"amount" : 6}
toReturn = doc.data['amount'];
}
);
return toReturn;
}
When I run the code, I get an error message on my screen which states that the AnimatedList I am using receives null from the _countRequest() function.
Putting a break on this line has helped me understand that this block gets skipped completely
CounterService().getCountRequests().then( ...
However when I put a break on this line, it shows that the code inside the block works and that the document is indeed received through the getCountRequests() function.
toReturn = doc.data['amount'];
My question is, what causes the .then block to be skipped causing the function to return null?
What you are trying to do cannot work.
You are trying to return the result of an asynchronous computation from a synchronous function. There is no way to make that work.
An asynchronous computation will always complete later, and a synchronous function always returns now, so the result of the computation cannot be returned now.
The then call is what makes the operation asynchronous. It passes a callback to a future, but the only thing you are guaranteed about when that callback is called is that it is definitely not immediately. The then call returns immediately, returning a future which will be completed when the callback has been called, and your code returns the current value of toReturn, which is still null.
In the end I figured that doing this asynchronously with a FutureBuilder wouldn't work at all, or with my current skillset I just can't figure out how to make it work. So I decided to convert to a StreamBuilder which uses a Stream<QuerySnapshot> gotten from the following function.
Stream<QuerySnapshot> findAllRequests() {
try{
return accountRequestCollection.snapshots();
} catch(e){
print(e.toString());
return null;
}
}
and built the AnimatedList into the StreamBuilder like so.
class Request extends StatefulWidget {
#override
_RequestState createState() => _RequestState();
}
class _RequestState extends State<Request> {
final AdministrationService _administrationService = AdministrationService();
final GlobalKey<AnimatedListState> _globalKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Account Requests'),
),
body: StreamBuilder(
stream: _administrationService.findAllRequests(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Loading();
default: if (snapshot.hasError) {
return new Text('Error: ${snapshot.hasError}');
} else {
return AnimatedList(
key: _globalKey,
initialItemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index, Animation animation) {
return _buildItem(snapshot.data.documents[index].data['email'], animation);
}
);
}
}
}
)
);
}
Widget _buildItem(String item, Animation animation) {
return SizeTransition(
sizeFactor: animation,
child: Card(
child: ListTile(
title: Text(
item,
style: TextStyle(
fontSize: 20.0,
)
),
),
),
);
}
}
It results into this page.
Emulator Screenshot.
I hope that this solution can help others with similar problems, though I am not sure how costly this could get when it comes to firestore read/writes considering this is just a proof of concept product.
I want to load user profile information from firestore after the user logs in and I want that data to be available to the whole app when the data is logged in. I plan on doing the later part using inherited widgets in flutter. I can load the data from firestore correctly and get all the data but the home page does not wait for the data to load before continuing so I get errors. It calls the method, then ignores the await part in the method and proceeds on, so I get errors, then after the errors, I get the data messages that the data has loaded.
I have tried a couple of things. I have all my user management code (login, sign up, ...) in one file and I wrote a getuserProfie() method in that file. The method gets all the data, populates a user model with the data and returns that model. See code below.
getUserProfile() async {
UserModel theUser;
await FirebaseAuth.instance.currentUser().then((user) {
Firestore.instance
.collection('/users')
.where('uid', isEqualTo: user.uid)
.snapshots()
.listen((data) {
print('getting data');
theUser = new UserModel(user.uid, data.documents[0]['email'],);
print('got data');
print(theUser.email);
return theUser;
});
}).catchError((e) {
print("there was an error");
print(e);
});
}
I then call this method in my home page as follows
#override
void initState() {
Usermode user = UserManagement().getUserProfile(); //Calling the method
super.initState();
}
This calls the method just fine and gets all the data but the program does not wait until the data is gotten to proceed. I tried moving the code that gets the data into the initState() method and calling super.initState() when I was sure there was a value but that did not work. I also tried calling the getUserProfile() before calling super.initstate() but that did not work.
I tried adding async to the initState() header but flutter does not like that. I can get the data from firebase fine but making the program wait until the data is gotten is the problem I am having. Seems like using await and async is not working. Any other suggestions to make sure the data is loaded before continuing ? I thought about using FutureBuilder but I am not sure how that would work in this case.
initState() can't be a async function.
What You Can try is - Move Usermode user = await UserManagement().getUserProfile(); to a new async function.
Usermode user;
#override
void initState() {
super.initState();
_getUser();
}
Void _getUser() async {
user = await UserManagement().getUserProfile();
setState(() {});
}
and in Build Method you can Check for User Value then pass the Widgets.
#override
Widget build(BuildContext context) {
if (user != null) {
return ProfilePage();
} else {
return CircularProgressIndicator();
}
Now whenever User Data is Loaded it will call setState() & your Profile Page will Load.
For this you need to make - Usermode user; state variable.
Since getUserProfile is async, you'll need to use await when calling it:
#override
void initState() async {
Usermode user = await UserManagement().getUserProfile();
super.initState();
}
After doing much research, I ended up using Flutter StreamBuilder. It was perfect for what I wanted to use it for. I call the method UserManagement().getUserProfile() and get the snapshot of the data from the returned value. If the data is loading, I display the CircularProgressIndicator(). If the snapshot is null, I display error message. See code snippet below.
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: UserManagement().getClientProfile(curUserID).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
UserModel _user = new UserModel.from(snapshot.data)
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(_usermodel.name),
],
});
}else if (snapshot.connectionState == ConnectionState.waiting) {
return Container(child: Center(child: CircularProgressIndicator()));
} else {
return Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.warning),
),
Text('There was an error please try again')
],
),
);
}
}
In this example, I am using StreamBuilder but you can also use FutureBuilder. StreamBuilder listens for changes in the data and updates the UI accordingly while FutureBuilder gets the data once until the page is loaded again. I had to make changes to my UserModel and also adjusted the way I stored my data but it all worked out. Got most of my solution from here
I'm trying to use the tab bar and tab bar view to appear some elements of the fire base. First, I used stream builder to get the text of the tabs in the tab bar:
class HomePage extends StatelessWidget {
final FirebaseUser user;
HomePage({this.user});
#override
Widget build(BuildContext context) {
return new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("places").snapshots(),
builder: (BuildContext context,AsyncSnapshot<QuerySnapshot> snapshot){
if (!snapshot.hasData){
return Center(child: CircularProgressIndicator());
}
else{
return DefaultTabController(
length: 20,
child: Scaffold(
appBar: AppBar(
title: Text("Home Page"),
bottom: TabBar( isScrollable: true,
tabs: new List.generate(snapshot.data.documents.length, (index) {
return new Tab(child: Text(snapshot.data.documents[index]['name'].toString().toUpperCase()));
}),)),
Then I want from the fire store to get a stream builder of collection named "temps" which has documents inside of it, every document id represents a document id in another collection named "users". In every document in users, i have a field named place. I already made the tabs and it works but,What I can't do is:
wanna get the document id of every document in collection temps, and get this document id and use it to access the documents which has the same id in "users" collection and check if the field place has the same value of the name in the tab bar i wanna appear it in the tab bar view!
How can i do this?
If I understood correctly, one solution would be creating a StatefulWidget, inside its State, using a local StreamController and pointing your StreamBuilder to it.
Separately, consume both Streams and add these items to your StreamController.
It would look a bit like that:
class YourClass extends StatefulWidget {
... createState() ...
}
class _YourClassState extends State<YourClass> {
StreamController<YourItem> _places;
#override
void initState() {
super.initState();
// the unified stream
_places = new StreamController();
// listening to changes of the first reference
CollectionReference places1Ref = Firestore.instance.collection("places1");
places1Ref.listen((snapshopt) {
// posting item to the unified streamController
_places.add(item);
});
// listening to changes of the second reference
CollectionReference places2Ref = Firestore.instance.collection("places2");
places2Ref.listen((snapshopt) {
// posting item to the unified streamController
_places.add(item);
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder<YourItem>(
stream: _places.stream, // using here only the unified stream
builder: (context, snapshot) {
return YourWidgets();
}
);
}
}
This mockup is using YourItem as unified object, but you can use something else, including dynamic itself.