Return code results after some delay in Flutter? - flutter

I'm trying to run this Future function that runs a Timer once 500 milliseconds have passed. The issue I'm having is that the timer is not passing the data to the results variable so the _destinationOnSearchChanged() ends up returning null.
Note: getCity(context, _typeAheadController.text); does return data but only inside the Timer function.
Future _destinationOnSearchChanged() async {
dynamic results;
//Cancels timer if its still running
if (_apiCityThrottle?.isActive ?? false) {
_apiCityThrottle.cancel();
}
//Makes API call half a second after last typed button
_apiCityThrottle = Timer(const Duration(milliseconds: 500), () async{
results = await getCity(context, _typeAheadController.text);
});
print(results);
return await results;
}

As pskink noted in a comment, you probably should look into existing debounce mechanisms instead of creating your own.
If you still want to proceed down this path: your problem is that create a Timer and then return results immediately. You don't wait for the Timer to fire (and you can't directly await a Timer). In this case, you could use a Completer:
Future _destinationOnSearchChanged() async {
var timerCompleter = Completer<dynamic>();
// Cancels timer if it's still running.
_apiCityThrottle?.cancel();
// Makes API call half a second after last typed button.
_apiCityThrottle = Timer(const Duration(milliseconds: 500), () async {
timerCompleter.complete(await getCity(context, _typeAheadController.text));
});
var results = await timerCompleter.complete();
print(results);
return results;
}

Related

Why is 'return' not awaiting this nested 'then' in Flutter/Dart?

I'm new to programming, and this is my first post on stack overflow! In building my first Flutter app I'm trying to understand the code, and I'm not sure why two pieces of code behave differently. Feel free to just look at the code and answer why one doesn't work... or if you'd like, here's the background.
Data structure:
Collection: chatters
Document: chatter doc
SubCollection: members-chatter, another SubCollection: approved-chatter
Documents: member doc, and approved doc
I'm listing all the chatter docs that a user is a member of, so from a CollectionGroup query with uid in the member doc, I then lookup the parent doc id. Next I want to have chatter docs be marked bool public, and for !public chatters, I only want them listed if the user's uid is also on an approved doc in SubCol approved-chatter.
So my main question is why the await doesn't hold through the entirety of the nested .then's in my first attempt.
But while I'm here, I'm also open to any insight on my approach to handling membership to groups of both public and approval-required types. It seems trickier than I first thought, once considering read/write permissions and appropriate security and privacy.
I tried this first.
// get my chatter IDs
Future<List<String>> oldgetMyChatterIDs() async {
List<String> myChatterIDs = [];
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
}
but return myChatterIDs; completes before:
document.reference.parent.parent!.get().then((value) {
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
}
Why doesn't the return await the await?
I rewrote the code, and this way works, but I'm not sure why it's different. It does appear a bit easier to follow, so I perhaps it's better this way anyway.
// get my chatter IDs
Future<List<String>> getMyChatterIDs() async {
List<String> myChatterIDs = [];
QuerySnapshot<Map<String, dynamic>> joinedChattersSnapshot = await FirebaseFirestore
.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get();
for (var i = 0; i < joinedChattersSnapshot.docs.length; i++) {
DocumentSnapshot<Map<String, dynamic>> aChatDoc =
await joinedChattersSnapshot.docs[i].reference.parent.parent!.get();
bool isPublic = aChatDoc.data()?['public'] ?? true;
if (isPublic) {
myChatterIDs.add(aChatDoc.id);
} else {
try {
DocumentSnapshot<Map<String, dynamic>> anApprovalDoc =
await FirebaseFirestore.instance
.collection('chatters')
.doc(aChatDoc.id)
.collection('approved-chatter')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
bool isApproved = anApprovalDoc.data()!['approved'];
if (isApproved) {
myChatterIDs.add(aChatDoc.id);
}
} catch (e) {
// // Could add pending to another list such as
// myPendingChatterIDs.add(aChatDoc.id);
}
}
}
return myChatterIDs;
}
Take a look at this piece of code:
print("1");
print("2");
result:
1
2
The print on both lines is synchronous, they don't wait for anything, so they will execute immediately one after the other, right ?
Take a look at this now:
print("1");
await Future.delayed(Duration(seconds: 2));
print("2");
result:
1
2
This now will print 1, then wait 2 seconds, then print 2 on the debug console, using the await in this case will stop the code on that line until it finishes ( until the 2 seconds pass).
Now take a look at this piece of code:
print("1");
Future.delayed(Duration(seconds: 2)).then((_) {print("2");});
print("3");
result:
1
3
2
this seems to be using the Future, but it will not wait, because we set the code synchronously, so 1 will be printed, it will go to the next line, it will run the Future.delayed but it will not wait for it across the global code, so it will go to the next line and print 3 immediately, when the previous Future.delayed finishes, then it will run the code inside the then block, so it prints the 2 at the end.
in your piece of code
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
using then, the return myChatterIDs will execute immediately after the Future before it, which will cause an immediate end of the function.
To fix this with the then, you need simply to move that return myChatterIDs inside the then code block like this:
/*...*/.then(() {
// ...
return myChatterIDs
});
using the await keyword in your second example will pause the code on that line until the Future is done, then it will continue for the others.
you can visualize what's happening live on your code, by setting breakpoints on your method lines from your IDE, then see what's running first, and what's running late.
It´s because document.reference.parent.parent!.get() is a Future too but you aren't telling your function to wait for it. The first await only applies to the first Future.
When you use then you are basically saying, execute this Future and when it's finished, do what is inside this function but everything outside that function doesn't wait for the then to finish unless you told it to with an await.
For example this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
//Here it doesn't have an await so it will execute
//asynchronously and just continues to next line
//while the Future executes in the background.
Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Number being printed after 1 second. But this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) async {
//Here it does have an await so it will wait for this
//future to complete too.
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Letter being printed after 2 seconds.
Additional to this, forEach cannot contain awaits as mentioned here in the Dart documentation and even if you use it will still be asynchronous.
Try using
for(var document in snapshot.docs){
//Your code here
}
Instead

Flutter - How to delay a function for some seconds

I have a function which returns false when the user select incorrect answer.
tappedbutton(int index) async {
final userAnswer = await userAnswer();
if (userAnswer) {
// Executing some code
}else{
ErrorSnackbar(); // This snackbar takes 2 second to close.
}
My objective is to delay calling the function for two seconds(user can click the button again , with no action triggering) after the user selects the wrong answer and to prevent the click immediatly. How can i achieve it?
You'll have to add an helper variable in the outer scope, that will indicate whether the user is on an answer cooldown or not.
The shortest solution will be:
var answerCooldownInProgress = false;
tappedbutton(int index) async {
// Ignore user taps when cooldown is ongoing
if (answerCooldownInProgress) {
return;
}
final userAnswer = await userAnswer();
if (userAnswer) {
// ...
} else {
ErrorSnackbar();
answerCooldownInProgress = true;
await Future.delayed(const Duration(seconds: 2));
answerCooldownInProgress = false;
}
}
You can use Future.delay or Timer() class to achieve that.
In order to delay a function you can do below code or use Timer() class
tappedbutton(int index) async {
await Future.delayed(Duration(seconds: 2));
}
I don't think that your goal is to delay the function.
You're trying to find a way to let the user wait until the ErrorSnackbar is gone, right?
Try this approach. It saves the time when a button was clicked the last time and cancels every button press until 2 seconds have passed.
DateTime lastPressed = DateTime(0);
tappedButton(int index) async {
DateTime now = DateTime.now();
if (lastPressed.difference(now).inSeconds < 2) {
// button pressed again in under 2 seconds, cancel action
return;
}
lastPressed = now;
final userAnswer = await userAnswer();
if (userAnswer) {
// answer correct
} else{
// answer incorrect
ErrorSnackbar();
}
}

Is there a way to skip await if it's too long? (Flutter)

I use async await in main so the user has to wait in the splash screen before entering the app.
void main() async {
await Firebase.initializeApp();
String? x;
await FirebaseDatabase.instance.ref().child("data").once().then((snapshot) {
Map data = snapshot.snapshot.value as Map;
x = jsonEncode(data);
});
return ChangeNotifierProvider<DataModel>.value(
value: DataModel(data: x),
child: MaterialApp()
);
}
If there are users entering the app without an internet connection, they will be stuck on the splash screen forever. If there are also users with slow internet connection, they will be stuck on the splash screen longer.
So no matter what the internet connection problem is, I want to set a maximum of 5 seconds only to be in the await, if it exceeds, skip that part and go straight into the app.
Pass your Firebase api call to a function, make the method return future,
Future<String?> getData() async {
await FirebaseDatabase.instance.ref().child("data").once().then((snapshot) {
Map data = snapshot.snapshot.value as Map;
return jsonEncode(data);
});
}
Then on main method, attach a timeout with that method you created
void main() async {
await Firebase.initializeApp();
String? x = await getData().timeout(Duration(seconds: 5), onTimeout: () {
return null };
return ChangeNotifierProvider<DataModel>.value(
value: DataModel(data: x),
child: MaterialApp()
);
}
So if you api takes more then 5 seconds of time, then it exit and return null
you can User Race condition or Future.timeout
Race condition using future.any
final result = await Future.any([
YourFunction(),
Future.delayed(const Duration(seconds: 3))
]);
in above code one who finish first will give result,
so if your code takes more than 3 sec than other function will return answer

In flutter, how to trigger something strictly after an async function is done executing?

Say I have a _mode property that I want to change in setState() when a button is pressed.
When the button is pressed (onPressed), I am calling this function,
Future <void> changeMode() async {
_result = await getResult(); // Returns a result. I want the mode to change AFTER the result is returned
setState((){
_mode = Modes.OFF;
});
}
What ends up happening is, the _mode is changed before the getResult() is done executing. How should I go fixing this?
When you use await inside a function, that function is in danger of blocking the main thread, so it must be marked as async.
Mark your function with async and it should work. Your state setting function also should be a parameter of setState().
Future<void> changeMode() async {
_result = await getResult(); // Returns a result. I want the mode to change AFTER the result is returned
setState(() {
_mode = Modes.OFF;
});
}

Why does this test using FakeAsync just hang on the "await" even though the future has completed?

I'm trying to write a test using FakeAsync but it seems to hang on my awaits. Here's a stripped down example:
test('danny', () async {
await FakeAsync().run((FakeAsync async) async {
print('1');
final a = Future<bool>.delayed(const Duration(seconds: 5))
.then((_) => print('Delayed future completed!'))
.then((_) => true);
print('2');
async.elapse(const Duration(seconds: 30));
// Tried all this too...
// async.flushMicrotasks();
// async.flushTimers();
// async.elapse(const Duration(seconds: 30));
// async.flushMicrotasks();
// async.flushTimers();
// async.elapseBlocking(const Duration(seconds: 30));
print('3');
await a;
print('4');
expect(1, 2);
});
});
This code outputs:
1
2
Delayed future completed!
3
// hangs and never prints '4'
The async.elapse call is allowing the future to be completed, but it still hangs on await a. Why?
This seems to occur because although the Future is completed, the await call requires the microtask queue to be processed in order to continue (but it can't, since nobody is calling async.elapse after the await).
As a workaround, contiually pumping the microstask queue while the function is running seems to work - for example calling this function in place of FakeAsync.run:
/// Runs a callback using FakeAsync.run while continually pumping the
/// microtask queue. This avoids a deadlock when tests `await` a Future
/// which queues a microtask that will not be processed unless the queue
/// is flushed.
Future<T> runFakeAsync<T>(Future<T> Function(FakeAsync time) f) async {
return FakeAsync().run((FakeAsync time) async {
bool pump = true;
final Future<T> future = f(time).whenComplete(() => pump = false);
while (pump) {
time.flushMicrotasks();
}
return future;
}) as Future<T>;
}
It seems that await does not work inside fakeAsync, so you are stuck with Future.then() and friends.
However you can very easily wait for all Futures to complete by calling time.elapse()and time.flushMicrotasks().
test('Completion', () {
fakeAsync((FakeAsync time) {
final future = Future(() => 42);
time.elapse(Duration.zero);
expect(future, completion(equals(42)));
time.flushMicrotasks();
});
});