Fixing Flutter FutureBuilder Rebuilds That Fetch Data on Every Render
Your Flutter screen loads perfectly.
The API request succeeds.
The data appears exactly as expected.
Then you tap a button.
Or scroll the page.
Or rotate the device.
Suddenly your application sends the same HTTP request again.
And again.
And again.
Your backend logs show dozens of identical requests even though the user hasn't refreshed the page.
If this sounds familiar, you're likely creating a new Future every time Flutter rebuilds your widget tree.
This is one of the most common mistakes developers make when using FutureBuilder. The widget itself is not causing unnecessary network requests. Instead, the problem is usually where and how the Future is created.
This guide explains why FutureBuilder appears to reload data on every render, how Flutter's rebuild system works, and several proven techniques for preventing redundant API calls.
What You'll Learn
After reading this guide, you'll understand:
- How
FutureBuilderworks. - Why widgets rebuild.
- Why repeated API calls happen.
- How to cache a
Future. - When to reload data intentionally.
- Best practices for asynchronous UI design.
Understanding FutureBuilder
FutureBuilder listens to a Future and rebuilds its UI as the asynchronous operation progresses.
Typical states include:
- Waiting
- Completed
- Error
It does not automatically execute network requests.
It simply observes the Future you provide.
Why Widgets Rebuild
Flutter rebuilds widgets frequently.
Examples include:
- Calling
setState() - Parent widget updates
- Theme changes
- Orientation changes
- Screen resizing
- Localization updates
- Navigation events
Frequent rebuilding is completely normal and is part of Flutter's rendering architecture.
The Common Mistake
Many developers write:
FutureBuilder(
future: fetchUsers(),
builder: (context, snapshot) {
...
},
)
Every rebuild calls:
fetchUsers()
which creates a new Future.
A new future means a new HTTP request.
The Correct Approach
Instead, create the future once.
Example:
late Future<List<User>> _usersFuture;
@override
void initState() {
super.initState();
_usersFuture = fetchUsers();
}
Then:
FutureBuilder(
future: _usersFuture,
builder: ...
)
Now rebuilds reuse the existing future instead of creating another network request.
Why initState() Matters
initState() executes only once when the widget is inserted into the widget tree.
It is the ideal place for:
- Initial API calls
- Database queries
- File loading
- Authentication checks
- Configuration loading
Avoid placing one-time initialization inside the build() method.
Remember: build() Can Run Many Times
Developers often assume:
build()
runs once.
It does not.
Flutter may invoke it dozensβor even hundredsβof times during a screen's lifecycle.
For this reason, build() should remain lightweight and free of expensive side effects.
Avoid Side Effects in build()
The build() method should describe the UI, not perform work.
Avoid:
- API requests
- Database writes
- File downloads
- Analytics events
- Network mutations
Keeping build() pure makes your widgets predictable and performant.
When You Actually Want to Refresh
Sometimes you do want to fetch new data.
Examples include:
- Pull-to-refresh
- Retry button
- User changes filters
- Search query changes
- Pagination
- Manual reload
In these cases, intentionally assign a new future:
setState(() {
_usersFuture = fetchUsers();
});
Now the reload is explicit rather than accidental.
Parent Rebuilds Don't Mean Reloads
Suppose a parent widget rebuilds because:
- Theme changed
- Screen resized
- Another widget updated
Your child FutureBuilder also rebuilds.
If the future is cached, Flutter reuses the completed result without issuing another HTTP request.
State Management Alternatives
As applications grow, you may prefer dedicated state management solutions.
Popular options include:
- Provider
- Riverpod
- Bloc
- Cubit
- GetX
- ValueNotifier
These approaches separate business logic from UI and often simplify asynchronous data handling.
Cache API Responses
Sometimes caching the future isn't enough.
Consider caching:
- Repository results
- Database queries
- HTTP responses
- Memory objects
This reduces unnecessary network traffic and improves responsiveness.
Watch for Nested FutureBuilders
Nested FutureBuilders can unintentionally multiply API requests.
Example:
FutureBuilder
βββ FutureBuilder
βββ FutureBuilder
If each creates its own future during rebuilding, network traffic can grow rapidly.
Consider combining asynchronous operations or using state management instead.
Use Async Repositories
A repository layer centralizes data access.
Example responsibilities include:
- HTTP requests
- Caching
- Error handling
- Retry logic
- Data transformation
Your widgets remain focused on rendering the UI.
Handle Errors Gracefully
A good FutureBuilder should manage:
- Loading state
- Success state
- Empty state
- Error state
Providing meaningful feedback improves the overall user experience and simplifies debugging.
Real-World Example
A Flutter application displays a list of products retrieved from a REST API. The developer writes:
FutureBuilder(
future: api.getProducts(),
builder: ...
)
The screen works correctly until the user opens and closes a navigation drawer. Because the drawer interaction triggers a widget rebuild, the getProducts() method executes again, resulting in another API request. After several interactions, dozens of unnecessary requests appear in the backend logs, slowing the application and increasing server load.
The developer moves the API call into initState(), stores the returned Future in a state variable, and passes that cached future to FutureBuilder. Now the UI rebuilds as often as necessary without repeatedly calling the server. Data is refreshed only when the user explicitly taps the refresh button.
Debugging Repeated Requests
If your API appears to execute multiple times:
Check:
- Is the future created inside
build()? - Does a parent widget rebuild frequently?
- Is
setState()called unnecessarily? - Is the widget recreated entirely?
- Are multiple
FutureBuilders requesting the same data?
Logging rebuilds and network requests can quickly reveal the source of the problem.
Performance Tips
To improve performance:
- Cache futures.
- Cache API responses.
- Reduce unnecessary rebuilds.
- Use
constwidgets where possible. - Separate UI from business logic.
- Profile your application using Flutter DevTools.
Small improvements often produce noticeable gains.
Best Practices Checklist
When using FutureBuilder:
β
Create the future in initState()
β Cache the future
β
Keep build() free of side effects
β Refresh data intentionally
β Handle loading and error states
β Use repositories for data access
β Minimize unnecessary rebuilds
β Profile network requests
β Test offline behavior
β Document asynchronous flows
Common Mistakes to Avoid
Avoid:
β Creating futures inside build()
β Assuming build() executes only once
β Performing API requests during rendering
β Ignoring parent widget rebuilds
β Nesting multiple FutureBuilders unnecessarily
β Mixing UI rendering with business logic
β Refetching identical data repeatedly
Rebuilds Are NormalβRepeated Network Calls Are Not
Flutter's rebuild mechanism is one of the reasons the framework delivers smooth, reactive user interfaces. Rebuilding widgets is inexpensive and expected. The real issue arises when rebuilds are coupled with side effects such as creating new futures or making network requests.
Separating asynchronous operations from rendering logic allows Flutter to rebuild efficiently while avoiding unnecessary work.
Design for Predictable State
A well-structured Flutter application clearly separates UI, state, and data retrieval. Whether you use simple FutureBuilder widgets or advanced state management libraries, the goal remains the same: asynchronous work should happen only when necessary, not every time the screen redraws.
By caching futures, using repositories, and refreshing data intentionally, you'll create applications that are faster, more efficient, and easier to maintain as they grow.
Frequently Asked Questions (FAQ)
Why does FutureBuilder keep calling my API?
Usually because the Future is created inside the build() method. Every widget rebuild creates a new Future, which triggers another asynchronous operation. Store the future in initState() instead.
Does FutureBuilder rebuild every frame?
No. FutureBuilder rebuilds when its associated Future changes state or when its parent widget rebuilds. Flutter rebuilds are normal, but they do not automatically create new futures unless your code does so.
When should I create a new Future?
Create a new future only when fresh data is actually requiredβfor example, after a pull-to-refresh, a filter change, a search query update, or an explicit retry action.
Should I use FutureBuilder or a state management package?
FutureBuilder is excellent for simple asynchronous screens. For larger applications with shared state, caching requirements, or complex business logic, solutions such as Provider, Riverpod, Bloc, or Cubit often provide a cleaner and more scalable architecture.
Wrapping Summary
FutureBuilder is a powerful Flutter widget for displaying asynchronous data, but it should observe a Future rather than create one during every render. Most repeated API calls stem from constructing a new future inside the build() method, not from FutureBuilder itself.
By caching futures in initState(), separating data retrieval from UI rendering, using repositories or state management where appropriate, and refreshing data only when necessary, you can eliminate redundant network requests, improve application performance, and build Flutter apps that remain responsive and maintainable.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!