Standard architecture
This document outlines the standardized patterns for all Riverpod services, providers, and bridges in the FlipDare application. Following these patterns ensures consistency, maintainability, and testability across the entire codebase.
- Naming Conventions
- File Structure
- Service Pattern
- State Management
- Testing Pattern
- Bridge Pattern
- Provider Initialization
- Usage Examples
- Migration Guide
// Service class names: XxxService
class AuthService extends _$AuthService { }
class PledgeService extends _$PledgeService { }
class UserService extends _$UserService { }
// Generated provider names: xxxServiceProvider
final authServiceProvider = ...;
final pledgeServiceProvider = ...;
final userServiceProvider = ...;
// State class names: XxxServiceState
class AuthServiceState { }
class PledgeServiceState { }
class UserServiceState { }
// Naming pattern: xxxIsLoading, xxxError, xxxData, etc.
@riverpod
bool authIsLoading(Ref ref) { }
@riverpod
String? authError(Ref ref) { }
@riverpod
bool pledgeIsLoading(Ref ref) { }
@riverpod
List<PledgeModel> pledgesList(Ref ref) { }
// Bridge class names: XxxBridge
class DareBridge extends BridgeService<DareModel> { }
class PledgeBridge extends BridgeService<PledgeModel> { }
class UserBridge extends BridgeService<UserModel> { }
// Generated provider names: xxxBridgeProvider
final dareBridgeProvider = ...;
final pledgeBridgeProvider = ...;
final userBridgeProvider = ...;
packages/services/lib/
โโโ provider/
โ โโโ auth/
โ โ โโโ auth_service.dart # Main service
โ โ โโโ auth_service_state.dart # State class
โ โ โโโ auth_event_data.dart # Event data
โ โ โโโ auth_service.g.dart # Generated file
โ โโโ pledge/
โ โ โโโ pledge_service.dart # Main service
โ โ โโโ pledge_service_state.dart # State class
โ โ โโโ pledge_service.g.dart # Generated file
โ โโโ user/
โ โ โโโ user_service.dart # Main service
โ โ โโโ user_service_state.dart # State class
โ โ โโโ user_service.g.dart # Generated file
โ โโโ current_user_service/
โ โ โโโ current_user_service.dart
โ โ โโโ current_user_service_state.dart
โ โ โโโ current_user_service.g.dart
โ โโโ _service_template.dart # Template file
โ โโโ _provider_pattern.dart # Pattern documentation
โโโ provider_bridge/
โ โโโ bridge_registration.dart # All bridge providers
โ โโโ bridge_service.dart # Base bridge interface
โ โโโ dare_bridge.dart # Dare bridge implementation
โ โโโ pledge_bridge.dart # Pledge bridge implementation
โ โโโ user_service.dart # User bridge implementation
โโโ test/unit/provider/
โโโ auth_service_unit.dart # Auth service tests
โโโ pledge_service_unit.dart # Pledge service tests
โโโ user_service_unit.dart # User service tests
โโโ _service_test_template.dart # Test template
All state classes follow this pattern:
class ServiceState {
final bool isLoading;
final String? error;
final DateTime? lastUpdated;
// Add service-specific properties here
const ServiceState({
this.isLoading = false,
this.error,
this.lastUpdated,
});
ServiceState copyWith({
bool? isLoading,
String? error,
DateTime? lastUpdated,
}) {
return ServiceState(
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
// Convenience getters - always include these
bool get hasError => error != null;
bool get isIdle => !isLoading && error == null;
bool get isReady => !isLoading && error == null;
}
All service classes follow this pattern:
@riverpod
class Service extends _$Service {
@override
ServiceState build() {
return const ServiceState();
}
// =============================================================================
// PUBLIC API METHODS - Always async with consistent error handling
// =============================================================================
Future<void> performAction() async {
state = state.copyWith(isLoading: true, error: null);
try {
// Business logic here
state = state.copyWith(
isLoading: false,
lastUpdated: DateTime.now(),
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
}
}
// =============================================================================
// UTILITY METHODS - Standard helper methods all services should have
// =============================================================================
void clearError() {
state = state.copyWith(error: null);
}
void reset() {
state = const ServiceState();
}
Future<void> refresh() async {
// Implement service-specific refresh logic
}
// =============================================================================
// CONVENIENCE GETTERS - Easy access to common state properties
// =============================================================================
bool get isLoading => state.isLoading;
String? get error => state.error;
bool get hasError => state.hasError;
bool get isIdle => state.isIdle;
}
Each service should provide these standard convenience providers:
@riverpod
bool serviceIsLoading(Ref ref) {
return ref.watch(serviceProvider).isLoading;
}
@riverpod
String? serviceError(Ref ref) {
return ref.watch(serviceProvider).error;
}
@riverpod
bool serviceIsReady(Ref ref) {
return ref.watch(serviceProvider).isReady;
}
// Start loading
state = state.copyWith(isLoading: true, error: null);
// Complete successfully
state = state.copyWith(
isLoading: false,
lastUpdated: DateTime.now(),
);
// Handle error
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
try {
// Risky operation
} catch (e) {
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
// Optional: rethrow if caller needs to handle
}
void runServiceUnitTests() {
group('Service Tests (Standardized Pattern)', () {
late ProviderContainer container;
setUp(() {
container = ProviderContainer(
overrides: [
// Add any required overrides
],
);
});
tearDown(() {
container.dispose();
});
group('Initialization Tests', () {
test('should initialize with default state', () {
final state = container.read(serviceProvider);
expect(state.isLoading, false);
expect(state.error, null);
expect(state.hasError, false);
});
test('should support ProviderScope', () {
expect(() => ProviderScope(child: Container()), returnsNormally);
});
test('should support UncontrolledProviderScope', () {
final testContainer = ProviderContainer();
expect(() => UncontrolledProviderScope(
container: testContainer,
child: Container()
), returnsNormally);
testContainer.dispose();
});
});
// More test groups...
});
}
class ServiceTestHelper {
static ProviderContainer createTestContainer({
List<Override> overrides = const [],
}) {
return ProviderContainer(overrides: overrides);
}
static ProviderContainer createMockContainer() {
final mockService = MockService();
return ProviderContainer(
overrides: [
serviceProvider.overrideWith(() => mockService),
],
);
}
}
All bridges are registered in a single file:
// bridge_registration.dart
@riverpod
DareBridge dareBridge(Ref ref) {
return DareBridge();
}
@riverpod
PledgeBridge pledgeBridge(Ref ref) {
final dareBridge = ref.watch(dareBridgeProvider);
return PledgeBridge(dareBridge: dareBridge);
}
@riverpod
UserBridge userBridge(Ref ref) {
return UserBridge();
}
class ServiceBridge extends BridgeService<ModelType> {
@override
Future<ModelType?> put(ModelType entry) async {
// Implementation
}
@override
Future<String> delete(String id) async {
// Implementation
}
// Service-specific methods
Future<List<ModelType>> fetchAll() async {
// Implementation
}
}
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final container = await ProviderInitialization.initializeApp();
runApp(
UncontrolledProviderScope(
container: container,
child: MyApp(),
),
);
}
class ProviderInitialization {
static Future<ProviderContainer> initializeApp() async {
final sharedPreferences = await SharedPreferences.getInstance();
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
);
return container;
}
}
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch entire state
final serviceState = ref.watch(serviceProvider);
// Watch specific aspects
final isLoading = ref.watch(serviceIsLoadingProvider);
final error = ref.watch(serviceErrorProvider);
// Trigger actions
final service = ref.read(serviceProvider.notifier);
return Column(
children: [
if (isLoading) CircularProgressIndicator(),
if (error != null) Text('Error: $error'),
ElevatedButton(
onPressed: () => service.performAction(),
child: Text('Perform Action'),
),
],
);
}
}
test('should perform action successfully', () async {
final container = ServiceTestHelper.createTestContainer();
final service = container.read(serviceProvider.notifier);
await service.performAction();
final state = container.read(serviceProvider);
expect(state.isLoading, false);
expect(state.hasError, false);
container.dispose();
});
Update imports:
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';Rename providers to follow consistent naming:
// Old final isUserAuthenticatedProvider = ...; // New final authIsAuthenticatedProvider = ...;Add utility methods:
void clearError() { /* implementation */ } void reset() { /* implementation */ } Future<void> refresh() async { /* implementation */ }Update tests to follow standard pattern:
setUp(() { container = ProviderContainer(overrides: [...]); }); tearDown(() { container.dispose(); });Run code generation:
dart run build_runner build --delete-conflicting-outputs
- Follow naming conventions
- Implement state class with copyWith and convenience getters
- Use @riverpod annotation for service class
- Implement standard utility methods (clearError, reset, refresh)
- Add convenience providers
- Create comprehensive unit tests
- Support both ProviderScope and UncontrolledProviderScope
- Handle loading states and errors consistently
- Document public API methods
- Run code generation
- Consistency: All services follow the same structure
- Maintainability: Easy to understand and modify
- Testability: Standardized testing approach
- Type Safety: Full type safety with code generation
- Performance: Efficient reactive updates
- Scalability: Easy to add new services
- Developer Experience: Clear conventions and patterns
This standardized approach ensures that all Riverpod services in the FlipDare application are consistent, maintainable, and easy to work with.