Implementing Flutter Base - Part 3: Details - Data Layer

Firebase

I. Tasks

The main task of this layer is to manage and interact with data sources (remote and local).

II. The tasks that need to be handled.

Going from bottom to top, these are the tasks that need to be addressed at this layer:

  1. Implementation on various environments: Dev -> SIT -> UAT -> Prod
  2. Implementing the base network for calling APIs
  3. Creating models using auto-generated code
  4. Implementing repositories

III. Detailed processing

Let's start handling each task one by one:

1. Implementing various environments, going from DEV -> SIT -> UAT -> PROD.

The applications we deploy all go through development, testing by testers, and then the Go-live phase, which means we will have different environments accordingly. Therefore, the flexibility to switch between these environments is necessary, and I've established this in the project's base.

On pub.dev, there are many libraries that can help with this. I chose the flutter_dotenv library based on its number of likes, and it's very easy to use.

Declaring environments: I create an assets folder in the root project and then create an env folder to manage the different environments. It will look like this:

In the .env file, it only contains one line: ENV=prod. The value of ENV is set based on the environment you want to build: dev, sit, uat, prod.

In the .env.dev, .env.ida, and .env.prod files, what do they contain?

These are the domain endpoints that your application needs to connect to the backend. Here, I have API_AUTH, and if you have additional backends to connect to, you can add corresponding domain endpoints here.

Fetching data: After declaring the environments, it's time to fetch data from the respective environments. I create a "Env" class located in the "utils" directory to handle this initialization in the main file and access the respective domain endpoints as follows:

And calling await Env.init() in the main.dart file completes the setup.

2. Implementing the base network to make API calls and interact with the backend

To complete the base network, we need to:

Initially, I used Dart's http library, but later switched to Dio for its convenience in transferring binary data. Separating the classes thoroughly makes switching libraries very simple, with minimal changes due to differences between the two libraries, all without affecting other layers or classes.

The most important aspect here is the implementation of BaseService and handling all exceptions, ensuring data is returned to the layers above. Here's a basic outline of this part:

So, looking at the diagram, I'll create a class called BaseService, which will contain the basic methods get, put, post, delete, and return data as described in the ServiceResponse class.

The data returned is in the form of a ServiceResponse, which includes: Success/Failure status, data in case of success, and an error message if there is a failure.


In which, it contains BaseException, which are the exceptions I defined when the API calls fail, including BadRequestException, ForbiddenException, FetchDataException, ApiNotRespondingException, UnAuthorizedException, ... and you can add many other exception cases you want to check as well.

The task of describing the ServiceResponse object returned by BaseService is completed. Now we proceed to finalize the GET, POST, PUT, DELETE methods, and handle all possible error cases that may occur when calling the API.

I wonder if you have any questions about what is contained in the _handleErrorDio method? Or have you already imagined that it's the place where we handle all the other exceptions? For example, if the API call times out, we return an ApiNotRespondingException error, or if there's no internet connection, we return a FetchDataException error, and so on.

Now that we've implemented BaseService, in the datasource_remote folder, you'll have corresponding services that simply call the API and return the response to the repositories. For example, in the login flow, I have AuthService that contains two APIs: login and registerFcm:

3. Implementing models using auto-generated code.

Dart is an object-oriented language, so we need models to describe the request and response objects. To make the data casting process more automatic, I used freezed to generate corresponding code for models. My request and response classes are structured as follows:

4. Implementing repositories.

This article is already quite lengthy, so I'll leave it here and handle those topics along with the repository interface in the next article. In that section, I will explain how to automatically cast data to the desired objects within the repositories.