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
Related
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());
}
Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
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.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.
![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');
}
);
}
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{}
I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.