What is the best approach for managing State for Views in RiverPod? I want to have all the screen states (list data, order, page, search filter, etc.) in State.
A. Use AsyncNotifier for @freezed State classes
- It is necessary to prepare multiple Providers because they are all AsyncData and you cannot select for specific fields.
- The method of writing Provider for data that is not AsyncData is not good.
B. Use Notifiers for @freezed State classes
- It is necessary to perform initialization processing in Notifier's build method.
C. Use AsyncNotifier and Notifier without creating a State class
- It becomes difficult to understand the feeling of managing the state because the Notifier is disjointed.
- It has the least amount of code and is simple.
Below is a code sample.
View
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:openapi/openapi.dart';
import 'package:flutter_app/providers/users/list1.dart';
// import 'package:flutter_app/providers/users/list2.dart';
// import 'package:flutter_app/providers/users/list3.dart';
class UsersListPage extends HookConsumerWidget {
const UsersListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// list1, list3
final AsyncValue<List<ModelUser>> users = ref.watch(usersProvider);
final String orderBy = ref.watch(orderByProvider);
// list2
// final AsyncValue<List<ModelUser>> users = ref.watch(usersListPageNotifierProvider.select((value) => value.users));
// final String orderBy = ref.watch(usersListPageNotifierProvider.select((value) => value.orderBy));
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("TEST"),
),
body: users.when(
data: (users) {
print('rendering!!!');
return ListView(
children: users.map((user) {
return ListTile(
leading: const Icon(Icons.map),
title: Text('${user.lastName ?? ""} ${user.firstName ?? ""}'),
onTap: () {
context.go('/users/${user.id}');
},
);
}).toList(),
);
},
error: (err, stack) {
print('error!!!');
return Text('Error: $err');
},
loading: () {
print('loading!!!');
return const CircularProgressIndicator();
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// list1, list2
ref.read(usersListPageNotifierProvider.notifier).setOrderBy("id desc");
// list3
// ref.read(orderByProvider.notifier).set("id desc");
},
tooltip: 'Increment',
child: const Icon(Icons.sort),
),
);
}
}
A. list1.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:openapi/openapi.dart';
import 'package:dio/dio.dart';
import 'package:flutter_app/general_provider.dart';
part 'list1.g.dart';
part 'list1.freezed.dart';
@freezed
class UsersListPageState with _$UsersListPageState {
const factory UsersListPageState({
@Default([]) List<ModelUser> users,
@Default("id") String orderBy,
@Default("") String filter,
}) = _UsersListPageState;
}
@riverpod
class UsersListPageNotifier extends _$UsersListPageNotifier {
Future<List<ModelUser>> _fetchUsers() async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel());
final String orderBy = state.value?.orderBy ?? "id";
final users = await ref
.read(openApiProvider)
.getUserApi()
.searchUser(orderBy: orderBy, cancelToken: cancelToken)
.then((res) => res.data!.users!.toList());
return users;
}
@override
Future<UsersListPageState> build() async {
return const UsersListPageState().copyWith(users: await _fetchUsers());
}
setOrderBy(String orderBy) {
final previousState = state.valueOrNull;
if (previousState == null) {
return;
}
state = AsyncValue.data(previousState.copyWith(orderBy: orderBy));
refresh();
}
setFilter(String filter) {
final previousState = state.valueOrNull;
if (previousState == null) {
return;
}
state = AsyncValue.data(previousState.copyWith(filter: filter));
refresh();
}
refresh() async {
final previousState = state.valueOrNull;
if (previousState == null) {
return;
}
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final users = await _fetchUsers();
return previousState.copyWith(users: users);
});
}
}
@riverpod
Future<List<ModelUser>> users(UsersRef ref) {
return ref.watch(
usersListPageNotifierProvider.selectAsync((data) => data.users)
);
}
@riverpod
String orderBy(OrderByRef ref) {
return ref.watch(
usersListPageNotifierProvider.select((data) => data.value?.orderBy ?? "")
);
}
B. list2.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:openapi/openapi.dart';
import 'package:dio/dio.dart';
import 'package:flutter_app/general_provider.dart';
part 'list2.g.dart';
part 'list2.freezed.dart';
@freezed
class UsersListPageState with _$UsersListPageState {
const factory UsersListPageState({
@Default(AsyncValue.loading()) AsyncValue<List<ModelUser>> users,
@Default("id") String orderBy,
@Default("") String filter,
}) = _UsersListPageState;
}
@riverpod
class UsersListPageNotifier extends _$UsersListPageNotifier {
Future<List<ModelUser>> _fetchUsers() async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel());
final String orderBy = state.orderBy;
final users = await ref
.read(openApiProvider)
.getUserApi()
.searchUser(orderBy: orderBy, cancelToken: cancelToken)
.then((res) => res.data!.users!.toList());
return users;
}
@override
UsersListPageState build() {
ref.listenSelf((previous, next) {
if (previous != null) {
return;
}
_fetchUsers().then((List<ModelUser> users) {
state = state.copyWith(users: AsyncValue.data(users));
});
});
return const UsersListPageState();
}
setOrderBy(String orderBy) {
state = state.copyWith(orderBy: orderBy);
refresh();
}
setFilter(String filter) {
state = state.copyWith(filter: filter);
refresh();
}
refresh() async {
state = state.copyWith(users: const AsyncValue.loading());
final users = await AsyncValue.guard(() async {
return _fetchUsers();
});
state = state.copyWith(users: users);
}
}
C. list3.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:openapi/openapi.dart';
import 'package:dio/dio.dart';
import 'package:flutter_app/general_provider.dart';
part 'list3.g.dart';
@riverpod
class Users extends _$Users {
Future<List<ModelUser>> _fetchUsers() async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel());
final String orderBy = ref.watch(orderByProvider);
final users = await ref
.read(openApiProvider)
.getUserApi()
.searchUser(orderBy: orderBy, cancelToken: cancelToken)
.then((res) => res.data!.users!.toList());
return users;
}
@override
Future<List<ModelUser>> build() async {
return await _fetchUsers();
}
}
@riverpod
class OrderBy extends _$OrderBy {
@override
String build() => "id";
void set(String orderBy) {
state = orderBy;
ref.invalidate(usersProvider);
}
}
@riverpod
class Filter extends _$Filter {
@override
String build() => "";
void set(String filter) {
state = filter;
ref.invalidate(usersProvider);
}
}
Please let me know the pros and cons of each pattern. Also, please let me know if there are any improvements in each pattern.