Flutter: Riverpod FutureProvider When, Why, and When Not to Use It

Published on January 12, 2026 3 min read

If you’ve been writing Flutter apps for a while, you’ve almost certainly used FutureBuilder. It works, it’s simple, and it’s everywhere in Flutter examples. Then you meet Riverpod, see FutureProvider, and ask:

Why would I replace something that already works?

That question is fair. The short answer is: you don’t always need to. The longer answer is what this article is about.

FutureBuilder is not bad. The problem starts when your app grows. Typical FutureBuilder usage looks like this:

FutureBuilder<User>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}

if (snapshot.hasError) {
return Text(snapshot.error.toString());
}

return Text(snapshot.data!.name);
},
);

Every time the widget rebuilds, you risk re-running the future unless you cache it manually.

late final Future<User> _future;

@override
void initState() {
super.initState();
_future = fetchUser();
}

Now you’re managing lifecycle and async state yourself. Error handling, retry logic, invalidation… all live inside widgets. This quickly turns UI code into state-management code.

Why FutureProvider Exists

FutureProvider solves one main thing:

It separates async state from UI.

Instead of the widget owning the future, Riverpod owns it.

You get:

  • caching
  • auto disposal
  • easy invalidation
  • shared access across the app

All without touching initState.

Opening a Bottom Sheet and Fetching Data Use Case

Let’s talk about a very common scenario.

  • User taps a button
  • A bottom sheet opens
  • It fetches data from the network
  • The same sheet can be opened multiple times

Without FutureProvider (Common Mistake)

void openSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) {
return FutureBuilder<Settings>(
future: fetchSettingsFromApi(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}

if (snapshot.hasError) {
return Text(snapshot.error.toString());
}

return Text(snapshot.data!.title);
},
);
},
);
}

What’s the Problem?

  • Every time the sheet opens → network call
  • No caching
  • No way to refresh from outside
  • No reuse in another widget

This becomes worse if:

  • the sheet opens often
  • the API is slow
  • the data is shared elsewhere

With FutureProvider

final settingsProvider = FutureProvider<Settings>((ref) async {
return fetchSettingsFromApi();
});

void openSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (_) {
return Consumer(
builder: (context, ref, _) {
final settingsAsync = ref.watch(settingsProvider);

return settingsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text(err.toString()),
data: (settings) => Text(settings.title),
);
},
);
},
);
}

What Did We Gain?

  • Data is cached
  • Sheet can open multiple times without refetching
  • Another widget can use the same provider
  • Logic is testable without UI

When You Should Not Use FutureProvider

if the value:

  • changes frequently
  • is user-controlled
  • is updated locally

For example counter, toggle, selected tab. Use StateProvider or ChangeNotifierProvider

Don’t Use It for One-Time Fire-and-Forget Calls

If you just need to:

  • send analytics
  • call an API once on button tap
  • not show loading state

A plain Future is often better.

FutureProvider is not about replacing FutureBuilder. It’s about moving async state out of widgets.

Thanks for reading.

Flutter: Riverpod FutureProvider When, Why, and When Not to Use It

Category: flutterTags: flutter, riverpod, flutter-widget, state-management, flutter-app-development