In this video, I will guide you
through the process of building a fully functional Flutter application. The
app will consume a REST API to retrieve a list of upcoming movies. By following this
tutorial, you will learn the following: - How to use SOLID patterns
to consume a remote API. - How to implement the Repository Pattern
to create a local cache of data retrieved from the API in a local database.
- How to develop a mechanism to detect new data from the API, and flush the
local cache to download the updated data. - How to create an infinite scroll list to
display the retrieved data as the user scrolls. To implement the functionalities
I have described, we will be use some powerful libraries that will enable us
to reduce development time significantly: - To create our domain layer models,
we will be using the "freezed" library, which works seamlessly with Dart classes.
- We will simplify the process of defining our data layer models using "json_serializable".
- For making requests to the remote API, we will be using the "dio" library, which
streamlines the process of consuming a REST API. - We will be using "sqflite" for the database and
learn how to implement the Repository Pattern to adhere to Clean Architecture standards.
- Finally, for the infinite scroll functionality, we will use the
"infinite_scroll_pagination" package. This tutorial is designed for individuals who
are new to Dart and Flutter, but already have a basic understanding of these technologies.
I will not be covering the fundamentals, such as variables, functions, classes
or Flutter widgets. Therefore, you should have a solid foundation in these
basic concepts before beginning this tutorial. I will be explaining the basics of Clean
Architecture applied to a Flutter application. While I will not be strictly adhering to the
principles, I will be sharing the adaptation that I have been using for many years in my
own apps and those of my clients. I encourage you to observe closely and listen carefully
in order to improve your learning process. The complete source code for this
app is available along with helpful links to the tools that I will be
using in the video description. Let's get started with the tutorial! Let's begin by creating a Flutter application
for both Android and iOS. Once created, you can open the project in your preferred IDE, either
Android Studio or VSCode. For me, I'll be using Android Studio since it's the IDE that I'm most
familiar with and have been using for many years. The first step is to open the pubspec.yaml
file and remove all the default comment blocks. I'll also remove the cupertino_icons
package since it's unnecessary. Next, I'll remove flutter_lints and instead
install the lint package. This step is optional, and you can still follow this tutorial with
flutter_lints if you prefer. However, if you choose to use lint like I did, you'll need to
modify the analysis_options.yaml file accordingly. To ensure that the configuration is correct, you
can run "flutter analyze". I'll also remove some content from the test file to prevent it from
interfering with the output of "flutter analyze". Moving on, the first thing I'm going to do is to
create a model that will serve as a representation of a movie. However, before doing that, let's take
a moment to examine the API that we'll be using. You can find the link to this
page in the video description. We'll be using an endpoint that
returns a list of upcoming movies. If we take a look at the response JSON,
we can see the data that's returned. For this tutorial, we'll only be using the movie's
identifier, title, image, and release date. One of the fundamental principles of a
SOLID architecture is that each layer should have a distinct responsibility
and should be decoupled from the others. This means that any changes made to one layer
should not affect the rest of the layers, reducing the potential for failure
and improving maintainability. To achieve this, we'll create a movie model
that meets the requirements of our application and aligns with the business rules that govern
it. This should be done independently of the API response. Next, we'll create an entity
that accurately represents the data returned by the API. Finally, we'll create a class that
converts the API entity into our domain model. One option to create the movie model is to use a
Dart class, but there are some utilities that are not available by default in Dart, such as methods
to duplicate objects and comparison methods. These utilities should be present
in any language. To address this, I will use the freezed package, which
generates these utilities automatically. I will follow the installation commands provided
in the freezed documentation and create a domain directory, with a models directory inside where
the domain models of my application will reside. Next, I'll create a file called movie.dart
and define the class with its properties. Note that this class includes the @freezed
annotation and extends another class, among other differences from a standard Dart class. This
is all explained in the freezed documentation. My Movie class will contain the movie identifier,
title, an optional image, and release date. Finally, we need to auto-generate the freezed
files to provide us with the additional functions. I will document the autogenerate
command in the README file. Running this command will result
in a new file being autogenerated with all the extra functions I mentioned earlier. My next task is to create the network layer.
To achieve this, I will make a directory named "data" and then create another directory inside it
named "network". Within the "network" directory, I will create another directory named
"entity", which will host the network models. These models are classes that precisely
represent the content of the remote API responses. To create the network model, I will
create a file named "movie_entity.dart". I will utilize a web tool called "quicktype" which auto-generates corresponding Dart
classes after pasting the expected JSON. To proceed, I will visit the API page and copy
the response of the upcoming movies endpoint. Click on "Test Endpoint" to get a response
and then copy and paste it into quicktype. I will name the main class
"UpcomingMovies" and copy all the generated code into the newly
created file "movie_entity.dart". After that, I will remove the
properties that are not required and all the "typename" properties and
their corresponding enums that I can see. I am going to create a new class responsible for
mapping the network models to my domain models. To do this, I'll navigate to the network folder
using the terminal as it's more convenient for me than using the interface sometimes. Then,
I'll create a file named network_mapper.dart. As explained before, the purpose of this class is
to achieve a clear separation of responsibilities between the different layers of our application.
Our domain models will cater to the specific needs of our application, while the network models will
accurately represent the data returned by the API. To make the code more intuitive, I'll
change the name of Result to MovieEntity. Before I begin mapping, I'll define an
exception directory in the domain where I'll create a custom exception for mapping
errors. This exception will take two generics: From and To, representing the classes we
want to convert from and to, respectively. Now, to perform the mapping, I'll
use a try-catch block and throw the newly created MapperException
if an error occurs. This way, we'll get precise information about the type of
mapping being performed when the error occurred. I'll also create another method
to convert lists of objects. Inside the for loop, I'll define a try-catch
block to avoid suspending the mapping of the entire list if one object fails. If this occurs,
I'll log the error using the logger package, you can install it with this command. I'm going to create a class responsible for
handling API requests. To do this, I'll create a directory called "client" inside the "network"
folder and a file called "api_client.dart". I'll use the dio package to communicate with the API,
so I'll install it using the following command. First, I'll define the Dio variable and initialize
it the constructor. To communicate with the API, we need its URL, as well as the apiKey and apiHost
parameters, which are specific authentication parameters for the API being used. You can find
this information on the API page. I'll set up the Dio object so that it includes these two pieces
of information in the header of each request. Additionally, I'll add an interceptor to
log the requests made to the console so we can better understand their content and timing. Next, I'll define a method called
"getUpcomingMovies()" to retrieve a list of the upcoming movies. This method
will take two parameters: "page" and "limit", which will limit the number of results
and indicate which page we want to load. I'll implement this request
using the API documentation. I'll create a custom exception
to handle communication errors. This exception will contain the status code
of the error and optionally an error message. If the response from the request has an error code
(400 or higher), I'll throw a NetworkException. If it has a status code but it's less than 400, it means the request was successful, so
I'll return the UpcomingMovies class. If it doesn't even have a status
code, something bad has happened, so I'll throw a traditional exception. Finally, I'll make some changes
to the "movies_entity.dart" file, installing json_serializable and following its
documentation to auto-generate the mapping code. After running the build_runner command, the
necessary methods will be auto-generated. After these changes, the error in the
"api_client.dart" file should be resolved. Our next step is to create a
crucial component: the repository. Repositories are responsible for providing
data to other layers while abstracting them from technical details. This means that the
repository will give you a list of movies, regardless of whether it obtained them from the
internet or a local database. This is an essential concept in implementing Clean Architecture,
where the data layer takes responsibility for dealing with data, and the domain and presentation
layers need not be concerned about its origin. To proceed, create a new directory called
"repository" in the "data" folder, and then create a new file called "movies_repository.dart". In
this file, we'll define the "getUpcomingMovies()" method, which will obtain data from the remote API
and map it to domain objects before returning it. To do this, we'll need instances
of "ApiClient" and "NetworkMapper". In a typical Clean Architecture system, we would
create a layer called "UseCases" or "Interactors". However, based on my experience,
these classes are usually empty, as the business logic for different systems
usually resides on backend servers. Hence, I opt to utilize repositories directly from
the presentation layer to work with data. To verify that everything is working correctly,
I want to view a sample of the data. First, create a directory called "presentation"
to handle the visual interface layer. Inside this directory, create
a sub-directory called "list" and create a new file within it
named "movies_list_screen.dart". In this file, I'll define the screen where
the list of movies will be displayed. To manage the state of the application
and obtain a reference to the repository, I'll use the provider package. If you're
already familiar with a different dependency injection or state management library,
feel free to use it instead. I prefer provider because I've been using it
for many years, but for this example, I won't make extensive use of it. You can
use the mechanisms you're comfortable with. Initially, I'll define a simple
user interface to load the first ten results. Later, we'll improve this interface, but for now, I just want to confirm if the
request and mapping are working correctly. Next, create the root widget of the application by defining the "app.dart" file
within the "app" directory. I'll set the screen I just created
as the home screen of the app. The only part missing is the creation of
all the classes and the initialization of the dependency injection, which is nothing more
than creating the class tree and establishing the way to satisfy each of the dependencies for each
class. But before, we are going to create a small functionality in which our app will be able
to load data from a local configuration file. This will help us to create a json file where
we will write our apiKey and our apiHost in a file that will not be tracked by git, in
this way we can save our credentials safely. To do this, create an assets directory, and
inside another config directory. We then set this configuration in pubspec.yaml so that Flutter
can access the files inside this config folder. Now create within the config folder
a file called config.json.temp, where we will place the schema of our
json as an example. The objective of this class is to have a file that acts as a
template in order to create the actual file. This file will be tracked by git, but it will
not contain the actual credentials. Then create another identical file, called config.json,
and put your actual API credentials in it. Now open the .gitignore file and write the
path to the config.json file. From now on you can commit and push your project without fear of
your credentials being in the remote repository. Now I'm going to create a model class to
represent this configuration file. Create the config.dart file in the model folder in the
domain layer. We will use freezed again for this. It will be necessary to run
the model autogeneration again. Now open main.dart, remove
everything except the main() method. Create a function that will return
the Config model, this function will contain the Logger parameter, which
we will use to log information. The job of parsing this json will
be done inside a try catch block. We can get the raw content of the file through
this variable called rootBundle that is inside the Flutter services package. We then decode it
with json.decode() as a map of String and dynamic. Then we simply have to return
Config, and in each parameter, we mention the name that identifies each field. The method to load the
rootBundle file is loadString(). If an error occurs, re-throw
the exception. From now on, this configuration file must be present
and valid for the application to run. Now we need to create the dependency list. Create
a class called InitialData, which contains a list of providers. We will use this class to store all
the providers and send them to the App widget. Now create a method that returns a Future
with InitialData. Within this method, I am going to proceed to instantiate all the
classes that we have been creating so far. At the end of it all I have to do is return
InitialData with this list of instances. In the main method, I get the InitialData
instance and pass it to the App widget. To do this I have to modify the App
widget to accept this input parameter. I'm going to wrap MaterialApp
inside a MultiProvider, and I'm going to provide it with the
list of providers I just created. This way, everything below the MultiProvider
can access all the providers I have defined. As I explained to you before, this is the way
to do it with the provider package. If you are using another dependency injection
methodology the way will be different. Now we are going to run the app to see if
everything works well. I get an error, we need to add WidgetsFluterBinding.ensureInitialized() to
the beginning of the main() method of main.dart. We run again and see that the list of
upcoming movies has loaded correctly. Ok, now we can successfully get the data from the
remote API. Next, we will proceed to improve the user interface a bit, we will make it possible
to scroll infinitely in this list of movies, and we will define our database layer
to create a local cache of data. Before continuing I would like to ask you a
favor. If you don't mind, could you please take a moment to give me a like and write a
comment with your feedback on this tutorial? I'm always looking to improve, so any comments
or suggestions would be greatly appreciated. Also, if you're able to, you can also
support me by clicking on the "Super Thanks" button below this video or visiting
my website's donation page. The link is on the video description. Thank you so much for your
support, and now let's get back to the tutorial! Next, we'll make some improvements to the
interface. To do this, create a file called "movie_preview.dart" in the "list" directory
within the "presentation" folder. Here, we will create a widget that represents
each movie in the list of entries. First, we will define a variable called "size",
which will determine the size of the movie image. The widget will take the movie to
be displayed as an input parameter. For each entry, we'll use a Card widget
that contains the movie's image, title, and release date. If the movie doesn't have an
image, we'll show a placeholder image instead. If the movie does have an image, we'll
use the "cached_network_image" package to download and cache the image. This way,
if the image needs to be rendered again, it will be loaded from the cache,
saving battery and bandwidth. If we need to display a placeholder image,
we'll define a specific widget for it. For now, we'll use one of the
icons already included in Flutter. Next, we will add the title and release date.
We will put everything inside a Flexible widget, because the title may be too long. By
putting everything inside Flexible, the Text widget that contains the title will
be able to correctly apply the ellipsis. To display the release date correctly, we'll
need to format the DateTime. We can do this using the DateFormat class, but we'll
need to install the "intl" package first, which contains utilities related to app
internationalization and language-related tools. We can then use this
DateFormat to format the date. Now, let's go to the "MoviesListScreen" and replace what's there with
the widget we just created. You may encounter an error at this
point, which can be solved by opening the "Podfile" file in the iOS folder
and adding the following block of code. Finally, we can execute the code
and see the new list interface. Now I want to create a screen to get the detail
of each movie. We will access this screen by tapping on each item on the list. Create a
detail directory in the presentation folder, and inside the movie_detail_screen.dart file. This widget receives a movie to display its
data. What I'm going to do is put it all inside a CustomScrollView, because I want to display
the movie image in the top bar, but I want it to get smaller if the user scrolls down. You
will see the final result in a moment. At the moment we define the list of slivers as
follows, first a SliverAppBar with the image of the movie, but we check that if the movie has
no image then we will not give it the same amount of height. Then a SliverList with a single
element: the synopsis of the movie. What's happening here is that the API we're using
doesn't give us the description of the movie, so for the purposes of this tutorial I'm going to
generate a lorem ipsum text and paste it as is. We then go back to MoviePreview and wrap the
Card inside a GestureDetector to detect the tap action. In case the user clicks, we push
the MovieDetailScreen that we just created. I'm going to run the app, you'll see that
when I click on each entry the detail opens. For movies without images, the top bar
has the normal height of the action bars. With the API data and a basic
visual interface in place, my next step is to implement infinite scroll
functionality. To do this, I will install the infinite_scroll_pagination
package by running this command. I will then create a model class
called "movies_list_model.dart" to manage the state of the MoviesListScreen. This class will require the use of Logger to log
data and MoviesRepository to retrieve the movies. I will create a method that takes in the current
page as a parameter and returns a list of movies. If an error occurs, it will be logged and then
re-thrown. The method will return the list of movies for the current page, with a limit of 10
movies as that is the maximum allowed by the API. Next, in the MoviesListScreen, I will define
a variable of type PagingController to handle pagination. This controller will contain an
index and each element will be of type "Movie". I will initialize the model I just
created in the "initState()" method. Then, I will add a listener
to the PagingController that uses the method we just created to get the data. In the appendPage() method, we pass it the list of movies and indicate that the next index
is the current page plus one. Lastly, the "dispose()" method of the PagingController
needs to be called to release its resources. To display the movies, I will define the
body of the Scaffold in a new way using classes from the infinite_scroll_pagination
package. The PagedListView receives the PagingController we defined earlier, and within
the PagedChildBuilderDelegate, we can create the widget that we want for each movie. We will use
the MoviePreview class that we created earlier. Finally, we can delete the repository from here. Once we run the app, we will be able to scroll
through the movies, and we will see that as we scroll, more requests are automatically made
to the remote API to get more data to display. We will now create the database layer, which
will store the data obtained from the remote API to form a local cache. This will enable the
user to quickly access the last list of movies that was loaded, regardless of whether
they have an internet connection or not. Once this local cache is created, we
will develop a system to enable users to download new data from the
remote API, if it is available. As we create the database layer,
you will begin to see the benefits of the Repository Pattern that
we applied in the data layer. To begin, we will install "sqflite,"
the Flutter library that we will use to work with the database. Additionally,
we will need "path" and "path_provider." The first step is to create the database entity.
We'll start by creating the "database" directory inside the "data" folder, and then create
a new folder called "entity" within it. Inside the "entity" folder, we'll create
a new file called "movie_db_entity.dart." The goal now is to define the entity that
will represent each entry in the database. To do this, we'll use "json_serializable," just
like we did with the network layer entities. The first thing we need to do is define the names
of the fields. You'll soon see why this is useful. Note that we're creating two fields for
identifiers: the first will be a unique integer managed by the app, and the second
is the movie ID that comes from the remote API. We're doing this because we need to apply
an order to the entries in the database. So, we're going to create an auto-incremental
integer as a primary key that will act as a unique identifier and help us to order the data. Next, we'll create a variable for each
field. Using the "@JsonKey()" annotation, we can give it whatever name we want. For
the release date, we'll use an int, which is simply the timestamp of the release date, as we
cannot store dates as such in SQLite databases. We'll then run the model build
command so that the "fromJson()" and "toJson()" methods are autogenerated.
In this case, they are called this way, even though they won't actually
convert to JSON, but to a Dart map. We need to create the database mapping class,
similar to what we did with the network layer. To do this, navigate to the database directory
and create a file named "database_mapper.dart". The process we'll use is identical to the
NetworkMapper, but in this case, the mapping class will be responsible for converting the
database entities to domain models and vice versa. Next, we need to create a data
source that connects to the database. These objects are commonly referred
to as DAOs, or Data Access Objects. Create a directory named "dao" inside the database
directory. We'll start by creating a base DAO, from which all other DAOs will inherit. The base
DAO will handle obtaining access to the database, as well as managing its creation and updating. Define "BaseDao" as an abstract class to prevent
it from being instantiated. Define a variable that contains the database version (currently
set to 1) and the name of the database. I always name a database using the app
identifier and its name. Define the name of the movies table. Then, create a protected
method that returns the database instance. If the instance does not exist, it will create it. Create a private method that will
handle creating the database instance. You can see how to obtain this
instance in the SQFlite documentation. For now, leave the onCreate() method
empty. The operator you see on the screen is used to return the object if it
is not null, or call the method if it is. We now need to create a method
that will create the movies table. This method takes an object of type Batch,
which we will use to execute queries more efficiently in a sequence. We define the
table using the variables we have created for the table name and field names, instead of
hardcoded Strings to reduce the risk of typos. We can now fill in the onCreate()
method by getting a batch from the database and running the create query. This method will be called the first time an attempt
is made to interact with the database. Next, we will create the DAO for movies, which
extends from the BaseDao we just created. We will first create a method to get all movies, taking
in a limit and an offset to narrow the search. We obtain a reference to the database using
the method we created in BaseDao and perform the query, sorting the results using
the auto-incrementing ID field we added. We then return a list generated from each input. We will also need a method to
insert data obtained from the remote API. Create an insertAll()
method with the following content. To insert all the data, we use a transaction, which we can implement as follows. This way of
working with the database is more efficient. The last step is to apply the
magic to the MoviesRepository. Add to it the DAO and the
mapping class we just created. Well, at this point we are going
to apply the following logic, first we try to get data from the database,
if there are elements then we return them. In this way, as I said before, we save having to
make an API call every time we want to get data. In case the database does not contain data for
the search criteria, then we make the API call as we already had before, and right after we save
the results in the database for future calls. Note that the insertion in the database
is done without the await keyword. The reason is because I don't need
to wait for the insert to be done, I execute it and continue with the next
step, which is to return the movies. Now open main.dart and add this in the main()
method. This will be used to spit out in the log the operations that are done in the database,
but only when the app is running in debug mode. Also add the classes we have
created to the dependency tree. To show you how this cache works, I'm
going to place two debug breakpoints, one when returning data from the database and
another when returning data from the remote API. I run the app, as you can see,
the API data is returned first. Right after another request is made
and also returned from the API. I scroll down and as I scroll down it keeps
making requests to the API and returning them. If everything has gone well, all these
requests have been saved in the database, when the app is reloaded these already downloaded
data must be returned from the database. I close the app and launch it again. And as
you see, the data comes from the database. As I scroll, it continues to load from
the database. If I keep scrolling down, there will come a point where I no longer have
data in the database, because I haven't scrolled down that much before, and then the data continues
to be loaded, but this time from the remote API. As you can see, by using the Repository Pattern we
have been able to articulate an intelligent data layer that pulls data from two different data
sources, the remote API and the local database; but making this process completely transparent
to the domain layer and the presentation layer. In this way, changes in this data logic will
not affect other layers of the application, thus reducing its maintenance cost
and reducing the possibility of bugs. The final task for this tutorial is to
create a system that synchronizes the database. Imagine a situation where you have
saved all the data in your local database, but new data is added to the remote API,
which you cannot see because your app is loading data from the database. Therefore,
we need to create a way to detect this and notify the user to refresh the app to
get the new data from the remote API. There is no specific method to do this, as
it depends on the API's functionality and your specific use case. For this example, we
will take a simple approach. We will compare the first record from the database with the
first record from the remote API, and if they are not the same, we will display a message
to the user indicating new data is available. To accomplish this task, open the
movies_dao.dart file and create a new method to remove all data from the database. Then create a delete method in the MoviesRepository that calls
the newly created method. Next, open the MoviesListModel
and create the delete method, which should be executed inside a
try-catch block in case of any errors. Now open the MoviesListScreen and create a
method that will refresh the data. First, remove all content from the database,
and then call the refresh() method of the PagingController to refresh the list. Finally,
call this method in a RefreshIndicator widget, which will wrap the PagedListView. I will
set breakpoints to demonstrate the result. When we open the app, the data is retrieved
from the database where it was saved. When we pull down the list, the RefreshIndicator
effect is triggered, and the movies are reloaded from the remote API. This happens because we have
deleted all the cached content in the database. Next, we go back to the MoviesRepository and create a
new method to detect changes in the remote API. We start by getting the first record from
the database. If the database is empty, we will get the last fresh data, and
there will be no need to notify the user. We then get the data from the remote API and
compare it with the first database record. If the data is the same, it means there
are no new updates in the remote API. If the data is different, it indicates that the
database is out of sync with the remote API, and we need to notify the user. Now, we will execute this method
in MoviesListScreen by creating a private method. We need to include
everything within the callback provided by WidgetsBinding. This is because we want to
execute this as soon as we open the screen, but the widget may not have all the
resources available yet. Using this callback, we can delay this operation until the
complete initialization of MoviesListScreen. To display a message to the user, we first need
to get a reference to ScaffoldMessenger. Go to MoviesListModel and create a new method that
calls the repository method we added. When we call this method, if it returns that there are
new data available, we need to inform the user. We can use a SnackBar for this purpose and add an action to make it easier for
the user to refresh the data. To execute this action when the screen is opened, we define a Future variable at the beginning
of the class and assign it the value of the method we just created. Then we wrap
RefreshIndicator inside a FutureBuilder. At this point I've made a mistake,
I'm calling the method of the model instead of the private method
of the class we just created. When we run the app, nothing happens if
there are no new updates in the remote API. To test this functionality, we can temporarily
change the page parameter in MoviesRepository, so that the first data obtained from the
API is different from the first database record. This will cause the SnackBar to
appear, informing us of the new data in the remote API. We can click on "Refresh" to
load the new data correctly. After testing, we should change the page parameter back to
1 to ensure that everything works correctly. To complete this tutorial, I'll perform
two actions that I always do after finishing a task. First, I'll execute "flutter
analyze" to check for any errors in the code. Once you run it, you'll notice several
errors in the autogenerated classes. Since these classes are autogenerated, I'll add a
rule to ignore them in the analysis_options.yaml file. To do this, you can specify the path
to ignore using a wildcard (*) to indicate that only the autogenerated files, which
have the ".g" naming, will be ignored. Then we have two more errors that
we can solve in the following way. Finally run "dart format ." to make sure
that the format of all Dart files is correct. That concludes this tutorial on how to
create a complete Flutter application using Clean Architecture. Please note that
the source code for the app we created and links to various libraries and tools used in this
tutorial can be found in the video description. If you haven't already, please leave a like
and leave me a comment with your impressions, doubts, suggestions or anything you would
like to tell me about this tutorial. I hope this video has been helpful, and that you've learned some useful
things. I'll see you in the next video. Goodbye.