Complete Movie App — Repositories & UseCases (3)

Prateek Sharma
Flutter Community
Published in
7 min readSep 27, 2020

You can also read this article here — https://www.techieblossom.dev/flutter/movieapp/repositories_usecase/

Hi, Glad that you’re here,

We are building a Movie App with the best coding practices and tools out there. In the previous Datasource article, I have explained how to create datasource and how to make API calls.

In this article, I will show how you can create repositories and usecases to separate the data layer with the presentation layer by introducing the domain layer, with the help of abstraction.

This is a companion article for the Video:

Problem

You have seen in the previous article, that we were calling data source from main.dart, which is your UI layer. In the long run, this is not right and certainly needs a layer in between. Also, what this does is, it tightly couples the UI layer with API responses, which can change over time and you’ll end up making changes to the data layer as well as the presentation layer.

Solution

We will create repositories in the domain layer and they will make decisions to fetch data either from the remote data source or the local data source. We will also create usecases, basically one usecase for one API or one feature. We’ll also create common error classes that will be returned from the repository layer and used by the presentation layer. I’ll show you how you can use dartz package to reduce some of the boilerplate code while dealing with common error messages and also clean up some logic written in the repository.

Well, a lot being said, let’s get into action.

Repository

Abstract Repository

Create an abstract class MovieRespository:

This class will have one method to return the — Future of List of Movies.

Implement The Repository

In the data/repositories folder, create a file movie_repository_impl.dart

Create a class MovieRepositoryImpl that extends MovieRepository

  1. You’ll need a remote data source to make the API calls that we discussed in the previous article. Additionally, you can have a local data source as well, that will fetch data from local DB.
  2. A constructor with the remote data source as its parameter.
  3. Since you’ve extended this class from the abstract class, you will implement the methods here. Notice, you can change the MovieEntity to MovieModel in the implementation, to separate the layers. Also, add async in the function definition.
  4. In the try/catch block, you’ll make an API call to handle any error that is thrown by the API.
  5. Fetch the trending movies and return them.

Call the Repository Method

Open main.dart and create an instance of MovieRepository and call the getTrending() method. You already have ApiClient and MovieRemoteDataSource.

Run the app. This will work as it worked before when we used the data source. But, doing so will not be scalable. Your UI will have to decide which repository to call to perform a certain action. And sometimes, it can be 2–3 calls to perform certain actions, so UI will have to decide to make those calls.

And, UI will always have too many widgets to deal with, so putting this logic in UI is not good. That’s why, we’ll have usecases to simplify code at the presentation layer.

UseCase

As I mentioned in the Pilot article, usecases are the features that the app will work on. Like, fetching popular movies, trending movies, movie details, etc. UseCases are simple classes that directly pass the input parameters to fetch details to the repository. UseCase will directly interact with the blocs.

In the domain/usecases, create a file get_trending.dart

Create a class GetTrending.

  1. This class will accept MovieRepository as the final variable
  2. Create a constructor, that will have MovieRepository.
  3. The call method is already present in all dart objects. So, creating a method with a call name, allows you to call this method just with the instance of the class. We'll see this in just a moment.
  4. You’ll call the getTrending() method from the repository here. This returns the list of movies.

Open main.dart and this time instead of calling getTrending() from the repository, instantiate GetTrending class with movieRepository as its parameter. Then, simply call the instance of GetTrending:

When you run, there is absolutely no difference in the output.

Error Handling - Problem

What will happen when instead of a proper list of movies, the API call has returned with an error. As you recall from the call in MovieRepositoryImpl, I returned null in case of an exception. There are two problems with this approach:

  1. To show on UI based on null, you’ll have to check null values wherever you call the usecase. This will be tedious when the app grows.
  2. Also, there will be only two possibilities of usecase to return, either List<MovieEntity> or null. With null, you can only show one generic message to the UI, so how you'll show different messages to UI based on the type of error from API.

Error Handling - Solution

Use the dartz plugin. This plugin is awesome and at first, it is hard to understand, but there is a very simple underlying concept.

Return Left when error, Right when success.

Left and Right are object/data holders.

Let’s add the dartz dependency in pubspec.yaml

Run pub get command to update the dependencies.

Now, update MovieRepository. Open movie_repository.dart. Change the dependency of the getTrending() method to return Either type, with left as AppError and right as List of MovieEntity.

AppError is a class that just holds an error message.

Create a class AppError and extend it with Equatable

  1. Declare field with the String type, that will hold any error message.
  2. Override props method to hold the messages, if required to compare at a later stage.

Let’s get back to repositories. We’ve updated the declaration of the getTrending() in the abstract class, so by this time implementation of MovieRepository has errors. Let's correct them by updating the signature.

  1. Update the method signature the same as MovieRepository's and again change MovieEntity to MovieModel to maintain the level of abstraction.
  2. When API has returned with success, wrap the response with Right, in this case, wrap movies in Right.
  3. In case of Exception, you can return AppError with wrapping with Left.

The last thing before we run this is, update the UseCase as well. You only have to change the signature of the GetTrending's call method.

Open main.dart and read the response of getTrending(). As you'll read the response of a Future returning method, you need to right await. Also, add async to the main function.

  1. Create a final variable to hold the response of GetTrending's call method.
  2. Use the fold operator to get either of the left or right value. Only, one will be returned at a time.
  3. When it’s an error, left will be called and print the message.
  4. When it’s a success, right will be called and prints the movies.
  5. Since dartz plugin also contains State class, you'll have an error using it within a file having a stateful widget. This is not a problem, because you'll call usecases from Blocs only.

More UseCases

As in our last part, we had created 3 more methods in the data source, let’s create usecases for all of them and methods to repositories as well.

Open movie_repository.dart and add 3 more methods for popular, playing now and coming soon movies:

Open the movie_repository_impl.dart and override the 3 methods:

The other 3 methods are a carbon copy of the getTrending(), the only change will be what method they will invoke from the data source. And, in this case, it's no brainer, because the names of the methods in data source and repository are the same.

You can change what message to return in case of an error in each of the methods.

Duplicate the GetTrending UseCase 3 times and name them GetPopular, GetPlayingNow, and GetComingSoon. In call(), call the respective methods from the repository:

For GetPopular call getPopular()

For GetPlayingNow call getPlayingNow()

For GetComingSoon call getComingSoon()

Caveat

Imagine you’re the sole developer in this project and you know everything about how to create UseCase and what does the call() method in usecase does. But, months later or years later, another couple of developers join you and they want to create UseCase. Will they remember to make the call() method in the usecase. Probably not. They could end up creating a method with a different name and start calling the method instead of in the Blocs. This will bring 2 different sets of ways for implementing the exact same thing. This is not good for code consistency in the long term.

UseCase Abstract Class

In the domain/usecases folder, create a file usecase.dart

Create an abstract class UseCase:

  1. UseCase class takes 2 generics — Type that says what will be the success response type and Params that says what are the parameters to make API calls.
  2. You can relate this signature, as it is the same as GetTrending's. Except that, now it is generic for any returned object and any type of parameters.

This type of code is very important in bigger projects for maintainability.

It is difficult to future proof code with assumptions, so it might be hard for you to completely understand the things right now. Let me explain with the help of examples.

Till now we’ve created 4 usecases all returning List<MovieEntity>. In the future, you'll return MovieDetail, List<CastEntity>, etc. So, specifying in the UseCase definition itself, what it will return will be good.

Till now we’ve called APIs without any extra parameter other than TMDb API Key. In the future, you’ll search movies by query text and call movie detail API with Movie ID, then you’ll require the Params as well.

For calling APIs with Params, you’ll create separate classes to hold Params for APIs that require query parameters. For those API’s that don’t require query parameters, we’ll create NoParams class.

NoParams

Create a class NoParams extending Equatable:

Update UseCases

Extend all the usecases with UseCase class

  1. Define the Type that GetTrending will return and Parameters that it takes(if any)
  2. Add @override annotation as now it is declared in parent UseCase class.
  3. call() now takes in NoParams, as explained before, this will change based on the type of API call being made.

Repeat these steps for the other 3 usecases. Absolutely, no brainer in that.

You can try running after making these changes, absolutely nothing will change as far as output is concerned. But, you need to now change the getTrending() in main.dart to getTrending(NoParams()):

This was all about creating repository and usecases. See you in the next part of this series.

If you loved reading the article, don’t forget to clap 👏. You can reach out to me and follow me on Medium, Twitter, GitHub, YouTube.

https://www.twitter.com/FlutterComm

--

--