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';
}
}