Flutter Clean Architecture Series — Part 3 (UPDATED)

AbdulMuaz Aqeel
13 min readFeb 18, 2021

--

Hi, welcome again in another part of this series which is gonna be the final part. Before we start, make sure you’ve already read my previous articles and prepared everything for this one.

Here are the previous part:

What will be explained?

We’re gonna be talking about caching data using an sql database, and one might ask “would this make me write lots of queries and code?

Well, luckily non of that will happen because you don’t need to write and setup everything yourself which could be a time consuming problem, instead we’re gonna be using something called floor.

What is floor and Why?

Floor is a popular Object Relational Mapping (ORM) library for Flutter that provides a simple and easy-to-use interface for working with SQLite databases. It is built on top of the SQLite database engine, which is a lightweight and fast database engine that is widely used in mobile and web applications. With Floor, you can define database tables as Dart classes, and the library will handle the details of mapping the objects to database rows and columns. This allows you to work with the database in a more object-oriented way, making it easier to write and maintain your code.

Why it is popular?

  1. Easy to use: Floor provides a simple and intuitive API that makes it easy to work with SQLite databases. It uses familiar Dart language constructs, such as annotations and classes, to define database entities and query data.
  2. Type-safe queries: With Floor, you can write queries using the Dart programming language, which provides type-safety and helps prevent runtime errors. This makes it easier to write and debug queries, and reduces the likelihood of introducing errors into your code.
  3. Efficient database operations: It is built on top of the SQLite database engine, which is a high-performance and widely used database engine. This means that you can expect fast and efficient database operations, even for large datasets.
  4. Migrations support: Floor includes support for database migrations, which makes it easy to evolve your database schema over time as your application changes. This allows you to add, modify or remove database tables, columns or indexes while preserving your data.
  5. Abstraction layer: Floor provides a layer of abstraction over SQLite, which makes it easier to switch to other database engines or use different storage solutions in the future. This can help improve the maintainability of your codebase over time.

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:

The relationship between Entity, DAO and Database

In order to understand how floor is actually working, read the following steps:

  1. Define database entities: You first need to define your database entities as Dart classes. These classes represent tables in your database and contain fields that correspond to columns in the table. You can use annotations to define the table name, primary key, and other details.
  2. Define DAOs: After defining your entities, you need to define Data Access Objects (DAOs) to interact with the database. DAOs are interfaces that define methods for querying and updating the database. You can annotate these methods to define the SQL queries and map results to entity objects.
  3. Initialize the database: Once you have defined your entities and DAOs, you need to initialize the database using the FloorDatabaseBuilder class. This class creates a database instance and generates the necessary code for your DAOs to interact with the database.
  4. Use the DAOs to interact with the database: After initializing the database, you can use the DAOs to perform CRUD (Create, Read, Update, Delete) operations on the database. The DAOs provide a simple and type-safe interface for working with the database, using the methods that you defined earlier.
  5. Handle migrations: As your application evolves, you may need to modify the database schema. Floor provides support for database migrations, which allow you to update the database schema without losing data. You can define migration steps using the Migration class and run them using the database builder.

What’s DAO?

DAO stands for “Data Access Object” and it is a design pattern used in software development to provide an abstract interface for accessing a database or other data source. In the context of Flutter Floor, DAOs are interfaces that define methods for interacting with SQLite databases.

DAOs are used to define the SQL queries and mapping between database rows and Dart objects. DAO methods are annotated with the @Query annotation to define the SQL statement, or with the @Insert, @Update, or @Delete annotations to define the corresponding database operation.

Flutter Floor generates the necessary implementation code for these methods at compile-time, based on the annotations and types used in the interface. This allows you to work with the database using a simple and type-safe interface, without having to write SQL statements or handle database connections and transactions directly.

Data Folder

Let’s jump into the coding side, head to the lib/src/data/datasources/local and create a folder called “dao” (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:

lib/src/data/datasources/local/dao/articles_dao.dart

articlesTableName is a constant value declared in the lib/src/utils/constants/strings.dart before, which is our table’s name (any name you choose).

As we said before, DAOs are interfaces or 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 is currently responsible of getting all the articles inside the articles table as the query written above the method, and should return a Future of articles list.

Note: We can also replace the Future with Stream, in the above method, since floor acts as reactive database which means that any changes occurs to the database we should be notified with the new changes and updates the UI accordingly.

  • insertArticle(): It’s responsible for inserting data into our database since 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 would happen if the database faces a conflicts while adding that entity (abort, replace, ignore, …etc).
  • deleteArticle(): This method basically performs the delete action on the given entity.

Floor’s Entities?

As we saw above in our Dao, we’re getting and setting data of type (Article) that we defined in our lib/src/domain/models 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:

lib/src/domain/entities/article.dart

As simply as that, annotating the class indicates that this model 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 should has a primary key and in our case the primary key is auto generated field which means every time we add record to the table this field gets filled automatically with an incremental integer value.

Can Floor store complex Dart objects?

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 lib/src/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:

lib/src/data/datasources/local/converters/source_type_converter.dart

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. And creating a database with Floor is much easier than we usually do with sqflite.

So, create a file in the lib/src/data/datasources/local and call is “app_database.dart” which contains the following:

lib/src/data/datasources/local/app_database.dart

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 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 following command:

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.

Domain Folder

repositories

Head to the lib/src/domain/repositories folder and create a new file called “database_repository.dart” contains the following:

lib/src/domain/repositories/database_repository.dart

This abstracted repository holds the abstractions of our database functionality and should be implemented in the Data Layer like we did with the ApiRepository.

And by this, our repository now contains methods that gets data locally (Database).. all we have left is implementing those abstracted methods in the Data layer but before that we need usecases for those.

Data Folder

We’re back again to the data folder to implement the new abstracted repository that have been added in the domain.

Create a new file under lib/src/data/repositories and name it “database_repository_impl.dart” which contains the following code:

lib/src/data/repositories/database_repository_impl.dart

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.

Presentation Folder

Blocs (Cubits)

Remember? this is another different problem/task which is (getting data from our database) so we need a different cubit to solve/handle this, and that’s why we have a folder that contains all of our cubits.

Enough talking, let’s write some code. Create a new cubit in the lib/src/presentation/cubits and call it “local_articles”.

By using this extension that we talked about it in Part 1 & Part 2, it will automatically generate 2 files for you (states, cubit) like so:

  • local_articles_cubit.dart
  • local_articles_state.dart

Let’s explain one by one starting from the bottom

States (local_articles_state.dart)

By default, the extension will create an abstracted and initial state, you can remove all of that with the following:

lib/src/presentation/cubits/local_articles/local_articles_state.dart

Two different states we defined because we need the (Loading) since our incoming data from the database will be of type Future so it might take a little time to fetch the data. We also have another state (Success) so when the Future is completed, it will return results (list of articles) whether an empty data or not.

Loading: LocalArticlesLoading

Success: LocalArticlesSuccess

Cubit (local_articles_cubit.dart)

We’ve prepared our States, now we should write our logic inside the cubit. Open the file (local_articles_cubit.dart) and remove all of the code with the following:

lib/src/presentation/cubits/local_articles/local_articles_cubit.dart

First thing here we see is that this cubit depends on the DatabaseRepository dependency that will be later on injected into this class. Also this cubit initially will have a state of type (LocalArticlesLoading) as we can see in the super constructor which tells the cubit that this is the state that you should firstly and initially emit to the UI.

We’ve also defined multiple methods that will be called directly from the UI in which they contains the implementation of our database and the repository.

Dependency Injection (Service locating)

Before we continue to build our UI (views, pages, widgets, …etc) we need to register our dependencies because many classes including the cubits we built depends on them.

Let’s open the lib/src/locator.dart file and add those new lines

Comments with ‘*’ means added newly

lib/src/locator.dart

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 (databaseName) 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 locator.

Next, the DatabaseRepository depends on the database, so we provided an instance of the registered database in line 3.

Presentation Folder

views

Back again to the presentation because we need to create 2 different views

  • ArticleDetailsView: the view where we can display more details about any article once we click on it.
  • SavedArticlesView: this is the view that’s responsible for displaying the saved articles once we navigate to it.

Let’s explain each one:

ArticleDetailsView

Create a new file in the lib/src/presentation/views and call it “article_details_view.dart” which contains the following:

lib/src/presentation/view/article_details_view.dart

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 using our cubit (LocalArticlesCubit) because we need to save or remove the article and that’s what this cubit is for. The way we get an instance of that cubit is by using the BlocProvider to get our cubit from the the BuildContext (remember, that our context doesn’t currently has this cubit yet).

To add this cubit globally to our application’s widget tree, open up the “main.dart” file and add it to the MultiBlocProvider like so:

lib/main.dart

Now let’s go back to our view, and see that once the user click on the floating action button, the article should instantly gets saved in the database using the cubit we defined. And also shows a little toast message to the user.

We don’t forget that this view takes a parameter of type Article. Make sure to add this view to the application’s router configuration because otherwise it won’t work properly.

SavedArticlesView

Create another file in the same folder lib/src/presentation/views and call it “saved_articles_view.dart” which contains the following:

lib/src/presentation/views/saved_articles_view.dart

This view again uses the HookWidget, and using the cubit in the build method as we did in the (ArticleDetailsView) previously. Notice here that we’re not calling any method belongs to our cubit (LocalArticlesCubit) that can get all saved articles because we already did that in our “main.dart” file remember? using the cascade notation allows you to call a method on that object and once the BlocBuilder gets called, the method instantly get triggered as well (Lazy call).

In the _buildBody() method, we can see that we have a BlocBuilder that listens to the cubit (LocalArticlesBloc) for any incoming states and build the suitable widget for it. Also each saved Article we get, has the ability to remove itself by adding calling the remove method in the cubit with the provided article.

Run

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.

Github Source

Consider giving the repo a pretty small star ❤️ for a little support.

See you next articles.

Feedback

If you find something wrong or anything else, you can always reach me at

--

--

AbdulMuaz Aqeel

Senior Software Engineer at Talabatey (I Stand with Palestine 🇵🇸)