Reputation: 1712
This is probably caused by something stupid I'm doing or forgetting to do. Below is a full working code example (as of Dec5 2019) for Flutter and an HTTP echo server (httpbin) for reproducing this problem.
Run httpbin:
docker run -p 1234:80 kennethreitz/httpbin
Then load the code into a new Flutter app. On a fresh load of the app, click Route A in the drawer and you get the following printed to console:
flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 1 times.
Click Route B and you get:
flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 1 times.
flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 2 times.
(it reloads Route A, which performs another HTTP request).
Load Route B again and you get:
flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 2 times.
flutter: Loaded <RouteB> (Stateful)
flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 3 times.
flutter: Got data from Route B 3 times.
Load Route B another time and you get:
flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 4 times.
flutter: Loaded <RouteB> (Stateful)
flutter: Loaded <RouteA> (Stateful)
flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 5 times.
flutter: Got data from Route B 6 times.
flutter: Got data from Route A 4 times.
Each of these loads corresponds to an HTTP request, so if the app has been open long enough, it might make 100 HTTP requests for a single Stateful widget load.
Note that if you load Route C (a Stateless widget) it only ever loads once.
This obviously has something to do with how StatefulWidgets are reloaded, but I'm stuck and haven't been able to find posts with a similar problem online.
Why is Flutter doing this? How can I make it behave like a StatelessWidget for HTTP requests?
See code example below
/*
* Flutter code for weird HTTP behavior with StatefulWidget
*
* Make sure you're also running httpbin locally with the following command:
*
* docker run -p 1234:80 kennethreitz/httpbin
*/
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
int routeAqueries = 0;
int routeBqueries = 0;
int routeCqueries = 0;
void main() {
runApp(HttpDebug());
}
class HttpDebug extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'HomeDebug',
home: HomeDebug(),
);
}
}
class HomeDebug extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
drawer: _Drawer(),
body: Center(child: Text('Home')),
);
}
}
class RouteA extends StatefulWidget {
@override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
@override
Widget build(BuildContext context) {
print('Loaded <RouteA> (Stateful)');
return Scaffold(
appBar: AppBar(title: Text('Route A')),
drawer: _Drawer(),
body: FutureBuilder<String>(
future: fetchRoute('routeA'),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text('RouteA Data: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Loading');
}
},
),
);
}
}
class RouteB extends StatefulWidget {
@override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
@override
Widget build(BuildContext context) {
print('Loaded <RouteB> (Stateful)');
return Scaffold(
appBar: AppBar(title: Text('Route B')),
drawer: _Drawer(),
body: FutureBuilder<String>(
future: fetchRoute('routeB'),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text('RouteB Data: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Loading');
}
},
),
);
}
}
class RouteC extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('Loaded <RouteC> (Stateless)');
return Scaffold(
appBar: AppBar(title: Text('Route C')),
drawer: _Drawer(),
body: FutureBuilder<String>(
future: fetchRoute('routeC'),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text('RouteC Data: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Loading');
}
},
),
);
}
}
class _Drawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text('Home'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomeDebug()),
),
),
ListTile(
title: Text('Route A'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => RouteA()),
),
),
ListTile(
title: Text('Route B'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => RouteB()),
),
),
ListTile(
title: Text('Route C'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => RouteC()),
),
),
],
)
);
}
}
Future<String> fetchRoute(String route) async {
Map<String, int> routes = {
'routeA': 200,
'routeB': 201,
'routeC': 202,
};
final response = await http.get('http://localhost:1234/status/${routes[route]}');
if (response.statusCode == 200) {
print('Got data from Route A ${++routeAqueries} times.');
return 'Welcome to Route A';
} else if (response.statusCode == 201) {
print('Got data from Route B ${++routeBqueries} times.');
return 'Welcome to Route B';
} else if (response.statusCode == 202) {
print('Got data from Route C ${++routeCqueries} times.');
return 'Welcome to Route C';
}
}
Upvotes: 1
Views: 2547
Reputation: 1168
As chunhunghan mentioned, the fetch should take place in initState
and not the build
method (this documention and the two steps preceding it were helpful to me in understanding and fixing the number of requests.) Using the information in that link, I ended up with this for each stateful widget:
class _RouteAState extends State<RouteA> {
Future<String> _post;
@override
void initState() {
super.initState();
_post = fetchRoute('routeA');
}
@override
Widget build(BuildContext context) {
// ...
future: _post,
If I understand correctly, the number of requests isn't the only thing you're wanting to fix. Even with moving the fetch to initState
, you'll still be seeing multiple flutter: Loaded <RouteX> (Stateful)
being fired off for each navigation. This is because all the routes are still on the navigator stack, so the stateful ones have their build methods run for each route on the stack. The easiest patch to see the desired result would be to replace Navigator.push
with Navigator.pushReplacement
, but you might want something more elaborate, in order to prevent a back navigation from exiting the app.
There are quite a few other options for replacing routes, so be sure to see if any others fit your desired semantics better.
Upvotes: 1
Reputation: 54407
Reason
https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2
when rebuilt, the new widget has a different Future instance than the old one
https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
didUpdateWidget of the FutureBuilder state is being called every time a rebuild is issued. This function checks if the old future object is different from the new one, and if so, refires the FutureBuilder.
To get past this, we can call the Future somewhere other than in the build function.
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
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.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
Solution
https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
instead of having:
FutureBuilder(
future: someFunction(),
....
We should have:
initState() {
super.initState();
_future = SomeFunction();
}
and then
FutureBuilder(
future: _future,
And in your Drawer, you need to use pushReplacement
Upvotes: 1