I am learning Dart and Flutter with a small mobile application. I have read everything I found about the await keyword but I still have problems I don't understand. Below the simplified code. I removed everything I thought it is unnessecary for understanding my problem. If something important is missing, please tell me.
My problem is the following line below the TODO (line 7): List locations = await _useCaseManager.findLocations(); In this method I query the database. I want the application to wait until the query is finished and the data are returned.
I call the method _findFirstLocation() in the build() method. But Flutter does not wait for the data. It goes on with the rest of the code, especially with the method createNextAppointmentsList(). In this method I need the data the application should wait for for the future - the attribute _selectedLocation. But because Flutter is not waiting, _selectedLocation is null.
This is the relevant part of the class.
class _AppointmentsOverviewScreenState extends State<AppointsmentsOverviewScreen> {
UseCaseManager _useCaseManager;
Location _selectedLocation;
void _findFirstLocation() async {
// TODO Hier wartet er schon wieder nicht.
List<Location> locations = await _useCaseManager.findLocations();
_selectedLocation = locations.first;
print(_selectedLocation);
}
#override
Widget build(BuildContext context) {
_useCaseManager = UseCaseManager.getInstance();
_findFirstLocation();
return Scaffold(
appBar: AppBar(
title: Text(LabelConstants.TITLE_APPOINTMENT_OVERVIEW),
),
body: Column(
children: <Widget>[
Container(child: createNextAppointmentsList(),)
],
),
);
}
Widget createNextAppointmentsList() {
return FutureBuilder<List<Appointment>>(
future: _useCaseManager.findAppointmentsForActiveGarbageCans(_selectedLocation.locationId),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(snapshot.data[index].garbageCanName),
subtitle: Text(snapshot.data[index].date),
);
},
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
);
}
}
In the method _findFirstLocation there is the following method with a database query called.
Future<List<Location>> findLocations() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(DatabaseConstants.LOCATION_TABLE_NAME);
return List.generate(maps.length, (i) {
Location location = Location(
locationId: maps[i][DatabaseConstants.COLUMN_NAME_LOCATION_ID],
street: maps[i][DatabaseConstants.COLUMN_NAME_STREET],
houseNumber: maps[i][DatabaseConstants.COLUMN_NAME_HOUSENUMBER],
zipCode: maps[i][DatabaseConstants.COLUMN_NAME_ZIP_CODE],
city: maps[i][DatabaseConstants.COLUMN_NAME_CITY],
);
return location;
});
}
Because I have had already problems with await and the cause for these problems was a foreach() with a lambda expression, I tried another type of for loop as alternative:
Future<List<Location>> findLocations() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(DatabaseConstants.LOCATION_TABLE_NAME);
final List<Location> locations = List();
for (int i = 0; i < maps.length; i++) {
var locationFromDatabase = maps[i];
Location location = Location(
locationId: maps[i][DatabaseConstants.COLUMN_NAME_LOCATION_ID],
street: maps[i][DatabaseConstants.COLUMN_NAME_STREET],
houseNumber: maps[i][DatabaseConstants.COLUMN_NAME_HOUSENUMBER],
zipCode: maps[i][DatabaseConstants.COLUMN_NAME_ZIP_CODE],
city: maps[i][DatabaseConstants.COLUMN_NAME_CITY],
);
locations.add(location);
}
return locations;
}
But in both cases, the application is not waiting for the data and I don't understand the reason.
Thank you in advance.
Christopher
You have several problems in your code:
First of all, if you want to 'await' you have to use the word await when you want the flow to await. You do it in your _findFirstLocation() function but you are not doing it when you call it, hence, you should call it like this:
await _findFirstLocation();
But even this is not correct, because you are trying to block the UI thread, which is totally prohibited (this would cause the UI to freeze having a poor user experience).
In this cases, what you need is a FutureBuilder, in which you specify what should happen while your background process is running, what should happen when it throws an error and what should happen when the result is returned.
And lastly, I suggest you to not initialize variables in the build() method, as it can be called multiple times:
_useCaseManager = UseCaseManager.getInstance();
I would move that to the body of the class if possible, and when not possible put it in the initState() method.
You do not await the method _findFirstLocation(); where you call it. That means the call will not wait for the result, it will just go to the next instruction and continue with that.
In this special case, you cannot actually await it because the build method is not async and you cannot change that. In this case, you need a FutureBuilder to show something like a spinner or wait dialog until your results are loaded.
You can find more information here:
What is a Future and how do I use it?
Related
My problem is: I subscribed to events in signalR, but I don’t understand how to correctly take the data from this answer and put it in UI. The documentation shows the same method as in my code, but an empty list is returned to me in the user interface. In my case i get the data at the moment when the event comes, until i get the data from the event the list is empty and i thought to capture this data somehow, because i have to show it to the user. But the data from the event is not coming to my UI
But there is data in the console. Here they are - [{warpedBox: [604.3993, 290.7302, 1106.364, 290.7302, 1106.364, 530.2628, 604.3993, 530.2628], name: Cats, date: 2022-09-05T09:01:11.9003992+03:00, additionInfo: new animal detected, baseName: TestBase, imageGuid: 00000000-0000-0000-0000-000000000000}] . How to get data from an event?
Many thanks to Robert Sandberg
I made some changes and now my code is like that (also I added UI part, because I don't understand how to make it work)
My code is now:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
class Animals {
Alarmplayer alarmplayer = Alarmplayer();
Future<void> fetchAnimals(CallbackFunc arguments) async {
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final hubConnection = HubConnectionBuilder()
.withUrl(
'secure_link',
options: httpConnectionOptions,
)
.build();
await hubConnection.start();
hubConnection.on('Animals', (arguments);
alarmplayer.Alarm(url: 'assets/wanted.mp3', volume: 0.01);
await Future.delayed(const Duration(seconds: 2))
.then((value) => alarmplayer.StopAlarm());
});
}
return agruments
}
My UI-part:
class _TestScreenState extends State<TestScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder<void>(
///can't understand how to pass here arguments
future: fetchAnimals(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(children: [
Text(snapshot.data?[index]['name']),
]);
},
);
}),
I am sorry but I am really noob in that and can't understand how can I use it in the UI
If I understand you correctly, then you mean that you get an empty list where you do the print(detectedAnimals ); ? With this setup, your method will basically always return an empty list.
And that is because your method have already executed that print line (and the return statement) when you get the event over SignalR.
The callback you send into the hubConnection must have a way to report back to the UI, meaning it have to have a way to communicate back to the method that is calling fetchAnimals() WHEN the callback is executed.
So I'd inject the callback into fetchAnimals() as such:
typedef CallbackFunc = void Function(List<dynamic>? arguments);
Future<void> fetchAnimals(CallbackFunc callback) async {
....
hubConnection.on('Animals', callback);
...
}
For some reason, Future.wait() is constantly returning null. I'm not completely certain I am using it correctly.
For context, I have a collection of posts in Firebase. For each post, I can extract the userID assigned to it, then for each post individually I use the userID of the poster to grab the username for display purposes. I grab the Post from a snapshot:
static Future<Post> fromSnapshot(QueryDocumentSnapshot<Object?> doc) async {
final _documentId = doc.id;
final _title = doc.get('title');
final _text = doc.get('text');
final _createdOn = doc.get('createdOn');
final _userID = doc.get('userID');
final userDoc = await FirebaseFirestore.instance.collection('users').doc(_userID).get();
final username = userDoc.get("username");
return Post(documentId: _documentId, title: _title, text: _text, createdOn: _createdOn, username: username);
}
and the extraction of posts occurs in a getPosts() function elsewhere:
Future<List<Post>> getPosts() async {
QuerySnapshot posts = await FirebaseFirestore.instance.collection('posts').get();
final allData = posts.docs.map(
(doc) async => await Post.fromSnapshot(doc)
).toList();
print(allData); // [Instance of 'Future<Post>', Instance of 'Future<Post>', Instance of 'Future<Post>']
final futurePosts = Future.wait(allData);
print(futurePosts); // Instance of 'Future<List<Post>>'
// why does this always return null?
return futurePosts;
}
the problem is it has to be async to extract the posts but also to get the username, meaning it returns a future list of future posts. I want to pass the result of getPosts() to a FutureBuilder, so I need a Future List of posts, and to not make all the posts Future I use Future.wait - but that always seems to return null. Essentially, I am mapping each post in the snapshot to its own Post item, where in the constructor it needs to run a further async call to extract the username. Am I missing something?
Note: even making the Future.wait() await returns null, it just also doesn't return a List of type Future so I can't use it in the FutureBuilder either.
Edit 1:
It turns out that futurePosts is actually an Instance of 'Future<List<Post>>', but when accessing the data within the FutureBuilder, snapshot.data is null:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Feed'),
),
body: FutureBuilder(
future: getPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
print(snapshot.data);
return postsToColumn(context, snapshot.data as List<Post>);
}
return const Center(
child: CircularProgressIndicator(),
);
}
),
);
}
Ok, lots of thanks to #IvoBeckers for helping me pin this down. It turns out, the snapshot actually did have an error, as they said, but this doesn't get printed unless you print it explicitly:
if (snapshot.hasError) {
print(snapshot.error.toString());
}
And the error is
Bad state: cannot get a field on a DocumentSnapshotPlatform which does not exist
So it turns out that not every User has a corresponding entry in the users collection with its username, which sounds like something I should have checked before, but I thought such an error would be printed out in the console. Once I updated the users collection, it worked perfectly.
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 BLOC Pattern and Provider to get Data through Streams...
#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,));
},
),
#puf published a solution in How to display a Firebase list in REAL TIME? using setState, but I don't know how to implement it because I can't use setState inside my BLoC pattern page.
UPDATE
My BLoC Pattern looks like this...
class AlarmaBloc {
final _alarmaController = new BehaviorSubject<List<AlarmaModel>>();
final _alarmaProvider = new AlarmaProvider();
Stream <List<AlarmaModel>> get alarmasStream => _alarmaController.stream;
Future<List<AlarmaModel>> cargarAlarmas() async {
final alarmas = await _alarmaProvider.cargarAlarmas();
_alarmaController.sink.add(alarmas);
return alarmas;
}
//---
dispose() {
_alarmaController?.close();
}
And my PROVIDER looks like this...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
print('Provider - Nuevo onChild Alarma ${element.snapshot.value['fecha']} - ${element.snapshot.value['nombreRefEstanque']} - ${element.snapshot.value['pesoPromedio']}}');
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("Las Alarmas se cargaron totalmente - ${alarmaList.length}");
});
return alarmaList;
How can I display a List from Firebase in "true" Real Time using BLoC Pattern?
I have a JSON method that returns a List after it is completed,
Future<List<Feed>> getData() async {
List<Feed> list;
String link =
"https://example.com/json";
var res = await http.get(link);
if (res.statusCode == 200) {
var data = json.decode(res.body);
var rest = data["feed"] as List;
list = rest.map<Feed>((json) => Feed.fromJson(json)).toList();
}
return list;
}
I then call this, in my initState() which contains a list hello, that will filter out the JSON list, but it shows me a null list on the screen first and after a few seconds it loads the list.
getData().then((usersFromServer) {
setState(() {. //Rebuild once it fetches the data
hello = usersFromServer
.where((u) => (u.category.userJSON
.toLowerCase()
.contains('hello'.toLowerCase())))
.toList();
users = usersFromServer;
filteredUsers = users;
});
});
This is my FutureBuilder that is called in build() method, however if I supply the hello list before the return statement, it shows me that the method where() was called on null (the list method that I am using to filter out hello() )
FutureBuilder(
future: getData(),
builder: (context, snapshot) {
return snapshot.data != null ?
Stack(
children: <Widget>[
CustomScrollView(slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
child: puzzle[0],
);
},
childCount: 1,
),
)
]),
],
)
:
CircularProgressIndicator();
});
You are calling your getData method multiple times. Don't do that. Your UI waits for one call and your code for the other. That's a mess. Call it once and wait for that call.
You need to define the future to wait for in your state:
Future<void> dataRetrievalAndFiltering;
Then in your initstate, assign the whole operation to this future:
(note that I removed the setState completely, it's not needed here anymore)
dataRetrievalAndFiltering = getData().then((usersFromServer) {
hello = usersFromServer.where((u) => (u.category.userJSON.toLowerCase().contains('hello'.toLowerCase()))).toList();
users = usersFromServer;
filteredUsers = users;
});
Now your FurtureBuilder can actually wait for that specific future, not for a new Future you generate by calling your method again:
FutureBuilder(
future: dataRetrievalAndFiltering,
builder: (context, snapshot) {
Now you have one Future, that you can wait for.
This response is a little too late to help you, but for anyone wondering how to load a Future and use any of it's data to set other variables without having the issue of saving it in a variable, and then calling setState() again and loading your future again, you can, as #nvoigt said, set a variable of the Future in your state, then you can call the .then() function inside the addPostFrameCallback() function to save the result, and finally using the future variable in your FutureBuilder like this.
class _YourWidgetState extends State<YourWidget> {
...
late MyFutureObject yourVariable;
late Future<MyFutureObject> _yourFuture;
#override
void initState() {
super.initState();
_yourFuture = Database().yourFuture();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_yourFuture.then((result) {
yourVariable = result;
// Execute anything else you need to with your variable data.
});
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _yourFuture,
builder: ...
);
}
}
dynamic doSomeStuffWithMyVariable() {
// Do some stuff with `yourVariable`
}
So, the advantage of this is that you can use the data loaded in the future outside the scope of the FutureBuilder, and only loading it once.
In my case, I needed to get a map of objects from my future, then the user could select some of those objects, save them on a map and make some calculations based on that selection. But as selecting that data called setState() in order to update the UI, I wasn't able to do so without having to write a complicated logic to save the user selection properly in the map, and having to call the future again outside the scope of the FutureBuilder to get it's data for my other calculations.
With the example above, you can load your future only once, and use the snapshot data outside the scope of the FutureBuilder without having to call the future again.
I hope I was clear enough with this example. If you have any doubts I will gladly clarify them.
I am trying to populate my ListView with the result from an API. The API call must take place after the values have been retrieved from Shared Preference. However on execution my function for API call runs an infinite loop and the UI doesn't render. I tracked this behaviour through debug statements.
The circular indicator that should be shown when Future builder is building UI is also not showing.
How can I resolve this?
My code:
class _MyHomePageState extends State<MyHomePage>{
#override MyHomePage get widget => super.widget;
String userID = "";
String authID = "";
//Retrieving values from Shared Preferences
Future<List<String>> loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> l= new List<String>();
if(prefs.getString("ID") == null){
l.add("null");
}
else{
l.add(prefs.getString("ID"));
}
if(prefs.getString("authID") == null){
l.add("null");
}
else{
l.add(prefs.getString("authID"));
}
return l;
}
//Setting values retrieved from Shared Pref
setData() async{
await loadData().then((value) {
setState(() {
userID = value[0];
print('the user ID is' + userID);
authID = value[1];
print('the authID is' + authID);
});
// getAllTasks(userID, authID);
});
print("Set data execution completed ");
}
//FUNCTION to use values from Shared Pref and make API Call
Future<List<Task>> getAllTasks() async{
await setData();
//Waiting for Set Data to complete
print('Ive have retrived the values ' + userID + authID );
List<Task> taskList;
await getTasks(userID, authID, "for_me").then((value){
final json = value;
if(json!="Error"){
Tasks tasks = tasksFromJson(json); //of Class Tasks
taskList = tasks.tasks; //getting the list of tasks from class
}
});
if(taskList != null) return taskList;
else {
print('Tasklist was null ');
throw new Exception('Failed to load data ');
}
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context){
_signedOut(){
widget.onSignedOut();
}
//To CREATE LIST VIEW
Widget createTasksListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty ? Ink(....
) : CircularProgressIndicator();
},
);
}
//MY COLUMN VIEW
Column cardsView = Column(
children: <Widget>[
....
Expanded(
child: FutureBuilder(
future: getAllTasks(),
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
],
);
return Scaffold(
body: cardsView,
);
}
}
Instead of being called once... my setData function is being called repeatedly.. How can I resolve this..please help
You're creating Future object on every rebuild of the widget. And since you're calling setState inside your setData method, it triggers a rebuild recursively.
To solve this problem you have to keep a reference to the Future object. And use that reference for the FutureBuilder then it can understand that it is the previously used one.
E.g:
Future<List<Task>> _tasks;
#override
void initState() {
_tasks = getAllTasks();
super.initState();
}
And in your widget tree use it like that:
Expanded(
child: FutureBuilder(
future: _tasks,
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
The FutureBuilder widget that Flutter provides us to create widgets based on the state of some future, keeps re-firing that future every time a rebuild happens!
Every time we call setState, the FutureBuilder goes through its whole life-cycle again!
One option is Memoization:
Memoization is, in simple terms, caching the return value of a function, and reusing it when that function is called again.
Memoization is mostly used in functional languages, where functions are deterministic (they always return the same output for the same inputs), but we can use simple memoization for our problem here, to make sure the FutureBuilder always receives the same future instance.
To do that, we will use Dart’s AsyncMemoizer.
This memoizer does exactly what we want! It takes an asynchronous function, calls it the first time it is called, and caches its result. For all subsequent calls to the function, the memoizer returns the same previously calculated future.
Thus, to solve our problem, we start by creating an instance of AsyncMemoizer in our widget:
final AsyncMemoizer _memoizer = AsyncMemoizer();
Note: you shouldn’t instantiate the memoizer inside a StatelessWidget, because Flutter disposes of StatelessWidgets at every rebuild, which basically beats the purpose. You should instantiate it either in a StatefulWidget, or somewhere where it can persist.
Afterwards, we will modify our _fetchData function to use that memoizer:
_fetchData() {
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
}
Note: you must wrap inside runOnce() only the body, not the funciton call
Special thanks to AbdulRahman AlHamali.
You need to save the Future in the State because doing getAllTasks() is triggering the call on every build callback.
In the initState:
this.getAllTasksFuture = getAllTasks();
Then you would use this Future property in the FutureBuilder.