Flutter FutureBuilder keeps Firing and rebuilding. It Generates a New Random Number on Every Rebuild - How can I Prevent This?
Asked Answered
M

1

3

Context

I am encountering an issue with FutureBuilder in Flutter where it recreates its Future on every widget rebuild, causing unexpected behavior in my app.

Specifically, I have a FutureBuilder that fetches a random number, but each time I trigger a rebuild (e.g., by pressing a button to update a counter), it generates a new random number instead of using the initially fetched value.

Problem

The issue arises when I try to update a counter with a button press. Each press not only updates the counter but also regenerates the random number displayed by the FutureBuilder. I want to maintain the first generated random number unless explicitly refreshed.

Here is a video representation of the problem:

enter image description here

Code

Here is the complete runnable program that demonstrates the problem:

import 'package:flutter/material.dart';

import 'dart:math'; // For generating random numbers

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'FutureBuilder Example',
      home: FutureBuilderExample(),
    );
  }
}

class FutureBuilderExample extends StatefulWidget {
  const FutureBuilderExample({super.key});

  @override
  _FutureBuilderExampleState createState() => _FutureBuilderExampleState();
}

class _FutureBuilderExampleState extends State<FutureBuilderExample> {
  int counter = 0;

  Future<int> fetchRandomNumber() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Random().nextInt(100);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FutureBuilder with Random Number')),
      body: Column(
        children: [
          Text('Counter: $counter'),
          FutureBuilder<int>(
            future:
                fetchRandomNumber(), // Future is recreated here on every build
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else if (snapshot.hasError) {
                return Center(child: Text('Error: ${snapshot.error}'));
              } else {
                return Center(child: Text('Random Number: ${snapshot.data}'));
              }
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            counter++; // Triggering rebuild
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Question

I'm looking for a way to prevent the FutureBuilder from regenerating the future on each rebuild. How can I achieve this?

This question aims to serve as a canonical reference to address similar issues with FutureBuilder and StreamBuilder.

Michaelemichaelina answered 4/1 at 18:9 Comment(0)
M
7

Explanation

The issue you're experiencing with the FutureBuilder in Flutter is very common.

The key point to understand here is that FutureBuilder should be provided with a future that does not change on every build unless you specifically want the asynchronous operation to be re-performed.

When you create the future directly inside the FutureBuilder, it is re-created on every build (e.g. every setState), which is why you are observing the random number changing with every button press.

Solution: Using initState to Initialize the Future

The solution is to initialize your future once and use this stable reference within FutureBuilder. This is usually achieved by defining the future in the initState method of your StatefulWidget.

class _FutureBuilderExampleState extends State<FutureBuilderExample> {
  int counter = 0;
  late Future<int> randomNumberFuture; // Define the future here, use the `late` keyword

  @override
  void initState() {
    super.initState();
    randomNumberFuture = fetchRandomNumber(); // Initialize the future in initState
  }

Then, in your FutureBuilder:

 FutureBuilder<int>(
    future: randomNumberFuture, // Use the above-initialized future here
    builder: (context, snapshot) {

Code example

Here's your complete refactored code snippet utilizing initState:

import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'FutureBuilder Example',
      home: FutureBuilderExample(),
    );
  }
}

class FutureBuilderExample extends StatefulWidget {
  const FutureBuilderExample({super.key});

  @override
  _FutureBuilderExampleState createState() => _FutureBuilderExampleState();
}

class _FutureBuilderExampleState extends State<FutureBuilderExample> {
  int counter = 0;
  //1. Define the future here
  late Future<int> randomNumberFuture; // Define the future here

  @override
  void initState() {
    super.initState();
    //2. Initialize the future here
    randomNumberFuture =
        fetchRandomNumber(); // Initialize the future in initState
  }

  Future<int> fetchRandomNumber() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Random().nextInt(100);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FutureBuilder with Random Number')),
      body: Center(
        child: Column(
          children: [
            Text('Counter: $counter', style: TextStyle(fontSize: 24)),
            FutureBuilder<int>(
              // 3. Use the initialized future here
              future: randomNumberFuture, // Use the reference
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const CircularProgressIndicator();
                } else if (snapshot.hasError) {
                  return Text('Error: ${snapshot.error}');
                } else {
                  return Text('Random Number: ${snapshot.data}',
                      style: TextStyle(fontSize: 24));
                }
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            counter++; // Triggering rebuild, but not affecting the future
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

As you can now see, only the counter is updating, and not the random number:

enter image description here

Going further

  • I recommend that you watch this YouTube video on how to create a FutureBuilder the correct way. Where he explains your problem at 1:55 in the video.

  • Another YouTube video by Randal Schwartz on the problem and how to solve it

  • This StackOverflow answer.

Michaelemichaelina answered 4/1 at 18:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.