In Flutter, why does a weather screen whose FutureBuilder calls fetchWeather() inside build() refetch on every rebuild, and how do you fix it?
answer
- build may run every frame
- a new Future each call
- didUpdateWidget compares futures
- store it in initState
- refetch when the city changes
basics
~20 sEach build() call runs fetchWeather() again and passes FutureBuilder a new Future, so it resubscribes, shows waiting and fires another request. Create the future once in a State field or initState and pass that field.
solid answer
~40 s`build` can run whenever a parent rebuilds, the theme or `MediaQuery` changes, or the keyboard opens, so `FutureBuilder(future: fetchWeather(city), ...)` starts a new HTTP request each time. `FutureBuilder`'s `didUpdateWidget` sees a future that is not `==` to the old one, drops the old one's callbacks and subscribes to the new one, so the UI flickers back to the waiting state and the server receives duplicate calls. The fix is to obtain the future outside `build`: keep it in a `State` field set in `initState`, re-create it in `didUpdateWidget` when an input such as the city changes, and in `setState` for a retry or refresh. A `StatelessWidget` cannot hold it, so either make the screen stateful or let a parent or state library own the request.
code
dart · 46 linesimport 'package:flutter/material.dart';
class Weather {
const Weather(this.summary);
final String summary;
}
class WeatherScreen extends StatefulWidget {
const WeatherScreen({super.key, required this.city, required this.fetch});
final String city;
final Future<Weather> Function(String city) fetch;
@override
State<WeatherScreen> createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
late Future<Weather> _weather;
@override
void initState() {
super.initState();
_weather = widget.fetch(widget.city);
}
@override
void didUpdateWidget(WeatherScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.city != widget.city) {
_weather = widget.fetch(widget.city);
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Weather>(
future: _weather,
builder: (context, snapshot) {
if (snapshot.hasError) return const Text('Weather unavailable');
if (snapshot.hasData) return Text(snapshot.requireData.summary);
return const CircularProgressIndicator();
},
);
}
}go deeper
Remember that build can run many times, so never start a request there; create the future once in initState.
Explain that FutureBuilder resubscribes when it receives a different future, and re-create the future in didUpdateWidget when an input changes.
Trace duplicate server calls and spinner flicker to futures created in build or in getters, and note that abandoned requests still run and cost load.
Decide when request ownership should move out of widgets into a cached repository or state library so rebuild frequency never drives network traffic.
## The bug A weather screen written like this looks harmless: ```dart @override Widget build(BuildContext context) { return FutureBuilder<Weather>( future: api.fetchWeather(widget.city), // runs on every build builder: (context, snapshot) => WeatherCard(snapshot: snapshot), ); } ``` The symptoms: the server sees a request each time the screen rebuilds, the spinner reappears when the keyboard opens or the device rotates, and data "flickers". On a slow network the request may never complete before the next rebuild replaces it. ## Why it happens Three facts combine: 1. **`build` is called often.** Flutter's API docs for `FutureBuilder` put it bluntly: assume that every `build` method could get called every frame, and treat omitted calls as an optimization. Parent rebuilds, `MediaQuery` changes (keyboard, rotation), theme changes and inherited-widget updates all trigger it. 2. **Calling an `async` function starts the work immediately** and returns a new `Future` object every time. The request is in flight before `FutureBuilder` even sees it. 3. **`FutureBuilder` compares futures in `didUpdateWidget`.** If the new widget's `future` is not `==` to the old one (two distinct `Future`s never are), it discards the old future's callbacks, moves the snapshot to `ConnectionState.none` and then `waiting`, and subscribes to the new one. The old request is **not cancelled**: a `Future` has no cancel. Its result simply arrives and is ignored because `FutureBuilder` tracks which subscription is current. ## The fix: obtain the future outside build The `FutureBuilder` docs require the future to have been obtained earlier, for example in `State.initState`, `State.didUpdateWidget` or `State.didChangeDependencies`. | Where | When to create or re-create the future | |---|---| | `initState` | the first load | | `didUpdateWidget` | an input from the widget changed, e.g. `widget.city` | | `didChangeDependencies` | it depends on an inherited value such as the locale | | `setState` in a handler | the user taps retry or pulls to refresh | ```dart class _WeatherScreenState extends State<WeatherScreen> { late Future<Weather> _weather; @override void initState() { super.initState(); _weather = api.fetchWeather(widget.city); } @override void didUpdateWidget(WeatherScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.city != widget.city) { _weather = api.fetchWeather(widget.city); } } // build() passes _weather to FutureBuilder } ``` Now rebuilds pass the **same** future, `didUpdateWidget` in `FutureBuilder` returns early, and the snapshot stays put. ## Variations of the same mistake - **A getter that fetches**: `Future<Weather> get weather => api.fetchWeather(city);` used as `future: weather` is the same bug in disguise. - **`StatelessWidget`**: it has no place to keep the future. Make it stateful, receive the future from a parent that owns it, or let a state library cache the request. - **`StreamBuilder`** has the mirror image: creating the stream in `build` makes it cancel and re-subscribe on each rebuild, which can drop events or restart a query. - **Side effects in `builder`**: starting the request inside the builder callback is equally wrong, since the builder also runs repeatedly. ## What interviewers listen for - That `build` must be cheap and idempotent, with no work started from it. - That identity matters: `FutureBuilder` only resubscribes when it receives a *different* future. - That the fix includes **when to refetch** (`didUpdateWidget` for changed inputs), not just `initState`. - That the abandoned request still runs and still costs bandwidth and server load.
- In Flutter, when FutureBuilder switches to a new future, is the old request cancelled?No. A Dart `Future` cannot be cancelled. `FutureBuilder` stops listening to the old future's result, and ignores it when it arrives, but the request itself still runs. Cancelling the underlying HTTP call needs the HTTP client's own mechanism.
- In Flutter, how would you refetch the weather when the user pulls to refresh?In the `RefreshIndicator`'s `onRefresh`, assign a new future inside `setState`, `setState(() => _weather = fetch(city))`, and return that future so the indicator stays visible until it completes. `FutureBuilder` receives a different future and resubscribes.
- In Flutter, why can't a StatelessWidget hold the future for its FutureBuilder?A `StatelessWidget` has no object that survives rebuilds; each rebuild may construct a new widget instance and re-run its `build`. Any future created there is recreated. The future needs a longer-lived owner: a `State`, a parent that passes it down, or a state library.
It is like a waiter who re-submits the kitchen order every time a customer glances at the menu. Each new ticket replaces the old one at the table, but the kitchen still cooks every order.
saying these in an interview costs you the question
- build() runs only once, so calling the API there is safe
- FutureBuilder cancels the old HTTP request when the future changes
- Marking the FutureBuilder const stops it from refetching
- initState alone is enough even when the city parameter changes
- FutureBuilder caches results by URL, so repeat calls are free