Which model is better while using Riverpod

369 Views Asked by At

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.

1

There are 1 best solutions below

0
On

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;
  }
}