Have you prepared your coffee ‘☕’ ? You gonna need it 😎
Hi, this is the final part of this series.. and if you haven’t read my previous parts (1 & 2).. then you should definitely check them out and read them before continuing with us since lots of things we discussed.
So I firstly encourage you to check them
What this part is about?
In this part, we’re gonna be talking about database 😆.. Hmm database? will I have to write queries by my self 🤔? or lots of lines I have to write?
Well, non of that is gonna happen because we’re gonna be using a cool sql database package in our app which is called floor.
What’s floor and Why?
Floor is basically a package that uses the sqflite library behind the scene and generates code for you based on the abstraction. Floor is inspired by Android Room if you ever worked with android, then this may be familiar to you.
We could use NoSql databases like (Hive, ObjectBox, …etc) or we could even use the sqflite library itself. But the reason why I used this floor is because in some cases we need the Sql databases (like we do in Android) and since we’re getting deeply into it.. I thought it’s better to explain the most used, famous, and needed by community.
Explaining our database
After making sure that the package (dependency & dev_dependency) are added to the pubspec.yaml as we already did in Part 1, we have to prepare our database but before that we should explain simple stuff about how floor is actually working?
Let’s take a look at this simple diagram..
In the above diagram we see at the top is the Database which is the main database that contains our tables (columns and rows). But then after, we see something called ‘Dao’?
Dao which also stands for (Data Access Objects) is a component that is responsible for managing access to that sqlite database, and should always defined as an abstracted classes. The database itself can contain multiple DAOs at the same time and each Dao contains the methods signatures and queries statements.
We’ll explain more in details, but now all you have to know is that DAOs are very important so that our floor can know what to do.
Let’s jump into the coding side, head to the data/datasources/local and create a folder called “DAOs” (this folder will contain all of our DAOs files), and inside that folder create a file and call it articles_dao.dart which contains the following
kArticlesTableName is a constant value declared in the core/utils/constants.dart before, which is our table’s name (any name you choose).
As we said before, DAOs are always an abstracted classes.. and notice here that we’re annotating the class with @ dao so that the build_runner and our package knows that this class is Dao.
The interesting part is what’s inside the class, so we’ve defined bunch of methods..
- getAllArticles(): This method is annotated with @ Query which tells our what it should do according to the query statement we write.. currently its job is getting all the available articles we saved in our database which is obvious since the return type is Future of type list<Article>.
We can also replace the Future with Stream, in the above method.. since floor is reactive database which means any changes happen to the database we’ll be notified with the new changes as well.
- insertArticle(): Its job is inserting data into our database because we’re annotating this method by @ Insert. The Insert annotation takes an optional parameter to specify the onConflict algorithm. The onConflict gives us the options to choose what option to make if the database could have a conflicts while adding that entity (abort, replace, ignore, …etc).
- deleteArticle(): This method is basically perform the delete action on the given entity.
How floor identifies our entities?
Pretty question.. as we saw above in our Dao, we’re getting and setting data of type Entity (Article) that we defined in our domain/entities folder. But this is not enough to make those classes behaves like a database entity!!
What we could do here is simply annotating the entity with @ Entity as shown here
As simply as that, annotating this indicates that this entity is a database entity. And notice here if you remember in the previous part when we defined this class we said that the “id” field will be used later, well we’re now using it as the primary key for the table (articles table) since every table we create it should has a primary key and in our case the primary key is auto generated field which means every time we add an entity to the table this value will be generated for us.
Can floor store Dart objects directly?
If you take a look back again to the entity above (article.dart) we can see that floor will create a table for us that contains columns based on those fields (author, title, url, …etc) and if you read more about sqlite database then you should have known that it can only store primitive data types such as INTEGER(int), TEXT(String), REAL(float).. so how can we store a data of type (Source) in our case?
Well, luckily our floor provides us a type converter so we can convert our Source class to something that floor can store in the database.
Now, create another folder inside the data/datasources/local and call it converters which will contains all of our converters (currently we have only one) and inside this folder create a file and call it “source_type_converter.dart” which contains the following
There’s nothing fancy going on here, except we’re providing the input and the output of our converter and the way it should convert that type.
So in our case, we’re trying to convert our Source class into String value which can be stored in the database and the way that TypeConverter does it is by providing a (encode & decode) methods and we should specify or implement the way of encoding and decoding as shown above.
Where’s the database?
Our DAOs, Entities, and Converters won’t do anything for us unless we have a database to communicate with it. And creating a database with floor is much easier than we usually do with sqflite.
So, create a file in the data/datasources/local and call is app_database.dart which contains the following
This is our database as simply as that.. you can imagine that this is the place where you hook everything in together.
We define an abstracted class that extends from The FloorDatabase and inside it we’re telling our database that we have a DAO and it should implement that DAO for us.
But, before that.. this abstracted class won’t be recognized by floor unless we annotate the class with @ Database which tells floor that this is a floor database and should be implemented.
Also, we’re again annotating the class with @ TypeConverters which takes a list of Types (classes that implements a TypeConverter) so the database can use those type converters that we define when it needs to convert specific type (like in our case the Source class).
Running floor generator
Now, it’s time to generate the final results.. open the terminal and run the build_runner
> flutter pub run build_runner build
if you face a conflicts errors with the generator, then run the following
> flutter pub run build_runner build — delete-conflicting-outputs
After running the command, you should see a new generated file in the same folder called app_database.g.dart that contains the implementations and all stuff we want about our database and the tables that we will use.
Head to the domain/repositories/articles_repository.dart file and open it, you should see one method we defined earlier which gets articles remotely through a network request. Now, we’re gonna be adding 3 new methods that deals only with our database.. so go ahead and type the following
And by this, our repository now contains methods that gets data remotely (API) and locally (Database).. all we have left is implementing those abstracted methods in the Data layer but before that we need usecases for those.
Since we’re not using the repository directly inside the presentation layer, we create separate usecases which they depends on the repository itself. So we have to create 3 usecases since we have 3 methods as we defined in the repository.
Create this usecase inside the domain/usecases which contains the following
As we knew, we’re implementing a usecase which returns a list of article in this case and takes no parameters. This usecase as you can see depends on the repository we defined earlier (will be injected later).
Create another usecase for removing an article and type the following
Nothing new, except this time we’re returning nothing but void and this usecase take a parameter of type Article for removing it from database.
It’s not recommended here to directly use the Article as a parameter like we’re doing, instead we should create a separate Param class that contains this Article like this
But since we only have one parameter and no need to over engineer everything, So I used it directly.. if you have multiple parameter and want to keep the code cleaner, then I encourage you to do a separate class.
Create another usecase for inserting an article and type the following
Again, nothing fancy.. a usecase for getting articles into our database and take a parameter of type Article and returns nothing but void.
We’re back again to the data folder to implement the new abstracted methods that have been added in the domain repository. Go ahead and open the data/repositories/articles_repository_impl.dart and type the following implementations as follow
Now, since our repository contains a database implementation.. we need of course our database instance, but don’t forget that we’re not gonna instantiate it in the class.. instead we’ll inject this dependency later like we did before in Part 2.
What we’re doing here is just calling the DAO’s methods since our database (AppDatabase) provides that Dao (ArticlesDao) for us to use it in this repository.
Until now, we finished creating our database, defining and implementing the database methods that we need inside the repository in the Domain and Data layers.. all we have left now is the Presentation.
Of course 😆, remember? this is another different problem/task which is (getting data from our database) so we need a different bloc to solve/handle this.. and that’s why we have a folder that contains all of our blocs.
Enough talking, let’s write some code.. create a new bloc in the presentation/blocs and call it local_articles.. by using this extension that we talked about it in Part 1 & Part 2, it will automatically generate 3 files for you (events, states, bloc) like so
Let’s explain one by one starting from the bottom
By default, the extension will create an abstracted and initial state.. you can remove all of that or refactor it as I did here
Two different states we defined because we need the (Loading) since our incoming data from the database will be of type Future, then we defined another state (Done) so when the Future complete.. it will return results (list of articles) whether an empty data or not.
Refactor the contents inside this file to the following
In our events, we have 3 different events that will be triggered in the UI
- GetAllSavedArticles: will be responsible for getting all of our saved articles.
- RemoveArticle: will be responsible for deleting a specific saved article.
- SaveArticle: will be responsible for inserting that article into the database.
All those above events will be mapped to states in the bloc.
We’ve prepared our Event as well as the States, now we should implement the Bloc.. open the file (local_articles_bloc.dart) and remove all of the code then type the following
First thing here we see is that this bloc depends on the 3 usecases we wrote earlier and as we know that those dependencies will be later on injected.
Notice here that every bloc we defined so far doesn’t depend on a repository which may contains lots of unnecessary stuff that our bloc doesn’t need, but instead we’re using usecases that have one purpose, then the bloc will use the ones that are needed for the exact job.
Also this bloc initially will have a state of type (LocalArticlesLoading) as we can see in the super constructor which tells the bloc that this is the state that you should firstly and initially emit to the UI.
This is where we expect incoming events from the UI and we should return back the suitable state for those events.
So, if the UI added an event of type (GetAllSavedArticles) to our bloc, then we’re returning back the results of this method (_getAllSavedArticles()) which gets the saved articles from the database by using the usecase instance (_getSavedArticlesUseCase()) that this bloc depends on. And so on for the rest of our events, and notice here we’re returning back this method (_getAllSavedArticles()) in every event.. because if you removed or added an article then the UI needs to know what changes actually happened.
We could directly return the Future to the UI, then it will use the FutureBuilder to listen and build upon any changes but I kept things simple and clear to notice here.
Before we continue to build our UI (views, pages, widgets, …etc) we need to inject our dependencies because lots of classes including the blocs we built depends on usecases and so on.
Let’s open the src/injector.dart file and add those new lines
Comments with ‘*’ means added newly
First of all, we need to build our database.. and this is what we’re doing at the first 2 lines in the initializeDependencies method. We’re getting our database built using the databaseBuilder in the FloorAppDatabase which takes a name (kDatabaseName) and the build method will then return asynchronously an instance of our database (AppDatabase). Once we get our database, we register it as a singleton in our injector.
Next, the ArticlesRepository depends on the database so we inject it since we already registered the database.
After that, we need to register our new usecases as a singleton too.. and all of our usecase depends on the ArticlesRepository class.
And finally, we’re registering our new bloc (LocalArticles) as a factory because by doing this, it will automatically dispose this instance once we dispose our view. And this bloc depends on the 3 usecases we defined earlier.
Back again to the presentation because we need to create 2 different views
- ArticleDetailsView: the view where we can display more clear details about any article once we press it.
- SavedArticlesView: this is the view that’s responsible for displaying the saved articles once we navigate to it.
Let’s explain each one
Create a new file in the presentation/views and call it article_details_view.dart which contains the following
Hooks! you can see here and every view we create is extending from HookWidget and not StatelessWidget, basically a HookWidget itself extend from the StatelessWidget so they’re the same but the HookWidget provides an extra capabilities to use hook easily.
In the build() method, we’re providing our Bloc (LocalArticles) because we need to save or remove the article and that’s what this bloc is for. And the way we get an instance of that bloc is by the injector since we already registered this bloc as a factory.
Once, we press the floating action button.. we’re adding an event to the current bloc to save this article for later by using the BlocProvider. The BlocProvider is a provider for you registered blocs in the BuildContext and since we already provided and registered our bloc in the build method, we can easily call it from here and get the same bloc and add the events as we did.
We don’t forget that this view takes a parameter of type Article, in the app routes make sure to configure that as well.
Create another file in the same folder presentation/views and call it saved_articles_view.dart which contains the following
This view again uses the HookWidget, and providing the bloc in the build method as we did in the (ArticleDetailsView) previously.. but notice here that we’re adding an event once we provide that bloc and it means here that we’re initializing our bloc with that event and expect some new or different state back. By doing this, once we navigate to that view, the bloc will immediately get an event (GetAllSavedArticles) and should return the results back to the UI.
In the _buildBody() method, we can see that we have a BlocBuilder that listens to the bloc (LocalArticlesBloc) for any incoming states and build the suitable widget for it.
And also each saved Article we get, we have the ability to remove it by adding the remove event to the bloc with the provided article.. this is what the _onRemoveArticle method does for us.
Go ahead and run the app.. and if nothing goes wrong then, congrats 😎 you built a cleaner app with lots of things used.
I hope I covered the most important things that you need to build clean apps like we did.. I’ll be very thankful if you share it with your communities, friends, and people you know that maybe interested in.
Consider giving the repo a pretty small star ❤️ for a little support.
See you next articles.