Skip to main content

Fetch Data

When your provider returns a Future or Stream, riverpod_craft wraps the result in DataState<T> — giving you loading, data, and error states automatically.

Functional Provider

The simplest way to fetch data:


Future<List<Note>> notes(Ref ref) async {
final response = await http.get(
Uri.parse('https://api.example.com/notes'),
);
return (jsonDecode(response.body) as List)
.map((e) => Note.fromJson(e))
.toList();
}

Use it in a widget with .watch() and .when():

class NotesPage extends ConsumerWidget {

Widget build(BuildContext context, WidgetRef ref) {
final state = ref.notesProvider.watch();

return state.when(
loading: () => const CircularProgressIndicator(),
data: (notes) => ListView(
children: notes.map((n) => ListTile(title: Text(n.title))).toList(),
),
error: (error) => Text('Error: $error'),
);
}
}

Class-Based Provider

Use a class when you need more control — custom methods, side effects with @command, or complex state management:


class Notes extends _$Notes {

Future<List<Note>> create() async {
final response = await http.get(
Uri.parse('https://api.example.com/notes'),
);
return (jsonDecode(response.body) as List)
.map((e) => Note.fromJson(e))
.toList();
}
}

The generated API is the same — ref.notesProvider.watch() works identically for both styles.

Family Provider

Add parameters to create separate provider instances for different arguments:


Future<Note> noteDetail(Ref ref, {required String id}) async {
final response = await http.get(
Uri.parse('https://api.example.com/notes/$id'),
);
return Note.fromJson(jsonDecode(response.body));
}

Each unique id creates its own provider with its own loading/data/error state:

// Two separate providers, each with their own state
final note1 = ref.noteDetailProvider(id: '123').watch();
final note2 = ref.noteDetailProvider(id: '456').watch();

Class-based family


class NoteDetail extends _$NoteDetail {

Future<Note> create({required String id}) async {
final response = await http.get(
Uri.parse('https://api.example.com/notes/$id'),
);
return Note.fromJson(jsonDecode(response.body));
}
}

Reading State

MethodUse
.watch()In build() — rebuilds widget on state change
.read()In callbacks — reads once, no rebuild
.select(selector).watch()Watch only part of the state

Widget build(BuildContext context, WidgetRef ref) {
// Watch full state — rebuilds on any change
final state = ref.notesProvider.watch();

// Select — only rebuild when note count changes
final count = ref.notesProvider
.select((s) => s.dataOrNull?.length ?? 0)
.watch();

// Read in a callback — no rebuild
return ElevatedButton(
onPressed: () {
final current = ref.notesProvider.read();
},
child: Text('Notes: $count'),
);
}

Reload & Invalidate

// Refetch and show loading state
ref.notesProvider.reload();

// Refetch silently (no loading state shown)
ref.notesProvider.silentReload();

// Mark stale — refetches on next access
ref.notesProvider.invalidate();

@keepAlive

By default, providers auto-dispose when no one is watching them. Use @keepAlive to prevent this:



Future<User> currentUser(Ref ref) async {
final response = await http.get(
Uri.parse('https://api.example.com/me'),
);
return User.fromJson(jsonDecode(response.body));
}

Use @keepAlive for data that should persist — like auth state, app config, or data that's expensive to refetch.

Listen for Changes

Use .listen() for one-time reactions like navigation or showing messages:

ref.notesProvider.listen((prev, next) {
next.whenOrNull(
error: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load: $error')),
);
},
);
});

Next

For paginated data (infinite scroll), see:

Pagination

For performing side effects (create, update, delete), see:

Side Effect (Command)