Complete Movie App — Local Database (14)

Prateek Sharma
Flutter Community
Published in
19 min readMar 29, 2021

--

Welcome back, Glad that you are here.

We’re building a Movie App with the best coding practices and tools. In the last video we worked on searching movie and with that we have covered all features that fetch movies from TMDb API.

This is a companion article for the Video:

In this video, we will work on 2 things related to local database. Mark movies as favorite and show them to user from navigation drawer and save the language selected by user so that when user reopens the app, the previously marked language is loaded instead of default English. For all of this, we will use Hive plugin.

The features that we will implement using Hive are:

  1. Insert favorite movie in db.
  2. Retrieve list of favorite movies from db.
  3. Check whether a particular movie is favorite or not.
  4. Unfavorite the Movie.
  5. Save User Preferred Language
  6. Retrieve Preferred Language

Understand Hive in Clean Architecture

Before we go anywhere in the code, you need to understand where does DB calls fall when following Clean Architecture. In simple one liner I can state that accessing DB is no different than accessing any API, both are considered outside world calls. Our DB CRUD operations will be written in data layer. You’ll create new data source first, i.e. MovieLocalDataSource and LanguageLocalDataSource, former deals with movie database calls and latter deals with language database.

Repositories in Data Layer have same responsibility to fetch data from API or local database. In our case, we will have favorite movie CRUD methods in the existing MovieRepositoryImpl class because those are related to movies. For this, you'll add instance of MovieLocalDataSource in the MovieRepositoryImpl so that you can call CRUD methods from the repository.

Logically, for language related methods in repositories, you’ll create a new AppRepositoryImpl class that will hold app related methods.

Coming to Tables, because we will store movie related data in hive, we will need to create MovieTable that should be Hive Object. Again as MovieModel, this will extend MovieEntity to maintain level of abstraction.

These are the only things that I wanted to clear before moving to actual implementation. So, Let’s start coding now.

Setup Hive

First, add dependencies for hive and path_provider to connect with hive db and get the application directory path respectively:

hive: ^1.4.1+1
path_provider: ^1.6.11

Second, initialize the Hive by giving application documents directory path. Since, we use await to get the path from path_provider, add async also to the main method.

final appDocumentDir = await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDir.path);

Hive DB Schema

We need to now define the structure in which the data should be stored. As I have previously mentioned, table also extends entity and some fields can be omitted as we don’t want to fill user’s phone memory with too much unimportant data that we don’t show in UI. This is the UI that we show in the favorite movies list, accordingly we will create the table class.

//1
part 'movie_table.g.dart';

//2
@HiveType(typeId: 0)
//3
class MovieTable extends MovieEntity {
//4
@HiveField(0)
final int id;
@HiveField(1)
final String title;
@HiveField(2)
final String posterPath;

MovieTable({
this.id,
this.title,
this.posterPath,
}) : super(
id: id,
title: title,
posterPath: posterPath,
);

}
  1. Give the generated file path here because hive will generate this file when we run the build runner command.
  2. Declare a class as HiveType and give the typeId as 0. Do not assign same typeId to other class. As per the doc, unique typeId is used to find the correct adapter when a value is brought back from disk.
  3. Extend this class by MovieEntity.
  4. Declare all fields with HiveField annotation. The number should be unique per class and should not be changed once used in the app.

Update the dev_dependencies in pubspec.yaml:

hive_generator: 0.7.2
build_runner: ^1.10.0

Now run the build_runner command:

flutter packages pub run build_runner build

You will see mobile_table.g.dart file at the same location.

Local DB operations (Data Layer)

Our usecases are saving favorite movie, fetching all the favorite movies, Delete Movie from favorite movies and check if movie is favorite or not. So, let’s create MovieLocalDataSource:

//1
abstract class MovieLocalDataSource {
//2
Future<void> saveMovie(MovieTable movieTable);
//3
Future<List<MovieTable>> getMovies();
//4
Future<void> deleteMovie(int movieId);
//5
Future<bool> checkIfMovieFavorite(int movieId);
}
  1. Create abstract class, we will implement this class similar to MovieRemoteDataSource.
  2. The saveMovie() will take in the table and save it in hive.
  3. The getMovies() will return us the all the favorite movies.
  4. To unfavorite movie we will create deleteMovie(). This will delete the movie from favorite table.
  5. The checkIfMovieFavorite() will help us in toggling the favorite icon in movie detail screen.

Implement the methods

//1
class MovieLocalDataSourceImpl extends MovieLocalDataSource {
@override
Future<bool> checkIfMovieFavorite(int movieId) async {
throw UnimplementedError();
}

@override
Future<void> deleteMovie(int movieId) async {
throw UnimplementedError();
}

@override
Future<List<MovieTable>> getMovies() async {
throw UnimplementedError();
}

@override
Future<void> saveMovie(MovieTable movieTable) async {
throw UnimplementedError();
}

}
  1. Implement all the methods and add async keyword.

Save Movie

First operation that we will apply on the database is save the movie where you will insert this movie in the database. If a movie is present in database, it will be considered as favorite.

@override
Future<void> saveMovie(MovieTable movieTable) async {
//1
final movieBox = await Hive.openBox('movieBox');
//2
await movieBox.put(movieTable.id, movieTable);
}
  1. First, you will open the box. movieBox is identifier of the box where you will keep the favorite movies.
  2. Once this box is opened, you’ll put the movie as value and movie id as the key.

Get the Movies

After saving the movie, we will fetch all the movies from movieBox and display them to the user.

@override
Future<List<MovieTable>> getMovies() async {
//1
final movieBox = await Hive.openBox('movieBox');
//2
final movieIds = movieBox.keys;
//3
List<MovieTable> movies = [];
//4
movieIds.forEach((movieId) {
movies.add(movieBox.get(movieId));
});
//5
return movies;
}
  1. Again, open the box. If the box is already opened, it will returned the opened box.
  2. Get all the keys of the movies in the movie box.
  3. Create an empty array of movies where you will insert the movie one by one.
  4. Loop over every key and get every movie from movieBox and add the movie in the list.
  5. Finally, return the movies. We will show these movies in the favorite movies list.

Delete Movie

We will mark movie unfavorite, so let’s implement the delete method functionality as well.

//1
final movieBox = await Hive.openBox('movieBox');
//2
await movieBox.delete(movieId);
  1. Open the box.
  2. Delete the movie in the movieBox by movieId.

Check If Movie Is Favorite

In Movie Detail Screen, we need to show whether the movie is favorite or not, so let’s add body to this method as well.

final movieBox = await Hive.openBox('movieBox');
//1
return movieBox.containsKey(movieId);
  1. After opening the box, check whether movieId exists or not. If not, that means movie is not marked as favorite. If present, that means the movie is marked as favorite.

Repository (Domain Layer)

To perform all these operations, let’s create the repository now. Open MovieRepository and add the 4 methods:

//1
final MovieLocalDataSource localDataSource;

//2
MovieRepositoryImpl(this.remoteDataSource, this.localDataSource);

@override
Future<Either<AppError, void>> saveMovie(MovieEntity movieEntity) async {
try {
//3
final response = await localDataSource.saveMovie(MovieTable.fromMovieEntity(movieEntity);
//4
return Right(response);
} on Exception {
//5
return Left(AppError(AppErrorType.database));
}
}
  1. Before adding the methods, we need to have reference of MovieLocalDataSource so that we can call the data source methods that we created before.
  2. Along with MovieRemoteDataSource, repository now depends on MovieLocalDataSource.
  3. In the saveMovie(), under the try block, save the movie. We cannot directly convert movie entity to movie table, so I will show the solution in a moment.
  4. On success, we will return the Right object.
  5. On any exception, we will return the Left object. The error would be of new type i.e. database. So add this to the AppErrorType enum.
enum AppErrorType { api, network, database }

To convert movie entity to movie table, let’s create a method fromMovieEntity(movieEntity) in MovieTable class.

//1
factory MovieTable.fromMovieEntity(MovieEntity movieEntity) {
//2
return MovieTable(
id: movieEntity.id,
title: movieEntity.title,
posterPath: movieEntity.posterPath,
);
}
  1. Create the factory method that accepts MovieEntity.
  2. Return the MovieTable instance with all the 3 fields.

After this method, let’s work on the other methods too, that should be quick:

@override
Future<Either<AppError, List<MovieEntity>>> getFavoriteMovies() async {
try {
//1
final response = await localDataSource.getMovies();
return Right(response);
} on Exception {
return Left(AppError(AppErrorType.database));
}
}

@override
Future<Either<AppError, void>> deleteFavoriteMovie(int movieId) async {
try {
//2
final response = await localDataSource.deleteMovie(movieId);
return Right(response);
} on Exception {
return Left(AppError(AppErrorType.database));
}
}

@override
Future<Either<AppError, bool>> checkIfMovieFavorite(int movieId) async {
try {
//3
final response = await localDataSource.checkIfMovieFavorite(movieId);
return Right(response);
} on Exception {
return Left(AppError(AppErrorType.database));
}
}
  1. Call the getMovies() in the getFavoriteMovies().
  2. Call the deleteMovie() in the deleteFavoriteMovie().
  3. Call the checkIfMovieFavorite in the checkIfMovieFavorite().

Modify GetIt

When we have made changes to the constructor of MovieRepository, we have to update the get_it declaration:

//1
getItInstance
.registerLazySingleton<MovieRepository>(() => MovieRepositoryImpl(
getItInstance(),
getItInstance(),
));

//2
getItInstance.registerLazySingleton<MovieLocalDataSource>(
() => MovieLocalDataSourceImpl());
  1. Add instance of MovieLocalDataSource as the second parameter of MovieRepository.
  2. You will also register MovieLocalDataSource similar to MovieRemoteDataSource.

Usecases

To access these repository methods from UI or Blocs, we will create usecases. Let’s create 4 usecases. First, saveMovie usecase:

Save Movie Usecase

Create a file save_movie.dart in the usecase folder:

//1
class SaveMovie extends UseCase<void, MovieEntity> {
final MovieRepository movieRepository;

SaveMovie(this.movieRepository);

@override
Future<Either<AppError, void>> call(MovieEntity params) async {
return await movieRepository.saveMovie(params);
}
}
  1. As explained in previous instances, when I showed creating usecase, you need to remember 2 things, input and output. Here, we need movie while saving and nothing as output.

Similarly, add other usecases. I don’t think any explanation is required there. In case of any doubts, please join in https://discord.gg/Q5GfbZsvPk Techie Blossom Discord Server and post your queries there.

Get Favorite Movies Usecase

Create another usecase in get_favorite_movies.dart file:

//1
class GetFavoriteMovies extends UseCase<List<MovieEntity>, NoParams> {
final MovieRepository movieRepository;

GetFavoriteMovies(this.movieRepository);

@override
Future<Either<AppError, List<MovieEntity>>> call(NoParams noParams) async {
return await movieRepository.getFavoriteMovies();
}
}
  1. Here, inputs are NoParams because we are only fetching all the favorite movies. List<MovieEntity> becomes the output here.

Delete Favorite Movie Usecase

Create third usecase in delete_favorite_movie.dart file:

//1
class DeleteFavoriteMovie extends UseCase<void, MovieParams> {
final MovieRepository movieRepository;

DeleteFavoriteMovie(this.movieRepository);

@override
Future<Either<AppError, void>> call(MovieParams movieParams) async {
return await movieRepository.deleteFavoriteMovie(movieParams.id);
}
}
  1. To delete favorite movie, we can delete by movie id because we have stored by movie id. Again the output of that will be void.

Check if Favorite Movie Usecase

Create last usecase in check_if_movie_favorite.dart file:

//1
class CheckIfFavoriteMovie extends UseCase<bool, MovieParams> {
final MovieRepository movieRepository;

CheckIfFavoriteMovie(this.movieRepository);

@override
Future<Either<AppError, bool>> call(MovieParams movieParams) async {
return await movieRepository.checkIfMovieFavorite(movieParams.id);
}
}
  1. Here input will be MovieParams because we want to check whether movie is in database or not by movie id. Output will be true or false, so keep the type as bool.

When all the usecases are in place, let’s put them in GetIt, so that we can move to Blocs now.

getItInstance
.registerLazySingleton<SaveMovie>(() => SaveMovie(getItInstance()));

getItInstance.registerLazySingleton<GetFavoriteMovies>(
() => GetFavoriteMovies(getItInstance()));

getItInstance.registerLazySingleton<DeleteFavoriteMovie>(
() => DeleteFavoriteMovie(getItInstance()));

getItInstance.registerLazySingleton<CheckIfFavoriteMovie>(
() => CheckIfFavoriteMovie(getItInstance()));

All usecases have MovieRepository in constructor, so use getItInstance() which will help in resolving instance of MovieRepository.

Favorite Bloc

After we are done with the data and domain layers, it’s time to move to presentation layer. If you have followed my previous videos/articles in this series, you know that presentation layer doesn’t contain only widgets, it also contains Blocs.

Events

Create a new bloc in bloc folder and in favorite_event.dart, add the events.

//1
class LoadFavoriteMovieEvent extends FavoriteEvent {
@override
List<Object> get props => [];
}

//2
class DeleteFavoriteMovieEvent extends FavoriteEvent {
final int movieId;

DeleteFavoriteMovieEvent(this.movieId);

@override
List<Object> get props => [movieId];
}

//3
class ToggleFavoriteMovieEvent extends FavoriteEvent {
final MovieEntity movieEntity;
final bool isFavorite;

ToggleFavoriteMovieEvent(this.movieEntity, this.isFavorite);

@override
List<Object> get props => [movieEntity, isFavorite];
}

//4
class CheckIfFavoriteMovieEvent extends FavoriteEvent {
final int movieId;

CheckIfFavoriteMovieEvent(this.movieId);

@override
List<Object> get props => [movieId];
}
  1. We will show the favorite movies list, so create LoadFavoriteMovieEvent.
  2. On press of delete icon on favorite movie card, we will call this event. This will use movieId to delete the movie from db.
  3. Now, we will create an event that will be used in movie detail screen when user pressed the filled or empty favorite icon. It will work like toggle, for example, if movie is favorite and user presses the icon, it will unfavorite the movie and the reverse happens when movie is not favorite. This event will take in MovieEntity and a flag of whether movie is favorite or not.
  4. Last event will be CheckIfFavoriteMovieEvent which will take in movieId and check if movie is favorite or not. This will be called when in movie detail screen, we have to either show filled favorite icon or empty favorite icon.

By the 4 events, you should have understood that events are not created by keeping usecases in mind, but by keeping the user events or actions in mind.

States

It doesn’t matter that one event has one state mapped. We can have lesser number of states as well. Create the states in favorite_state.dart keeping the UI in mind at various stages.

//1
class FavoriteMoviesLoaded extends FavoriteState {
final List<MovieEntity> movies;

FavoriteMoviesLoaded(this.movies);

@override
List<Object> get props => [movies];
}

//2
class FavoriteMoviesError extends FavoriteState {}

//3
class IsFavoriteMovie extends FavoriteState {
final bool isMovieFavorite;

IsFavoriteMovie(this.isMovieFavorite);

@override
List<Object> get props => [isMovieFavorite];
}
  1. When you want to show all the favorite movies, you will yield the FavoriteMoviesLoaded with list of movies.
  2. When there is an error in any of the operation that we perform, we will yield FavoriteMoviesError.
  3. When we are done with checking whether the movie is favorite or not, we will return IsFavoriteMovie state with a boolean flag, which will tell us if movie is favorite or not. Based on this, we can show the icon as filled or empty.

Logic

Now we will write the exact logic that bloc should handle instead of widgets. We know that we will call usecases here so declare all the usecases as the required fields.

class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
//1
final SaveMovie saveMovie;
final GetFavoriteMovies getFavoriteMovies;
final DeleteFavoriteMovie deleteFavoriteMovie;
final CheckIfFavoriteMovie checkIfFavoriteMovie;

FavoriteBloc({
@required this.saveMovie,
@required this.getFavoriteMovies,
@required this.deleteFavoriteMovie,
@required this.checkIfFavoriteMovie,
}) : super(FavoriteInitial());

@override
Stream<FavoriteState> mapEventToState(
FavoriteEvent event,
) async* {
//Handle events here...
}
}
  1. Declare all the usecases in the constructor and mark them @required.

Now, handle the events in mapEventToState():

@override
Stream<FavoriteState> mapEventToState(
FavoriteEvent event,
) async* {
//1
if (event is ToggleFavoriteMovieEvent) {
//2
if (event.isFavorite) {
await deleteFavoriteMovie(MovieParams(event.movieEntity.id));
} else {
await saveMovie(event.movieEntity);
}
//3
final response =
await checkIfFavoriteMovie(MovieParams(event.movieEntity.id));
yield response.fold(
(l) => FavoriteMoviesError(),
(r) => IsFavoriteMovie(r),
);
}
//4
else if (event is LoadFavoriteMovieEvent) {
yield* _fetchLoadFavoriteMovies();
}
//5
else if (event is DeleteFavoriteMovieEvent) {
await deleteFavoriteMovie(MovieParams(event.movieId));
yield* _fetchLoadFavoriteMovies();
}
//6
else if (event is CheckIfFavoriteMovieEvent) {
final response = await checkIfFavoriteMovie(MovieParams(event.movieId));
yield response.fold(
(l) => FavoriteMoviesError(),
(r) => IsFavoriteMovie(r),
);
}
}

//4
Stream<FavoriteState> _fetchLoadFavoriteMovies() async* {
final Either<AppError, List<MovieEntity>> response =
await getFavoriteMovies(NoParams());

yield response.fold(
(l) => FavoriteMoviesError(),
(r) => FavoriteMoviesLoaded(r),
);
}
  1. Handle the ToggleFavoriteMovieEvent.
  2. Check if the movie is favorite or not. If movie is favorite, remove it from the favorite list in the db.
  3. If the movie is not favorite, mark it as favorite. Using fold operator, yield the error state for left and success state for right.
  4. We will create a method to handle when the event is LoadFavoriteMovieEvent. Here, you can call getFavoriteMovies usecase and handle the response using fold operator.
  5. If the event is DeleteFavoriteMovieEvent, delete the movie from db.
  6. If event is CheckIfFavoriteMovieEvent, call the checkIfFavoriteMovie usecase and yield the state using fold operator.

Register the Bloc

In GetIt, declare the bloc initialization:

//1
getItInstance.registerFactory(() => FavoriteBloc(
saveMovie: getItInstance(),
checkIfFavoriteMovie: getItInstance(),
deleteFavoriteMovie: getItInstance(),
getFavoriteMovies: getItInstance(),
));
  1. Declare the FavoriteBloc as factory and give the four required usecases.

UI

Let’s start with saving the movie. In movie detail screen, we need to show filled or empty favorite icon, so for that we need FavoriteBloc in MovieDetailBloc. Let's do that:

Update the toggle icon

Update MovieDetailBloc:

//1
final FavoriteBloc favoriteBloc;

MovieDetailBloc({
@required this.getMovieDetail,
@required this.castBloc,
@required this.videosBloc,
@required this.favoriteBloc,
}) : super(MovieDetailInitial());
  1. Declare the bloc as final field and make it required.

Next, open get_it.dart and update the dependency of MovieDetailBloc

Last, when the movie details are loaded, emit CheckIfFavoriteMovieEvent.

favoriteBloc.add(CheckIfFavoriteMovieEvent(event.movieId));

To update the UI for favorite bloc state, let’s declare the favoriteBloc in MovieDetailScreen:

//1
FavoriteBloc _favoriteBloc;

//2
_favoriteBloc = _movieDetailBloc.favoriteBloc;

//3
_favoriteBloc?.close();

//4
BlocProvider.value(value: _favoriteBloc),
  1. Declare the bloc.
  2. Get the instance in initState() from movieDetailBloc.
  3. In dispose(), close the _favoriteBloc.
  4. In the MultiBlocProvider, add the _favoriteBloc.

Open MovieDetailAppBar and listen for the states of FavoriteBloc.

//1
BlocBuilder<FavoriteBloc, FavoriteState>(
builder: (context, state) {
//2
if (state is IsFavoriteMovie) {
return Icon(
state.isMovieFavorite ? Icons.favorite : Icons.favorite_border,
color: Colors.white,
size: Sizes.dimen_12.h,
);
}
//3
else {
return Icon(
Icons.favorite_border,
color: Colors.white,
size: Sizes.dimen_12.h,
);
}
},
),
  1. Wrap the favorite_border icon with BlocBuilder.
  2. When state is IsFavoriteMovie, you can get the boolean value and decide which icon to show.
  3. Since, builder has to return with one widget, put the default icon i.e. border icon.

Now, wrap the Icon with GestureDetector to fire an event to save the movie when we tap the favorite icon.

//1
GestureDetector(
onTap: () {
//2
_favoriteBloc.add(
ToggleFavoriteMovieEvent(
//4
MovieEntity.fromMovieDetailEntity(
movieDetail,
),
//6
state.isMovieFavorite,
),
);
}
),

//3
final MovieDetailEntity movieDetailEntity;

const MovieDetailAppBar({
Key key,
@required this.movieDetailEntity,
}) : super(key: key);

//5
factory MovieEntity.fromMovieDetailEntity(
MovieDetailEntity movieDetailEntity) {
return MovieEntity(
posterPath: movieDetailEntity.posterPath,
id: movieDetailEntity.id,
backdropPath: movieDetailEntity.backdropPath,
title: movieDetailEntity.title,
voteAverage: movieDetailEntity.voteAverage,
releaseDate: movieDetailEntity.releaseDate,
);
}
  1. Use GestureDetector to handle tap events.
  2. In the onTap(), dispatch ToggleFavoriteMovieEvent.
  3. To save movie in local db, we need movie entity so pass it from BigPoster and make MovieDetailAppBar able to use the movie detail entity. We will later convert movie detail entity to movie entity.
  4. This event needs instance of MovieEntity and a true or false value stating whether the movie is favorite or not.
  5. Create a factory method that converts the movie detail entity to movie entity. Use this as the first parameter for the ToggleFavoriteMovieEvent.
  6. The second parameter of the event will be taken from the state.

Run the app now. Check the debug console and there is an error. This error is because we forgot to add the super in MovieTable. Let's add that and re-run the app:

MovieTable({
this.id,
this.title,
this.posterPath,
})
//1
: super(
id: id,
title: title,
posterPath: posterPath,
backdropPath: '',
releaseDate: '',
voteAverage: 0,
);
  1. 3 fields are important so, assign values to them. Rest all can be empty or default values. These will not be saved in local db. Only id, title and posterPath will be saved.

Finally, run the app and check what we have done till now in this video. You can mark and unmark the movie as favorite.

Let’s work on listing down all the favorite movies now.

Favorite Movies List

If you want to have a screen where you can see all the favorite movies, then you can press on the Favorite Movies menu item in navigation drawer. Let’s create a screen for the same and link it with navigation drawer.

Open

NavigationListItem(
title: TranslationConstants.favoriteMovies.t(context),
//1
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FavoriteScreen(),
),
);
},
)
  1. Open a new screen, FavoriteScreen.

Now, create a new folder favorite in journeys because this is new journey. Create a new widget in this folder — favorite_screen.dart:

class FavoriteScreen extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Container();
}
}

We will need favorite bloc here so make this one Stateful widget.

//1
FavoriteBloc _favoriteBloc;

_favoriteBloc = getItInstance<FavoriteBloc>();

//2
_favoriteBloc?.close();

//3
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
TranslationConstants.favoriteMovies.t(context),
),
),
);
}
  1. Declare the variable in state class for FavoriteBloc and initialise it in initState().
  2. Close the bloc in dispose().
  3. In the build(), use Scaffold with AppBar and same title as that in the navigation drawer tile.

Run the app and now you’ll see title in the screen.

Favorite Movie Grid and Movie Card

In favorite screen, we will use BlocProvider and BlocBuilder to display different stages of UI, like no favorite movies and grid of favorite movies. We also need to fire an LoadFavoriteMovieEvent in initState()

//5
_favoriteBloc.add(LoadFavoriteMovieEvent());

//1
body: BlocProvider.value(
value: _favoriteBloc,
//2
child: BlocBuilder<FavoriteBloc, FavoriteState>(
builder: (context, state) {
//3
if (state is FavoriteMoviesLoaded) {
if (state.movies.isEmpty) {
return Center(
child: Text(
TranslationConstants.noFavoriteMovie.t(context),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
),
);
}
return const SizedBox.shrink();
}
return const SizedBox.shrink();
},
),
)
  1. Since, the instance of FavoriteBloc is not being provided above, we need to provide it in this screen itself.
  2. Use the BlocBuilder to read the states.
  3. If and only if the state is FavoriteMoviesLoaded, we will display some thing otherwise, we will return empty sized box. In this state, we will have 2 outputs, empty list or filled list. If we have an empty list, we will show a text in center. We will return empty sized box when movies list is not empty for now.

Create a new widget FavoriteMovieGridWidget and call it when list of favorite movies is not empty.

class FavoriteMovieGridView extends StatelessWidget {
//1
final List<MovieEntity> movies;

const FavoriteMovieGridView({
Key key,
@required this.movies,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container();
}
}
  1. This widget will accept list of movies as required field.

Build the UI in the build():

@override
Widget build(BuildContext context) {
//1
return Padding(
padding: EdgeInsets.symmetric(horizontal: Sizes.dimen_8.w),
//2
child: GridView.builder(
shrinkWrap: true,
itemCount: movies.length,
//3
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: Sizes.dimen_16.w,
),
//4
itemBuilder: (context, index) {
return FavoriteMovieCardWidget(
movie: movies[index],
);
},
),
);
}
  1. We need horizontal spacing for the grid view, so use Padding.
  2. Use builder of GridView to build grid for favorite movies.
  3. In the gridDelegate, use the fixed cross axis count parameter with 2 items in one row with sufficient spacing in between.
  4. Lastly, return FavoriteMovieCardWidget for each item.

Now, create a new file having FavoriteMovieCardWidget widget:

class FavoriteMovieCardWidget extends StatelessWidget {
//1
final MovieEntity movie;

const FavoriteMovieCardWidget({
Key key,
@required this.movie,
}) : super(key: key);

@override
Widget build(BuildContext context) {
//2
return Container(
margin: EdgeInsets.only(bottom: Sizes.dimen_8.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(Sizes.dimen_8.w),
),
//7
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MovieDetailScreen(
movieDetailArguments: MovieDetailArguments(movie.id),
),
),
);
},
//6
child: ClipRRect(
borderRadius: BorderRadius.circular(Sizes.dimen_8.w),
//3
child: Stack(
children: <Widget>[
//4
CachedNetworkImage(
imageUrl: '${ApiConstants.BASE_IMAGE_URL}${movie.posterPath}',
fit: BoxFit.cover,
width: Sizes.dimen_100.h,
),
//5
Align(
alignment: Alignment.topRight,
child: GestureDetector(
onTap: () => BlocProvider.of<FavoriteBloc>(context)
.add(DeleteFavoriteMovieEvent(movie.id)),
child: Padding(
padding: EdgeInsets.all(Sizes.dimen_12.w),
child: Icon(
Icons.delete,
size: Sizes.dimen_12.h,
color: Colors.white,
),
),
),
),
],
),
),
),
);
}
}
  1. This widget will take movie as the required field.
  2. In the build(), we will give some margin and rounded corners.
  3. Use Stack to position image below the delete icon.
  4. First child will be the poster image of 100 width and will always follow aspect ratio.
  5. Next, we need delete icon on top. On tap of this icon, we will delete the movie from favorite list. So, fire DeleteFavoriteMovieEvent on favorite bloc to delete the movie.
  6. Clip the Stack so that we can see the round edges of Container above.
  7. When we tap on the card, we must navigate to the movie detail screen. So use GestureDetector.

We are done with favorite movies features completely. Let’s start with the language now.

Save Preferred Language In Local DB

Till now we only have user’s preferred language as app specific data, so once user closes the app and re-opens the app the preferred language is not persisted. Today, we have language, tomorrow we can have other such things. So, we will create a new repository which will deal with this type of data. Name is as app_repository.dart in domain layer.

//1
abstract class AppRepository {
//2
Future<Either<AppError, void>> updateLanguage(String language);
Future<Either<AppError, String>> getPreferredLanguage();
}
  1. Declare an abstract class because like MovieRepository, this will also have the implementation in data layer.
  2. We need 2 functionalities, save language in local db and fetch the saved language.

Create the implementation file in data/repositories now:

class AppRepositoryImpl extends AppRepository {

@override
Future<Either<AppError, String>> getPreferredLanguage() {
throw UnimplementedError();
}

@override
Future<Either<AppError, void>> updateLanguage(String language) {
throw UnimplementedError();
}

}

Before adding any functionality to it, let’s head on to data source for language.

Create a new file language_local_data_source.dart:

//1
abstract class LanguageLocalDataSource {
Future<void> updateLanguage(String languageCode);
Future<String> getPreferredLanguage();
}

//2
class LanguageLocalDataSourceImpl extends LanguageLocalDataSource {
@override
Future<String> getPreferredLanguage() {
throw UnimplementedError();
}

@override
Future<void> updateLanguage(String languageCode) {
throw UnimplementedError();
}
}
  1. Start with the abstract class and have the similar 2 methods for 2 functionalities.
  2. Create an implementation of the same and override the 2 methods.

Let’s now work on saving language to local db and getting back language from local db.

//1
@override
Future<void> updateLanguage(String languageCode) async {
final languageBox = await Hive.openBox('languageBox');
unawaited(languageBox.put('preferred_language', languageCode));
}

//2
@override
Future<String> getPreferredLanguage() async {
final languageBox = await Hive.openBox('languageBox');
return languageBox.get('preferred_language');
}
  1. First, open the box and save the language code in preferred_language key. We are using unawaited because we need not wait for this operation.
  2. In second method as well, fetch the language from same box and same key.

Now update the repository implementation by making use of these methods.

//1
final LanguageLocalDataSource languageLocalDataSource;

AppRepositoryImpl(this.languageLocalDataSource);

//2
@override
Future<Either<AppError, String>> getPreferredLanguage() async {
try {
final response = await languageLocalDataSource.getPreferredLanguage();
return Right(response);
} on Exception {
return Left(AppError(AppErrorType.database));
}
}

//3
@override
Future<Either<AppError, void>> updateLanguage(String language) async {
try {
final response = await languageLocalDataSource.updateLanguage(language);
return Right(response);
} on Exception {
return Left(AppError(AppErrorType.database));
}
}
  1. Declare variable for LanguageLocalDataSource and pass it in constructor.
  2. Fetch the preferred language by using the getPreferredLanguage() from local data source. Use catch block to return the database type error.
  3. In the next method, update the language by calling updateLanguage() in the same fashion.

Last thing before we make these calls in bloc is making usecases for these 2.

Create UpdateLanguage useacase:

//1
class UpdateLanguage extends UseCase<void, String> {
//2
final AppRepository appRepository;

UpdateLanguage(this.appRepository);

@override
Future<Either<AppError, void>> call(String languageCode) async {
return await appRepository.updateLanguage(languageCode);
}
}
  1. This usecase takes in String and returns with nothing, fire and forget.
  2. Declare the instance of AppRepository so that you can call the updateLanguage().

Create GetPreferredLanguage usecase now:

//1
class GetPreferredLanguage extends UseCase<String, NoParams> {
final AppRepository appRepository;

GetPreferredLanguage(this.appRepository);

@override
Future<Either<AppError, String>> call(NoParams params) async {
return await appRepository.getPreferredLanguage();
}
}
  1. This usecase take no params and returns the language code as String.

Dependency Injection

We have new repository, data source and usecase. Let’s put them in GetIt.

//1
getItInstance.registerLazySingleton<AppRepository>(() => AppRepositoryImpl(
getItInstance()));

//2
getItInstance.registerLazySingleton<LanguageLocalDataSource>(
() => LanguageLocalDataSourceImpl());

//3
getItInstance.registerLazySingleton<UpdateLanguage>(
() => UpdateLanguage(getItInstance()));

getItInstance.registerLazySingleton<GetPreferredLanguage>(
() => GetPreferredLanguage(getItInstance()));
  1. Declare AppRepository with instance of LanguageLocalDataSource.
  2. Declare LanguageLocalDataSource like others.
  3. Now declare 2 usecases with instance of AppRepository.

We need some improvements in the LanguageBloc:

//1
class LanguageBloc extends Bloc<LanguageEvent, LanguageState> {
final GetPreferredLanguage getPreferredLanguage;
final UpdateLanguage updateLanguage;

LanguageBloc({
@required this.getPreferredLanguage,
@required this.updateLanguage,
}) : super(
LanguageLoaded(
Locale(Languages.languages[0].code),
),
);

@override
Stream<LanguageState> mapEventToState(
LanguageEvent event,
) async* {
//2
if (event is ToggleLanguageEvent) {
await updateLanguage(event.language.code);
add(LoadPreferredLanguageEvent());
}
//3
else if (event is LoadPreferredLanguageEvent) {
final response = await getPreferredLanguage(NoParams());
yield response.fold(
(l) => LanguageError(),
(r) => LanguageLoaded(Locale(r)),
);
}
}
}
  1. Add the 2 usecases so that we can call them based on event. Mark them required also.
  2. Handle the ToggleLanguageEvent and update the language. After this call, you will load the preferred language.
  3. When event is LoadPreferredLanguageEvent, call the getPreferredLanguage() and yield the state based on success or failure.

Create the LoadPreferredLanguageEvent in languge_event.dart:

class LoadPreferredLanguageEvent extends LanguageEvent {}

At last, invoke this event on language bloc in MovieApp:

_languageBloc.add(LoadPreferredLanguageEvent());
  1. Call this in the initState(), so that this is the first call when we land on the application.

Now, before running you will also have to update the declaration of language bloc in GetIt:

getItInstance.registerSingleton<LanguageBloc>(
LanguageBloc(
getPreferredLanguage: getItInstance(),
updateLanguage: getItInstance(),
),
);

Run the app after stopping. Select the Spanish language and stop the app. Again open the app, you’ll see the text are in Spanish language. This is what we have achieved by saving the language.

By this, we have come to an end for this article. In the next article, we will work on the search feature. I hope you have learnt local database in this article. See you in the next article. Thanks for reading.

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.

--

--