1

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

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the link to the Github issue discussing this. Is using pushReplacement best practices? Or should I just be using StatelessWidgets in this case? I came across some posts where people mentioned adding memoization/caching to check if it's already been loaded, and it just seemed like a huge hack to avoid the Navigator.push feature of reloading everything on the stack every time a new route is pushed.
If you use push , you need to do pop in somewhere. If you just do push without pop. when user click Back button on device, user have to click a lot of back button to go back to home. you can directly test with your case. and click back to see what happen.
I do not know how to caching to check if it's already been loaded, could you provide link to me. thanks.
I found this issue github.com/flutter/flutter/issues/11655 , there is a PR going on. may be this rebuild will go away after PR landed.
Yeah there are cases (like navigating from a Drawer on a main page) where having a back button doesn't make sense. I came across that same issue and got the impression that the Flutter devs don't believe that this is an issue. I think the documentation around this behavior could at least be improved, as it's unintuitive in my opinion. Fingers crossed that something is done, given the number of people posting about the issue there.
1

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.

1 Comment

In my research I came across this ongoing issue from August 17 2017 and closed 7 days ago. github.com/flutter/flutter/issues/11655 I personally find it weird that Flutter rebuilds the entire Navigator stack every time a route is pushed, just in case a previously visited page on the stack has changed. Thanks for the information on placing the fetch in initState. I must have overlooked that note when I read that documentation a few months ago.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.