Youya
Youya

Reputation: 21

Which model is better while using Riverpod

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

Answers (1)

Charles
Charles

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

Related Questions