Riverpod has been a great confusion and debate among the developers community. Unlike BLoC, Riverpod tries to provide many features which is good and bad at the same time.
After extensively working on BLoC architecture and clean architecture together, we found a way to integrate Riverpod into clean architecture. Being said that, we will also strip out some of the Riverpod features. We will use Riverpod just as a state management package. In our example app, we will keep aside the below features of Riverpod away from the project.
1. Riverpod error handling
2. Riverpod family modifier
3. Riverpod AsyncValue
The above features may cause anti-clean architecture of your project or having a very difficult time integrating riverpod into clean architecture. For error handling we may use Dartz, for family modifier we may use http package and instead of AsyncValue we will return our custom data type from our build() method.
Simply treat Riverpod as BLoC, just keep the state management part, keep everything else out of the project. You will see how well Riverpod blends into clean architecture. Then let's get started. Clean architecture tips quickly
Core and src folder
You may create a project and inside lib folder create core and src folder. Core folder would contain our important functionalities or features and src folder would include the app screen features. The src folder would also contain sub folders for clean architecture. Now first we see the src folder first and later we will talk about core folder in depth.
In the src folder you will create many sub folders based on your app screen or features. That's the general convention of clean architecture. But this is also true for MVC architecture. In our case we see at least four sub folders.
Here we will start from on_boarding folder. As the name suggest, it is a feature of on boarding at the start up time. In our case on_boarding would just contain two folder views and widgets. Having views and widgets are common folder convention in Flutter community.
Inside the views folder, we have StatefulWidget or StatelessWidget class file. In general only one class file. And inside the widgets folder, you will have widgets as classes those would be used by views class.
Auth folder is one of the most important features of any modern apps since we all require registration and login. It's almost at the heart of the app features.
Things started to get complex from here since, we require network connection, http request and error handling for registration and login.
At the same time, this is also place where we introduce correct folder structure for clean architecture. Let's see
Yes, this is where we introduce data, domain and presentation layers (folders) which stays at the core of the clean architecture. I have detailed introduction of each layer here on the BLoC clean architecture. Let me give you a quick overview.
Domain layer==> Make sure you always start with domain layer. In general inside domain layer you will have repos and usescases sub folder.
Repos would contain the network calls to the other layers like data layer. And usecases would tell you for which situations or cases we should call the repos.
In the usecases folder you may see that we have different files. These are usages, we call them usecases. Each of these dart files contain app features. These files or classes would be called from presentation layer. We will talk about presentation layer in depth layer.
Usescases files would contain simple dart class. Each usecases would contain one or two classes. Each of these usecases would be called from presentation layer(from the UI) and then each usecases would call the repos. These repos would reach out to data layer.
So you can see, data layer is the glue between presentation layer and data layer.
As you can see this is a login usecase. As the call method (the callable function of dart) gets triggered from presentation layer, we reach out to the repos. We can verify this via _repo.login() method. _repo is an instance of a class which resides inside repo folder or layer.
So for each of the usecases, there will be one method in AuthRepo abstract class.
So you may just put as many methods necessary for your auth screen features and later reach out to the data layer.
Data layer==> Domain layer reaches to data layer for repos implementation. In our case, auth repo methods will reach out to the data layer.
In general data layer will have datasources and repos folder. Domain layer repos first reaches to repos in the data layer. In this case it finds a same method in auth_repo_impl.dart which is login method. It's also true for other methods. Her we actually start the inter layer communication.
But of course, it's not clear enough how domain layer communicates with data layer even though we see the similar name pattern. In terms how it happens, we will see later. But as a hint I can tell you that, it happens through service locator. We are using get_it package as a service locator.
From the above picture, if we see that there's not actual implementation of login method. At the same time we see that there's _remoteDataSource.login() method. That's our actual implementation of login method. That you can see inside datasources layer file name auth_remote_data_src.dart.
in the auth_remote_data_src.dart file, login implementations happens in two ways.
1==>. first create an abstract class including the method names like login, registration..
2==>. then use the abstract class as a parent class and do the actual implementation in a method.
From the above picture, we see that we have an abstract class name AuthRemoteDataSrc. So we see that we need actual implementation of login in another class.
In the AuthRemoteDataSrc we will list all the methods declared without body. So far this almost same all the previous two files like auth_repo.dart and auth_repo_impl.dart.
The above picture shows the actual implementation of our repo. So far we have completed three steps. There's one more step before this repo comes alive. Now everything has to be triggered from the presentation layer. This is also where our Riverpod provider resides.
Firebase instances are in general placed in the data source folder.
Presentation layer==> This layer only talks with domain layer and thats it. For managing states, you need to put your state management mechanism here. In this case we will put our Riverpod's provider in this layer.
We choose to separate states that would be emitted or used by our app. Thus Riverpod acts like BLoC in terms of clean architecture approach.
This is where the expertise comes in and we made Riverpod state management completely acts like BLoC which is generally more accepted.
If you ever used BLoC or Cubit, you will see that pattern creating a class for each state. At the same time, you should notice that, the abstract class AuthState which is also just like BLoC or Cubit.
Instead of return Future or FutureOr type, return custom data type for Riverpod build() method. Return Future or FutureOr type would force you to handle error right in the view using Riverpod's own mechasim. This mechanism creates difficult to follow clean architecture pattern.