A Flutter app that uses the "Dummy Json" api.
- Login
- Fetch products
- Search products
- Pagination
-
Riverpod Used for state management
-
Freezed Code generation
-
Dartz Functional Programming
Either<Left,Right> -
Auto Route Navigation package that uses code generation to simplify route setup
-
Dio Http client for dart. Supports interceptors and global configurations
-
Shared Preferences Persistent storage for simple data
-
Flutter and Dart And obviously flutter and dart 😅
The data layer is the outermost layer of the application and is responsible for communicating with the server-side or a local database and data management logic. It also contains repository implementations.
Describes the process of acquiring and updating the data. Consist of remote and local Data Sources. Remote Data Source will perform HTTP requests on the API. At the same time, local Data sources will cache or persist data.
The bridge between the Data layer and the Domain layer. Actual implementations of the repositories in the Domain layer. Repositories are responsible for coordinating data from the different Data Sources.
The domain layer is responsible for all the business logic. It is written purely in Dart without flutter elements because the domain should only be concerned with the business logic of the application, not with the implementation details.
Describes the logic processing required for the application. Communicates directly with the repositories.
Abstract classes that define the expected functionality of outer layers.
The presentation layer is the most framework-dependent layer. It is responsible for all the UI and handling the events in the UI. It does not contain any business logic.
Widgets notify the events and listen to the states emitted from the StateNotifierProvider.
Describes the logic processing required for the presentation.
Communicates directly with the Providers from the domain layer.
main.dartfile has services initialization code and wraps the rootMyAppwith aProviderScopemain/app.darthas the rootMaterialAppand initializesAppRouterto handle the route throughout the application.servicesabstract app-level services with their implementations.- The
sharedfolder contains code shared across featuresthemecontains general styles (colors, themes & text styles)modelcontains all the Data models needed in the application.httpis implemented with Dio.storageis implemented with SharedPreferences.- Service locator pattern and Riverpod are used to abstract services when used in other layers.
For example:
final storageServiceProvider = Provider((ref) {
return SharedPrefsService();
});
// Usage:
// ref.watch(storageServiceProvider);- The
featuresfolder: the repository pattern is used to decouple logic required to access data sources from the domain layer. For example, theDashboardRepositoryabstracts and centralizes the various functionality required to fetch theProductfrom the remote.
abstract class DashboardRepository {
Future<Either<AppException, PaginatedResponse>> fetchProducts({required int skip});
Future<Either<AppException, PaginatedResponse>> searchProducts({required int skip, required String query});
}The repository implementation with the DashboardDatasource:
class DashboardRepositoryImpl extends DashboardRepository {
final DashboardDatasource dashboardDatasource;
DashboardRepositoryImpl(this.dashboardDatasource);
@override
Future<Either<AppException, PaginatedResponse>> fetchProducts(
{required int skip}) {
return dashboardDatasource.fetchPaginatedProducts(skip: skip);
}
@override
Future<Either<AppException, PaginatedResponse>> searchProducts(
{required int skip, required String query}) {
return dashboardDatasource.searchPaginatedProducts(
skip: skip, query: query);
}
}Using Riverpod Provider to access this implementation:
final dashboardRepositoryProvider = Provider<DashboardRepository>((ref) {
final datasource = ref.watch(dashboardDatasourceProvider(networkService));
return DashboardRepositoryImpl(datasource);
});And finally accessing the repository implementation from the Presentation layer using a Riverpod StateNotifierProvider:
final dashboardNotifierProvider =
StateNotifierProvider<DashboardNotifier, DashboardState>((ref) {
final repository = ref.watch(dashboardRepositoryProvider);
return DashboardNotifier(repository)..fetchProducts();
});Notice how the abstract NetworkService is accessed from the repository implementation and then the abstract DashboardRepository is accessed from the DashboardNotifier and how each of these layers acheive separation and scalability by providing the ability to switch implementation and make changes and/or test each layer seaparately.
The test folder mirrors the lib folder in addition to some test utilities.
state_notifier_test is used to test the StateNotifier and mock Notifier.
mocktail is used to mock dependecies.
test('dashboardDatasourceProvider is a DashboardDatasource', () {
dashboardDataSource = providerContainer.read
(dashboardDatasourceProvider(networkService));
expect(
dashboardDataSource,
isA<DashboardDatasource>(),
);
});And here is how we can test it separately from Flutter:
void main() {
late DashboardDatasource dashboardDatasource;
late DashboardRepository dashboardRepository;
setUpAll(() {
dashboardDatasource = MockRemoteDatasource();
dashboardRepository = DashboardRepositoryImpl(dashboardDatasource);
});
test(
'Should return AppException on failure',
() async {
// arrange
when(() => dashboardDatasource.searchPaginatedProducts(skip: any(named: 'skip'), query: any(named: 'query')))
.thenAnswer(
(_) async => Left(ktestAppException),
);
// assert
final response = await dashboardRepository.searchProducts(skip: 1, query: '');
// act
expect(response.isLeft(), true);
},
);
}
class MockRemoteDatasource extends Mock implements DashboardRemoteDatasource {}run bash gencov.sh
lib
├── configs
│ └── app_configs.dart
│
├── main
│ ├── app.dart
│ ├── app_env.dart
│ ├── main_dev.dart
│ ├── main_staging.dart
│ └── observers.dart
│
├── configs
│ └── app_configs.dart
├── routes
│ ├── app_route.dart
│ └── app_route.gr.dart
│
├── services
│ └── user_cache_service
│ ├── data
│ │ ├── datasource
│ │ │ └── user_local_datasource.dart
│ │ └── repositories
│ │ └── user_repository_impl.dart
│ ├── domain
│ │ ├── providers
│ │ │ └── user_cache_provider.dart
│ │ └── repositories
│ │ └── user_cache_repository.dart
│ └── presentation
│
├── shared
│ ├── data
│ │ ├── local
│ │ │ ├── shared_prefs_storage_service.dart
│ │ │ └── storage_service.dart
│ │ └── remote
│ │ ├── dio_network_service.dart
│ │ ├── network_service.dart
│ │ └── remote.dart
│ ├── domain
│ │ ├── models
│ │ │ ├── product
│ │ │ │ ├── product_model.dart
│ │ │ │ ├── product_model.freezed.dart
│ │ │ │ └── product_model.g.dart
│ │ │ ├── user
│ │ │ │ └── user_model.dart
│ │ │ ├── models.dart
│ │ │ ├── paginated_response.dart
│ │ │ ├── parse_response.dart
│ │ │ └── response.dart
│ │ └── providers
│ │ ├── dio_network_service_provider.dart
│ │ └── sharedpreferences_storage_service_provider.dart
│ ├── exceptions
│ │ └── http_exception.dart
│ ├── mixins
│ │ └── exception_handler_mixin.dart
│ ├── theme
│ │ ├── app_colors.dart
│ │ ├── app_theme.dart
│ │ ├── test_styles.dart
│ │ └── text_theme.dart
│ ├── widgets
│ │ ├── app_error.dart
│ │ └── app_loading.dart
│ └── globals.dart
│
├── features
│ ├── authentication
│ │ ├── data
│ │ │ ├── datasource
│ │ │ │ ├── auth_local_data_source.dart
│ │ │ │ └── auth_remote_data_source.dart
│ │ │ └── repositories
│ │ │ └── atuhentication_repository_impl.dart
│ │ ├── domain
│ │ │ ├── providers
│ │ │ │ └── login_provider.dart
│ │ │ └── repositories
│ │ │ └── auth_repository.dart
│ │ └── presentation
│ │ ├── providers
│ │ │ ├── state
│ │ │ │ ├── auth_notifier.dart
│ │ │ │ ├── auth_state.dart
│ │ │ │ └── auth_state.freezed.dart
│ │ │ └── auth_providers.dart
│ │ ├── screens
│ │ │ └── login_screen.dart
│ │ └── widgets
│ │ ├── auth_field.dart
│ │ └── button.dart
│ ├── dashboard
....
git clone https://github.com/Uuttssaavv/flutter-clean-architecture-riverpod
cd flutter-clean-architecture-riverpod
flutter pub get
flutter pub run build_runner build
flutter run or simply press F5 key if you are using VSCode
Do visit my portfolio site or connect with me on linkedin