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


Solution 1:[1]

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

Solution 2:[2]

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.

Solution 3:[3]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jeremy Caney
Solution 2 croxx5f
Solution 3 Regular Jo