FutureBuilder runs twice - flutter

I have problems with FutureBuilder starting twice.
First it fetch the data correctly, returning my StartScreen, then after few seconds, the StartScreen rebuilds and I noticed that the FutureBuilder fires again.
Here is my code and it's pretty simple, so I wonder what may the problem be?!?
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
FirebaseUser user;
#override
void initState() {
// TODO: implement initState
super.initState();
getNewestlocation();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'APP',
theme: buildTheme(),
home: FutureBuilder<FirebaseUser>(
future: Provider.of<AuthService>(context).getUser(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print('error');
return Text(snapshot.error.toString());
}
user = snapshot.data;
print('user here $user');
return snapshot.hasData ? StartScreen(user) : LoginScreen();
} else {
return LoadingCircle();
}
},
),
);
}
}
Can anyone help me with this, please?

The future is firing again because you're creating it in the build method at the same time as the FutureBuilder.
From the FutureBuilder docs:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, 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.
So to prevent it from firing you'd have to do something like this:
class _MyAppState extends State<MyApp> {
Future<String> _myString;
#override
void initState() {
super.initState();
_myString = _fetchString();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _myString,
builder: (context, snapshot) {
// build page stuff...
},
),
);
}
}
Future<String> _fetchString() async {
print('running future function');
await Future.delayed(Duration(seconds: 3));
return 'potatoes';
}
Note, to access a provider in initState() you have to set listen to false, as detailed in this answer.

I think you have some things bad in your code, maybe that's not the problem but is good to correct that:
first: It is not recommendable to do that job in your main file, you should have something like a Splash page to handle that.
second: You should use blocs and not write your logic code on the same place at the view(UI)

If you're using android studio, try if running from the terminal fix the issue. The run button attached the debug service, which then force the entire app to be rebuilt

Related

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());
}

How can I refresh the page when the user comes back to the app (from background)?

Right now the only way my app can refresh (it's a news app so it needs to constantly refresh) is two ways: 1) Refresh by scrolling up or 2) Restart the app after killing it from background.
I want to make it so that when a user just comes back to the app (say I'm using my app, then I go to WeChat to send a text, then I come back), the app is refreshed.
This is the refresh scroll code.
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
new GlobalKey<RefreshIndicatorState>();
Then it calls this function:
Future<void> _refresh() async {
print("Refreshed");
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
),
).then((value) => null);
}
What should I do to achieve what I need?
Is there a way to check if someone has "returned" to my app? Then I can just call the function.
You need to subscribe to the application life cycle. In Flutter it's not build in the same was it is natively. However, there is a good article I stumbled across a last year, trying to accomplish the same thing:
https://medium.com/pharos-production/flutter-app-lifecycle-4b0ab4a4211a
The gist of the implementation looks like this:
class MyHomePage extends StatefulWidget {
#override _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('state = $state');
}
#override
Widget build(BuildContext context) {
return YourWidget();
}
}
A better solution is to give the child widget of your FutureBuilder or StreamBulder a random unique key. This forces the underlying widget to redraw (and trigger the future or stream) when you come back from the background.
key: UniqueKey(),
Example:
return FutureBuilder<PriceData>(
future: DbService.getPriceData(),
builder: (context, priceData) {
if (priceData.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
return AnimatedAppear(
key: UniqueKey(), <---- here!

Flutter: How to fetch data from api only once while using FutureBuilder?

How can I fetch data only once while using FutureBuilder to show a loading indicator while fetching?
The problem is that every time the user opens the screen it will re-fetch the data even if I set the future in initState().
I want to fetch the data only the first time the user opens the screen then I will use the saved fetched data.
should I just use a stateful widget with a loading variable and set it in setState()?
I'm using Provider package
Future<void> fetchData() async {
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
and my screen widget:
class _MyScreenState extends State<MyScreen> {
Future<void> fetchData;
#override
void initState() {
fetchData =
Provider.of<Data>(context, listen: false).fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchData,
builder: (ctx, snapshot) =>
snapshot.connectionState == ConnectionState.done
? Consumer<Data>(
builder: (context, data, child) => Text(data.fetchedData)): Center(
child: CircularProgressIndicator(),
),
);
}
}
If you want to fetch the data only once even if the widget rebuilds, you would have to make a model for that. Here is how you can make one:
class MyModel{
String value;
Future<String> fetchData() async {
if(value==null){
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
value=(YourReturnedString)
}
}
return value;
}
}
Don't forget to place MyModel as a Provider. In your FutureBuilder:
#override
Widget build(context) {
final myModel=Provider.of<MyModel>(context)
return FutureBuilder<String>(
future: myModel.fetchData(),
builder: (context, snapshot) {
// ...
}
);
}
A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<String> _future;
#override
void initState() {
_future = callAsyncFetch();
super.initState();
}
#override
Widget build(context) {
return FutureBuilder<String>(
future: _future,
builder: (context, snapshot) {
// ...
}
);
}
}
Or you can simply use a FutureProvider instead of the StatefulWidget above:
class MyWidget extends StatelessWidget {
// Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
#override
Widget build(BuildContext context) {
// print('building widget');
return FutureProvider<String>(
create: (_) {
// print('calling future');
return callAsyncFetch();
},
child: Consumer<String>(
builder: (_, value, __) => Text(value ?? 'Loading...'),
),
);
}
}
You can implement provider and pass data among its child.
Refer this example for fetching the data once and using it throughout its child.
As Aashutosh Poudel suggested, you could use an external object to maintain your state,
FOR OTHERS COMING HERE!
To manage state for large applications, the stateful widgets management becomes a bit painful. Hence you have to use an external state object that is shall be your single source of truth.
State management in flutter is done by the following libraries | services:
i. Provider: Well, i have personally played with this a little bit, even did something with it. I could suggest this for beginners.
ii. GetX: That one library that can do everything, its a good one and is recommended for novice || noob.
iii. Redux: For anyone coming from the react and angular world to flutter, this is a very handy library. I personally love this library, plus when you give it additional plugins, you are just superman
iv. Bloc: Best for data that is in streams. in other words, best for reactive programming approach....
Anyways, that was a lot given your question. Hope i helped

Flutter: Stateful Widget does not update

Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});

future builder keeps rebuilding on every setstate

I am building an app which uses an api and I am using the future builder to fetch the data but the problem is when the state changes it rebuilds and I want to prevent this from happen.
Thanks,
try using this :
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<response> future;
#override
void initState() {
future = _asyncmethodCall();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
Future<someResponse> _asyncmethodCall() async {
// async code here
}
}
similar question: How to deal with unwanted widget build?