How to wait for async in initState - flutter

I have an async function that needs to be called in initState in an Stateful widget, but I cannot use await as initState is not async.
sendHealthData is async as it get some information from the Health Api, and then I need to send that information to HomeSubScreen which is another StatefulWidget. But at the time the HomeSubScreen is created, the sendHealthData method didn't get all the information, so If I try to send some value in a param to the HomeSubScreen, the value will be null.
How can I do that?
#override
void initState() {
super.initState();
sendHealthData();
_widgetOptions = <Widget>[
HomeSubScreen(healthData),
];
}
Update:
If I added a then() I get the following error:
NoSuchMethodError: The method 'elementAt' was called on null.
Code Updated:
#override
void initState() {
super.initState();
sendHealthData().then((response){
_widgetOptions = <Widget>[
HomeSubScreen(healthData),
];
});
}

I'm giving you general idea of it.
#override
void initState() {
super.initState();
function().then((int value) {
// future is completed you can perform your task
});
function2(); // or you can simply call the method if you don't want anything to do after future completes
}
Future<int> function() async {
// do something here
}
Future<int> function2() async {
// do something here
}

You can also use FutureBuilder
FutureBuilder<String>(
future: getYourDataFromApi(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('start widget');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result from api...');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return Text('Result: ${snapshot.data}');
}
return null; // unreachable
},
)

You cannot await for async function in the initState, but a little trick is to use the Then keyword to execute code after that future complete.
eg:
#override
void initState() {
super.initState();
sendHealthData().then((response){
_widgetOptions = <Widget>[
HomeSubScreen(response),
];
});
}
and the function must be like:
Future sendHealthData() async{}

Related

Initializing a future in flutter?

I would like to run a downloading Future function when opening a page in flutter, however it is being called multiple times.
I would like to implement a solution like the second in this article:
https://flutterigniter.com/future-async-called-multiple-times/
(memoizing the future after initialisation so that the init function is not called multiple times)
however in his solution, he initialises the future like this
Future<String> _future;
this is no longer possible in the current version of dart and I was wondering if there was an equivalent, I have tried using the Late keyword and initializing it to null, neither of which work.
Here is the code currently and how I want it
currently:
class _ARState extends State<AR> {
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_downloadFiles();
});
}
Future<dynamic> _downloadFiles() async {
// some downloading function that is getting run multiple times ....
}
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _downloadFiles(),
builder: /// stuff i want built
}
how I want it:
class _ARState extends State<AR> {
Future<dynamic> _future;
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_downloadFiles();
});
}
Future<dynamic> _downloadFiles() async {
// some downloading function that is getting run multiple times ....
}
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _future,
builder: /// stuff i want built
}
One way is to make _future nullable and to make your asynchronous function idempotent by checking if _future is null. If it's null, do work; if it's not null, then just return the existing Future.
class _ARState extends State<AR> {
Future<dynamic>? _future;
...
Future<dynamic> _downloadFiles() {
Future<dynamic> helper() async {
// Do actual downloading work here.
}
if (_future == null) {
_future = helper();
}
return _future;
}
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _downloadFiles(),
...
}
}
Try the code below, using late keyword, but there are other options for that. I think you don't need the addPostFrameCallBack:
class _ARState extends State<AR> {
late Future<dynamic> _future;
#override
void initState() {
super.initState();
_future = _downloadFiles();
}
Future<dynamic> _downloadFiles() async {
}
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasData || snapshot.data != null) {
// build your widget
}
// progress indicator or something while future is running
return ...;
});
}

Flutter - FutureBuilder fires twice on hot reload

In my flutter project when I start the project in the simulator everything works fine and the future builder only fires once, but when I do hot reload the FutureBuilder fires twice which causes an error any idea how to fix this?
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
I solved the issue. I put the Future function in the initState and then used the variable in the FutureBuilder. I'm not sure why it works this way, but here's the code:
var futures;
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
futures = frameFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futures,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
The solution as you already figured out is to move the future loading process to the initState of a StatefulWidget, but I'll explain the why it happens:
You were calling your future inside your build method like this:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
The issue is that Flutter calls the build method each time it renders the Widget, whenever a dependency changes(InheritedWidget, setState) or Flutter decides to rebuild it. So each time you redraw your UI frameFuture() gets called, this makes your build method to have side effects (this async call) which it should not, and is encouraged for widgets not to have side effects.
By moving the async computation to the initState you're only calling it once and then accessing the cached variable futures from your state.
As a plus here is an excerpt of the docs of the FutureBuilder class
"The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted."
Hope this makes clear the Why of the solution.
This can happen even when the Future is called from initState. The prior solution I was using felt ugly.
The cleanest solution is to use AsyncMemoizer which effectively just checks if a function is run before
import 'package:async/async.dart';
class SampleWid extends StatefulWidget {
const SampleWid({Key? key}) : super(key: key);
final AsyncMemoizer asyncResults = AsyncMemoizer();
#override
_SampleWidState createState() => _SampleWidState();
}
class _SampleWidState extends State<SampleWid> {
#override
void initState() {
super.initState();
_getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.asyncResults.future,
builder: (context, snapshot) {
if (!snapshot.hasData) return yourLoadingAnimation();
// ... Do things with the data!
});
}
// The async and await here aren't necessary.
_getData() async () {
await widget.asyncResults.runOnce(() => yourApiCall());
}
}
Surprisingly, there's no .reset() method. It seems like the best way to forcibly rerun it is to override it with a new AsyncMemoizer(). You could do that easily like this
_getData() async ({bool reload = false}) {
if (reload) widget.asyncResults = AsyncMemoizer();
await widget.asyncResults.runOnce(() => yourApiCall());
}

call a build function in StatefullWidget after finish the Future function

i need to call and get the result from an async function before build the widget in statefulWidget in flutter, i tried to do like this, but it didn't work:
#override
void initState() {
super.initState();
loadDocument(details).whenComplete((){
setState(() {});
});
vid = YoutubePlayer.convertUrlToId(details);
}
#override
Widget build(BuildContext context) {
print("from details in build widget");
return Scaffold(
appBar: AppBar(
title: Text(name.toString()),
backgroundColor: Colors.redAccent,
),
body : Center(child: PDFViewer(document: controller.document)))
in this example, first thing call the function (loadDocument), after that call the (build) methode for widget, and then show the result after (whenComplete) has been finished, but what i need is to only call the build function after (whenComplete) finish....
this code for loadDocument
PDFDocument document = PDFDocument();
loadDocument(String url) async {
try {
return document = await PDFDocument.fromURL(
url);
} on Exception catch (e) {
print(e);
}
}
You can use FutureBuilder to build your ui based on the different Future states:
FutureBuilder<*Your future return type*>(
future: *Your future*,
builder: (BuildContext context, AsyncSnapshot<*Your future return type*> snapshot) {
if (snapshot.hasData) {
// Handle future resolved case
} else if (snapshot.hasError) {
// Handle future error case
} else {
// Handle future loading case
}
},
)
You can read more about FutureBuilder here: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Can I extend the State class and override setState

I kept running into the issue where you call setState when the widget is not mounted (especially after a data fetch). My question is:
Can I extend the State class and override setState like so
abstract class MountedState<T extends StatefulWidget> extends State<T> {
#override
void setState(fn) {
if(mounted) super.setState(fn);
}
}
I did this and it worked. I just want to know if it is not ideal or I am not supposed to
Future<void> fetchSubjectsAndClasses() async {
try {
Response res = await TeachersAPI.classesAndSubjects();
classes = res.data;
setState(() {});
} catch (e) {
print(e);
}
}
This is my data fetch that causes the issue. It gets called on initState
This is not ideal. You should be using a FutureBuilder when dealing with async functions that deal with the UI. It's not mandatory, but it takes care of the more annoying parts of updating the UI with Future data.
You should obtain your future in initState and store it in the State of the widget. Then pass that to your FutureBuilder in build:
Future myFuture;
#override
void initState() {
super.initState();
myFuture = futureCall();
}
#override
Widget build() {
return FutureBuilder(
future: myFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
//Show widget that has data
}
else if (snapshot.hasError) {
//Show widget that has error
}
else {
//Show widget while loading
}
}
);
}

State.initState() must be a void method without an `async` keyword

![State.initState() must be a void method without an async keyword.
how can i solve this probelms]1
#override
Future<void> initState() async {
// TODO: implement initState
super.initState();
_current_location();
BitmapDescriptor.fromAssetImage(
ImageConfiguration(devicePixelRatio: 2.5),
'assets/fff.png').then((onValue) {
pinLocationIcon = onValue;
});
//createCustomMarker(context);
// final Marker marker = Marker(icon: BitmapDescriptor.fromBytes(markerIcon));
DatabaseReference ref = FirebaseDatabase.instance.reference();
ref.child('users').once().then((DataSnapshot snapshot) {
Map<dynamic, dynamic> values = snapshot.value;
print(values.toString());
values.forEach((k, v) {
allMarkers.add(Marker(
markerId: MarkerId(k),
draggable: false,
icon: pinLocationIcon,
position: LatLng(v["latitude"], v["longitude"]),
infoWindow: InfoWindow(title: v["name"]),
onTap: () {
_onMarkerTapped(v["name"]);
},
),);
});
});
}
initState must be a method which takes no parameters and returns void. This is because it overrides the method of the same name in the superclass (either StatelessWidget or State<StatefulWidgetType>. As such, this limitation is a contract that is fixed and binding; you cannot change it.
Of course, this also means that initState cannot be marked as async. This is because any method marked as async will implicitly return a Future, but if the method returns anything, it cannot have a return type of void which breaks the override contract.
If you need to call an async method from within initState, you can do so simply by not awaiting it:
#override
void initState() {
super.initState();
doSomeAsyncStuff();
}
Future<void> doSomeAsyncStuff() async {
...
}
If, however, you need the data from the async method for your widget, you cannot simply wait for the Future to return before you build the widget. Flutter does not allow this, because there is no telling how long it will take for the Future to return, and stalling the widget building until then could potentially block your entire app.
Instead, you need to have your widget build normally and then have a way to notify your widget to update when the Future has returned. This is most easily done with a FutureBuilder:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doSomeAsyncStuff(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
// Future hasn't finished yet, return a placeholder
return Text('Loading');
}
return Text('Loading Complete: ${snapshot.data}');
}
);
}
(Notice how instead of calling the async method from initState, I am calling it from the FutureBuilder during the build process.)
EDIT: As pointed out, this approach only works in OP's situation where the awaited future will always eventually return a value. This is not always the case - sometimes the future doesn't return a value at all and is just a long-running process. Sometimes the future might return null instead of concrete data. And sometimes the future may result in an error instead of completing successfully. In any of these cases, snapshot.data will be null after the future completes, in which case snapshot.hasData will always be false.
In these situations, instead of depending on snapshot.hasData to wait for data to appear, you can use snapshot.connectionState to monitor the state of the future itself:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: doSomeAsyncStuff(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// Future hasn't finished yet, return a placeholder
return Text('Loading');
}
return Text('Loading Complete');
}
);
}