Know right proper code structure - feature first structure with riverpod state management and writing test case with it.
#state#riverpod
Answer
Overview
This covers the recommended feature-first folder structure with Riverpod state management and how to write test cases alongside it.
Feature-First Folder Structure with Riverpod
textlib/ ├── core/ │ ├── di/ ← Dependency injection │ ├── network/ ← Dio, API client │ ├── theme/ ← App theme │ └── utils/ ← Helpers, extensions │ ├── features/ │ ├── auth/ │ │ ├── data/ │ │ │ ├── datasources/ ← Remote + local │ │ │ ├── models/ ← JSON models │ │ │ └── repositories/ ← Repo implementations │ │ ├── domain/ │ │ │ ├── entities/ ← Pure Dart classes │ │ │ ├── repositories/ ← Abstract interfaces │ │ │ └── usecases/ ← Business logic │ │ └── presentation/ │ │ ├── providers/ ← Riverpod providers │ │ ├── screens/ ← UI screens │ │ └── widgets/ ← Reusable widgets │ │ │ └── products/ ← Same structure │ └── main.dart
Riverpod State Management Setup
dart// features/auth/presentation/providers/auth_provider.dart final authRepositoryProvider = Provider<AuthRepository>((ref) { return AuthRepositoryImpl( remoteDataSource: ref.watch(authRemoteDataSourceProvider), ); }); final loginUseCaseProvider = Provider((ref) { return LoginUseCase(ref.watch(authRepositoryProvider)); }); final authStateProvider = AsyncNotifierProvider<AuthNotifier, AuthState>( AuthNotifier.new, ); class AuthNotifier extends AsyncNotifier<AuthState> { Future<AuthState> build() async => AuthState.initial(); Future<void> login(String email, String password) async { state = AsyncValue.loading(); final result = await ref.read(loginUseCaseProvider).call(email, password); state = result.fold( (failure) => AsyncValue.error(failure, StackTrace.current), (user) => AsyncValue.data(AuthState.authenticated(user)), ); } }
Test Structure
texttest/ ├── features/ │ ├── auth/ │ │ ├── data/ │ │ │ └── repositories/ │ │ │ └── auth_repository_test.dart │ │ ├── domain/ │ │ │ └── usecases/ │ │ │ └── login_usecase_test.dart │ │ └── presentation/ │ │ └── providers/ │ │ └── auth_provider_test.dart │ └── products/ │ └── ... └── test_helpers/ └── mock_providers.dart ← Shared mocks
Writing Tests with Riverpod
dart// test/features/auth/presentation/providers/auth_provider_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthRepository extends Mock implements AuthRepository {} void main() { group('AuthNotifier', () { late ProviderContainer container; late MockAuthRepository mockRepo; setUp(() { mockRepo = MockAuthRepository(); container = ProviderContainer( overrides: [ authRepositoryProvider.overrideWithValue(mockRepo), ], ); }); tearDown(() => container.dispose()); test('login success emits authenticated state', () async { when(() => mockRepo.signIn(any(), any())) .thenAnswer((_) async => User(id: '1', email: 'test@test.com')); await container.read(authStateProvider.notifier).login('test@test.com', 'pass'); final state = container.read(authStateProvider); expect(state, isA<AsyncData<AuthState>>()); expect(state.value!.isAuthenticated, true); }); test('login failure emits error state', () async { when(() => mockRepo.signIn(any(), any())) .thenThrow(ServerException('Invalid credentials')); await container.read(authStateProvider.notifier).login('bad@test.com', 'wrong'); final state = container.read(authStateProvider); expect(state, isA<AsyncError>()); }); }); }
UseCase Test
dart// test/features/auth/domain/usecases/login_usecase_test.dart void main() { late LoginUseCase useCase; late MockAuthRepository mockRepo; setUp(() { mockRepo = MockAuthRepository(); useCase = LoginUseCase(mockRepo); }); test('returns user on valid credentials', () async { final user = User(id: '1', email: 'test@test.com'); when(() => mockRepo.signIn(any(), any())).thenAnswer((_) async => Right(user)); final result = await useCase('test@test.com', 'password'); expect(result, Right(user)); verify(() => mockRepo.signIn('test@test.com', 'password')).called(1); }); }
Key Principle: Feature-first = all layers of a feature live together. Riverpod providers are the glue. Tests mirror the same folder structure with mocked dependencies injected via
overrides.textProviderContainer