Reputation: 21
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
B. Use Notifiers for @freezed State classes
C. Use AsyncNotifier and Notifier without creating a State class
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.
Upvotes: 1
Views: 520
Reputation: 1241
I usually do something similar to C with an extra SortedAndFilteredUsers
provider, the sorting and filtering logic is inside the SortedAndFilteredUsers
provider instead of the widget and your view consumes this provider directly. With this approach, the code is easier to read and the sorting/filtering logic can be reused across different widgets.
@riverpod
FutureOr<List<ModelUser>> users(UsersRef ref) {
final result = ... // fetch users from api
return result;
}
@riverpod
FutureOr<List<ModelUser>> sortedAndFilteredUsers(SortedAndFilteredUsersRef ref) async {
final users = await ref.watch(usersProvider.future);
final orderBy = ref.watch(orderByProvider);
final filter = ref.watch(filterProvider);
final result = ... // sort and filter users
return result;
}
@riverpod
class OrderBy extends _$OrderBy {
@override
String build() => 'id';
void set(String orderBy) {
state = orderBy;
}
}
@riverpod
class Filter extends _$Filter {
@override
String build() => '';
void set(String filter) {
state = filter;
}
}
Upvotes: 0