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