riverpod_architecture 1.0.4 copy "riverpod_architecture: ^1.0.4" to clipboard
riverpod_architecture: ^1.0.4 copied to clipboard

State management architecture built on Riverpod 3.0 with BaseNotifier, PaginatedNotifier, Either error handling, and sealed classes.

Riverpod Architecture #

Pub Version License

A Flutter state management architecture built on Riverpod 3.0, providing reusable notifier classes and utilities for clean, scalable applications. This package reduces boilerplate by providing standardized patterns for handling state, errors, pagination, and data mapping.

Features #

Modern Riverpod 3.0 - Fully migrated to latest Riverpod with Notifier base classes ✅ State Management - BaseNotifier for handling initial/loading/error/data states ✅ Pagination - Built-in PaginatedNotifier with infinite scroll support ✅ Error Handling - Functional error handling with Either<Failure, T>Global Providers - App-wide loading, failure, and info notifications ✅ Data Mapping - Mappers for API responses, forms, and requests ✅ Type Safety - Sealed classes with pattern matching ✅ Auto Dispose - Memory-efficient with auto-dispose variants

Installation #

Add to your pubspec.yaml:

dependencies:
  riverpod_architecture: ^2.0.0
  flutter_riverpod: ^3.0.3
  either_dart: ^1.0.0

Then run:

flutter pub get

Quick Start #

1. Wrap your app with BaseWidget #

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_architecture/riverpod_architecture.dart';

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BaseWidget(
      loadingIndicator: const Center(
        child: CircularProgressIndicator(),
      ),
      onGlobalFailure: (failure) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(failure.title),
            backgroundColor: Colors.red,
          ),
        );
      },
      onGlobalInfo: (info) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(info.message)),
        );
      },
      child: MaterialApp(
        title: 'My App',
        home: const HomeScreen(),
      ),
    );
  }
}

2. Create a Repository with Either error handling #

import 'package:either_dart/either.dart';
import 'package:riverpod_architecture/riverpod_architecture.dart';

class UserRepository {
  EitherFailureOr<User> getUser(int id) async {
    try {
      // Your API call here
      final response = await api.getUser(id);
      return Right(User.fromJson(response));
    } catch (error, stackTrace) {
      return Left(
        Failure(
          title: 'Failed to fetch user',
          error: error,
          stackTrace: stackTrace,
        ),
      );
    }
  }
}

3. Create a Notifier #

Basic Notifier

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_architecture/base_notifier.dart';

class UserNotifier extends AutoDisposeBaseNotifier<User> {
  late final UserRepository _repository;

  @override
  void prepareForBuild() {
    _repository = UserRepository();
  }

  Future<void> fetchUser(int userId) async {
    await execute(_repository.getUser(userId));
  }

  Future<void> refresh() async {
    await execute(_repository.getUser(_currentUserId));
  }
}

final userNotifierProvider =
    NotifierProvider.autoDispose<UserNotifier, BaseState<User>>(
  UserNotifier.new,
);

Family Notifier (with parameter)

When you need different instances for different parameters, use a Family notifier:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_architecture/base_notifier.dart';

class UserFamilyNotifier extends AutoDisposeFamilyBaseNotifier<User, int> {
  UserFamilyNotifier(super.userId);

  late final UserRepository _repository;

  @override
  void prepareForBuild(int userId) {
    _repository = UserRepository();
    // Automatically fetch user on build
    fetchUser();
  }

  Future<void> fetchUser() async {
    // Access the argument via 'arg' field
    await execute(_repository.getUser(arg));
  }

  Future<void> refresh() async {
    await execute(_repository.getUser(arg));
  }
}

final userFamilyNotifierProvider =
    NotifierProvider.autoDispose.family<UserFamilyNotifier, BaseState<User>, int>(
  UserFamilyNotifier.new,
);

// Usage in UI:
// final userState = ref.watch(userFamilyNotifierProvider(userId));

4. Use in UI with Pattern Matching #

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_architecture/base_notifier.dart';

class UserScreen extends ConsumerWidget {
  const UserScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('User Details')),
      body: switch (userState) {
        BaseInitial() => const Center(
            child: Text('Press button to load user'),
          ),
        BaseLoading() => const Center(
            child: CircularProgressIndicator(),
          ),
        BaseError(:final failure) => Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Error: ${failure.title}'),
                ElevatedButton(
                  onPressed: () => ref.read(userNotifierProvider.notifier).refresh(),
                  child: const Text('Retry'),
                ),
              ],
            ),
          ),
        BaseData(:final data) => UserDetailsWidget(user: data),
      },
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(userNotifierProvider.notifier).fetchUser(1),
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

Core Concepts #

State Types #

BaseState <T>

For single-value state management:

  • BaseInitial - Initial empty state
  • BaseLoading - Loading state
  • BaseError(Failure) - Error with failure details
  • BaseData(T) - Success with data

PaginatedState <T>

For paginated data:

  • PaginatedLoading - Loading first page
  • PaginatedLoadingMore(List<T>) - Loading more items
  • PaginatedLoaded(List<T>, isLastPage) - Successfully loaded
  • PaginatedError(List<T>, Failure) - Error preserving already-loaded data

Notifier Types #

Each notifier type has 4 variants:

Base Class Auto Dispose Family Auto Dispose + Family
BaseNotifier<T> AutoDisposeBaseNotifier<T> FamilyBaseNotifier<T, Arg> AutoDisposeFamilyBaseNotifier<T, Arg>
PaginatedNotifier<T, Param> AutoDisposePaginatedNotifier<T, Param> FamilyPaginatedNotifier<T, Param, Arg> AutoDisposeFamilyPaginatedNotifier<T, Param, Arg>
PaginatedStreamNotifier<T, Param> (same variants) (same variants) (same variants)

Recommended: Use AutoDispose variants by default for memory efficiency.

Pagination Example #

Basic Paginated Notifier

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_architecture/paginated_notifier.dart';
import 'package:either_dart/either.dart';

class UsersNotifier extends AutoDisposePaginatedNotifier<User, void> {
  late final UserRepository _repository;

  @override
  ({PaginatedState<User> initialState, bool useGlobalFailure})
      prepareForBuild() {
    _repository = UserRepository();
    // Automatically load first page
    getInitialList();
    return (
      initialState: const PaginatedState.loading(),
      useGlobalFailure: true,
    );
  }

  @override
  Future<Either<Failure, PaginatedList<User>>> getListOrFailure(
    int page,
    [void parameter]
  ) {
    return _repository.getUsers(page: page);
  }
}

final usersNotifierProvider =
    NotifierProvider.autoDispose<UsersNotifier, PaginatedState<User>>(
  UsersNotifier.new,
);

Family Paginated Notifier (with parameter)

When you need paginated lists that depend on a parameter (e.g., users by department):

class DepartmentUsersNotifier extends AutoDisposeFamilyPaginatedNotifier<User, void, String> {
  DepartmentUsersNotifier(super.departmentId);

  late final UserRepository _repository;

  @override
  ({PaginatedState<User> initialState, bool useGlobalFailure})
      prepareForBuild(String departmentId) {
    _repository = UserRepository();
    // Use 'arg' field to access the department ID
    getInitialList();
    return (
      initialState: const PaginatedState.loading(),
      useGlobalFailure: true,
    );
  }

  @override
  Future<Either<Failure, PaginatedList<User>>> getListOrFailure(
    int page,
    [void parameter]
  ) {
    // Access the department ID via 'arg' field
    return _repository.getUsersByDepartment(departmentId: arg, page: page);
  }
}

final departmentUsersNotifierProvider =
    NotifierProvider.autoDispose.family<DepartmentUsersNotifier, PaginatedState<User>, String>(
  (departmentId) => DepartmentUsersNotifier(departmentId),
);

// Usage in UI:
// final usersState = ref.watch(departmentUsersNotifierProvider('engineering'));

Using in UI

class UsersListScreen extends ConsumerWidget {
  const UsersListScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersState = ref.watch(usersNotifierProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => ref.read(usersNotifierProvider.notifier).refresh(),
          ),
        ],
      ),
      body: switch (usersState) {
        PaginatedLoading() => const Center(child: CircularProgressIndicator()),
        PaginatedLoadingMore(:final list) => _buildList(context, ref, list, isLoadingMore: true),
        PaginatedLoaded(:final list, :final isLastPage) => _buildList(context, ref, list, isLastPage: isLastPage),
        PaginatedError(:final list, :final failure) => list.isEmpty
            ? ErrorWidget(failure)
            : _buildList(context, ref, list, error: failure.title),
      },
    );
  }

  Widget _buildList(
    BuildContext context,
    WidgetRef ref,
    List<User> users, {
    bool isLoadingMore = false,
    bool isLastPage = false,
    String? error,
  }) {
    return ListView.builder(
      itemCount: users.length + (isLoadingMore || !isLastPage ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == users.length) {
          if (isLoadingMore) {
            return const Center(child: CircularProgressIndicator());
          }
          return ElevatedButton(
            onPressed: () => ref.read(usersNotifierProvider.notifier).getNextPage(),
            child: const Text('Load More'),
          );
        }
        return UserTile(user: users[index]);
      },
    );
  }
}

Advanced Features #

Global Providers #

The package provides three global providers for app-wide notifications:

// Show global loading overlay
ref.read(globalLoadingProvider.notifier).show();
ref.read(globalLoadingProvider.notifier).clear();

// Show global failure notification
ref.read(globalFailureProvider.notifier).set(
  Failure(title: 'Something went wrong'),
);

// Show global info notification
ref.read(globalInfoProvider.notifier).set(
  GlobalInfo(
    globalInfoStatus: GlobalInfoStatus.success,
    message: 'Operation completed successfully',
  ),
);

Data Mappers #

The package includes mapper interfaces for clean data layer patterns:

EntityMapper

class UserMapper extends EntityMapper<User, UserResponse> {
  @override
  User mapToEntity(UserResponse response) {
    return User(
      id: response.id,
      name: response.fullName,
      email: response.emailAddress,
    );
  }
}

FormMapper

class LoginFormMapper extends FormMapper<LoginRequest, LoginForm> {
  @override
  LoginRequest mapToRequest(LoginForm form) {
    return LoginRequest(
      email: form.email.trim(),
      password: form.password,
    );
  }
}

Custom State Handling #

Override behavior with callbacks:

Future<void> fetchUser(int userId) async {
  await execute(
    _repository.getUser(userId),
    withLoadingState: true,
    globalLoading: false,
    globalFailure: true,
    onDataReceived: (user) {
      // Custom data handling
      print('User loaded: ${user.name}');
      return true; // true to update state, false to handle manually
    },
    onFailureOccurred: (failure) {
      // Custom error handling
      print('Error: ${failure.title}');
      return true; // true to show globally, false to handle manually
    },
  );
}

Important Rules #

DO NOT Override build() Method #

The build() method is marked @nonVirtual. Instead, override prepareForBuild():

// ❌ DON'T DO THIS
@override
BaseState<User> build() {
  // This will cause errors!
}

// ✅ DO THIS
@override
void prepareForBuild() {
  _repository = ref.read(userRepositoryProvider);
}

Use Pattern Matching, Not .when() #

States are sealed classes without .when() methods. Use Dart 3 pattern matching:

// ❌ DON'T DO THIS
state.when(
  loading: () => LoadingWidget(),
  data: (data) => DataWidget(data),
  // ...
);

// ✅ DO THIS
switch (state) {
  case BaseLoading():
    return LoadingWidget();
  case BaseData(:final data):
    return DataWidget(data);
  // ...
}

Example Project #

A complete example app demonstrating all features is available in the example/ directory. It includes:

  • BaseNotifier Example - Single user fetch with error handling
  • PaginatedNotifier Example - User list with infinite scroll
  • Mock Repository - Clean repository pattern with Either
  • Pattern Matching - Modern Dart 3 state handling

Run the example:

cd example
flutter pub get
flutter run

Migration from Riverpod 2.x #

If you're migrating from an older version using StateNotifier:

  1. Replace StateNotifier with Notifier
  2. Replace AutoDisposeNotifierNotifier (use provider type for auto-dispose)
  3. Use NotifierProvider.autoDispose() instead of AutoDisposeNotifierProvider
  4. Update family syntax to NotifierProvider.family()
  5. Override prepareForBuild() instead of constructor initialization

Package Structure #

lib/
├── riverpod_architecture.dart          # Main export
├── base_notifier.dart                  # BaseNotifier exports
├── paginated_notifier.dart             # Pagination exports
└── src/
  ├── data/
  │   ├── mappers/                    # Mapper interfaces
  │   │   ├── entity_mapper.dart
  │   │   ├── form_mapper.dart
  │   │   ├── form_with_option_mapper.dart
  │   │   ├── mappers.dart
  │   │   └── request_mapper.dart
  │   └── mixins/
  │       └── error_to_failure_mixin.dart
  ├── domain/
  │   ├── either.dart                 # Either/Failure helpers
  │   └── entities/
  │       ├── failure.dart
  │       ├── global_info.dart
  │       ├── paginated_list.dart
  │       └── enums/
  │           └── global_info_status.dart
  ├── extensions/
  │   ├── int_extension.dart
  │   └── iterable_extensions.dart
  └── presentation/
    ├── mixins/                     # Notifier mixins
    │   ├── base_notifier_mixin.dart
    │   ├── paginated_notifier_mixin.dart
    │   ├── paginated_stream_notifier_mixin.dart
    │   └── simple_notifier_mixin.dart
    ├── notifiers/                  # Sealed states and notifiers
    │   ├── base_notifier.dart
    │   ├── base_state.dart
    │   ├── paginated_notifier.dart
    │   ├── paginated_state.dart
    │   ├── paginated_stream_notifier.dart
    │   └── simple_notifier.dart
    ├── providers/                  # Global providers
    │   ├── global_failure_provider.dart
    │   ├── global_info_provider.dart
    │   └── global_loading_provider.dart
    └── widgets/                    # BaseWidget, PaginatedListView
      ├── base_loading_indicator.dart
      ├── base_widget.dart
      └── paginated_list_view.dart

Requirements #

  • Dart SDK: >=3.0.0 <4.0.0
  • Flutter: Latest stable
  • Riverpod: ^3.0.3

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Credits #

Based on Q-Architecture, fully migrated to Riverpod 3.0 with modern patterns.

1
likes
160
points
322
downloads

Publisher

unverified uploader

Weekly Downloads

State management architecture built on Riverpod 3.0 with BaseNotifier, PaginatedNotifier, Either error handling, and sealed classes.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

either_dart, equatable, flutter, flutter_riverpod, meta

More

Packages that depend on riverpod_architecture