Services Provider Consistency Guide

This document outlines the consistent pattern used across all service providers in the services package. All providers now follow the modern @riverpod annotation pattern for better consistency, testing support, and code generation.

๐ŸŽฏ Consistent Pattern Overview

All services now follow the same pattern:

1. Service Class Structure

@riverpod
class ServiceName extends _$ServiceName {
  @override
  ServiceState build() {
    return const ServiceState();
  }

  // Service methods...
}

2. Convenience Providers

@riverpod
bool serviceIsLoading(Ref ref) {
  return ref.watch(serviceNameProvider).isLoading;
}

@riverpod
String? serviceError(Ref ref) {
  return ref.watch(serviceNameProvider).error;
}

3. Code Generation

All services require:

  • import 'package:riverpod_annotation/riverpod_annotation.dart';
  • part 'service_name.g.dart';
  • Running dart run build_runner build to generate providers

๐Ÿ“‹ Service Implementations

โœ… AuthService (auth_service.dart)

@riverpod
class AuthService extends _$AuthService {
  @override
  AuthState build() {
    return const AuthState();
  }

  // Methods: trySignIn, signIn, signUp, signOut, etc.
}

// Convenience providers
@riverpod bool authIsAuthenticated(Ref ref) => ...
@riverpod bool authIsLoading(Ref ref) => ...
@riverpod String? authError(Ref ref) => ...
@riverpod UserModel? currentUser(Ref ref) => ...

Usage:

// Access service
final authState = ref.watch(authServiceProvider);
final authService = ref.read(authServiceProvider.notifier);

// Use convenience providers
final isAuth = ref.watch(authIsAuthenticatedProvider);
final user = ref.watch(currentUserProvider);

// Call methods
await authService.signIn(attempt, rememberMe);

โœ… NetworkService (network_service_provider.dart)

@riverpod
class NetworkService extends _$NetworkService {
  @override
  NetworkServiceState build() {
    _startMonitoring();
    return NetworkServiceState();
  }

  // Methods: forceCheck, updateStatus, etc.
}

// Convenience providers
@riverpod bool isOnline(Ref ref) => ...
@riverpod bool isOffline(Ref ref) => ...
@riverpod NetworkStatus networkStatus(Ref ref) => ...

Usage:

// Access service
final networkState = ref.watch(networkServiceProvider);
final networkService = ref.read(networkServiceProvider.notifier);

// Use convenience providers
final online = ref.watch(isOnlineProvider);
final status = ref.watch(networkStatusProvider);

// Call methods
await networkService.forceCheck();

โœ… PledgeService (pledge_service.dart)

@riverpod
class PledgeService extends _$PledgeService {
  @override
  PledgeServiceState build() {
    return const PledgeServiceState();
  }

  // Methods: fetchMyPledges, createPledge, updatePledge, etc.
}

// Convenience providers
@riverpod bool pledgeIsLoading(Ref ref) => ...
@riverpod List<PledgeModel> pledgesList(Ref ref) => ...
@riverpod int pledgeCount(Ref ref) => ...

Usage:

// Access service
final pledgeState = ref.watch(pledgeServiceProvider);
final pledgeService = ref.read(pledgeServiceProvider.notifier);

// Use convenience providers
final pledges = ref.watch(pledgesListProvider);
final count = ref.watch(pledgeCountProvider);

// Call methods
await pledgeService.fetchMyPledges();
await pledgeService.createPledge(pledge);

โœ… UserService (user_service.dart)

@riverpod
class UserService extends _$UserService {
  @override
  UserServiceState build() {
    return const UserServiceState();
  }

  // Methods: fetchUser, searchUsers, getPublicProfile, etc.
}

// Convenience providers
@riverpod bool userIsLoading(Ref ref) => ...
@riverpod UserModel? fetchedUser(Ref ref) => ...

Usage:

// Access service
final userState = ref.watch(userServiceProvider);
final userService = ref.read(userServiceProvider.notifier);

// Use convenience providers
final loading = ref.watch(userIsLoadingProvider);
final user = ref.watch(fetchedUserProvider);

// Call methods
await userService.fetchUser(userId);
final users = await userService.searchUsers(query);

๐Ÿงช Testing Support

All services now have consistent testing support through the _testing_utilities.dart file:

import 'package:services/provider/_testing_utilities.dart';

// Create test container
final container = createTestContainer();

// Test with overrides
final container = createTestContainer(
  overrides: [
    authServiceProvider.overrideWith(() => MockAuthService()),
  ],
);

// Use convenience functions
expect(isAuthenticated(container), true);
expect(isOnline(container), true);
expect(getPledgeCount(container), 5);

๐Ÿ”„ Migration Benefits

Before (Inconsistent)

  • AuthService: Used StateNotifier with Ref parameter
  • NetworkService: Used StateNotifierProvider manually
  • PledgeService: Used @riverpod (correct)
  • UserService: Used @riverpod (correct)

After (Consistent)

  • All services: Use @riverpod annotation
  • All services: Generate providers automatically
  • All services: Have consistent convenience providers
  • All services: Support testing with same pattern

๐Ÿ“ Adding New Services

When adding new services, follow this template:

import 'package:riverpod_annotation/riverpod_annotation.dart';
// ... other imports

part 'your_service.g.dart';

@riverpod
class YourService extends _$YourService {
  @override
  YourServiceState build() {
    return const YourServiceState();
  }

  // Your service methods here
  Future<void> someMethod() async {
    state = state.copyWith(isLoading: true);
    try {
      // Implementation
      state = state.copyWith(isLoading: false);
    } catch (e) {
      state = state.copyWith(error: e.toString(), isLoading: false);
    }
  }
}

// Convenience providers
@riverpod
bool yourServiceIsLoading(Ref ref) {
  return ref.watch(yourServiceProvider).isLoading;
}

@riverpod
String? yourServiceError(Ref ref) {
  return ref.watch(yourServiceProvider).error;
}

โœจ Key Benefits

  1. Consistency: All services follow the same pattern
  2. Testing: Easy to mock and test with overrides
  3. Code Generation: Automatic provider generation
  4. Type Safety: Better type inference and compile-time checks
  5. Performance: Optimal re-rendering with fine-grained providers
  6. Maintainability: Single pattern to learn and maintain

๐Ÿš€ Usage Examples

Authentication Flow

class LoginWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isLoading = ref.watch(authIsLoadingProvider);
    final error = ref.watch(authErrorProvider);
    final authService = ref.read(authServiceProvider.notifier);

    return Column(
      children: [
        if (isLoading) CircularProgressIndicator(),
        if (error != null) Text('Error: $error'),
        ElevatedButton(
          onPressed: () => authService.signIn(attempt, true),
          child: Text('Sign In'),
        ),
      ],
    );
  }
}

Network Status

class NetworkIndicator extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isOnline = ref.watch(isOnlineProvider);

    return Container(
      color: isOnline ? Colors.green : Colors.red,
      child: Text(isOnline ? 'Online' : 'Offline'),
    );
  }
}

This consistent pattern makes the codebase more maintainable, testable, and easier to understand for all developers.