Halo semuanya, pada bagian ini, saya akan membuat use case dan contract pada lapisan domain di aplikasi kita. Untuk diagram use case dapat di lihat pada artikel bagian 2.
Baiklah langsung saja kita mulai!
Movie Contract
Contract merupakan interface yang akan kita gunakan untuk menghubungkan layer domain dengan layer data.
lib/features/movie/domain/repositories/movie_contract.dart
import 'package:dartz/dartz.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/features/movie/domain/entities/genre.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie_detail.dart';
import 'package:movie_flutter/features/movie/domain/entities/search_query.dart';
abstract class MovieContract {
Future<Either<Failure, List<Movie>>> getTrendingMovies();
Future<Either<Failure, List<Movie>>> getMoviesByGenre(Genre genre);
Future<Either<Failure, List<Movie>>> searchMovies(SearchQuery searchQuery);
Future<Either<Failure, MovieDetail>> getMovieDetail(Movie movie);
}
lib/core/domain/failures.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'failure.freezed.dart';
@freezed
class Failure with _$Failure {
const factory Failure.localFailure({
required String message,
}) = MovieLocalFailure;
const factory Failure.serverFailure({
required String message,
}) = MovieServerFailure;
}
lib/features/movie/domain/entities/search_query.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
part 'search_query.freezed.dart';
@freezed
class SearchQuery with _$SearchQuery {
const factory SearchQuery({
required Title title,
}) = _SearchQuery;
}
test/features/movie/domain/entities/search_query_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:movie_flutter/features/movie/domain/entities/search_query.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
void main() {
group('SearchQuery entity', () {
test(
'should be created',
() {
final title = Title('Hello World');
final query = SearchQuery(title: title);
expect(query.title, title);
expect(query.title.getOrCrash(), title.getOrCrash());
},
);
});
}
Karena use case memiliki dependency terhadap MovieContract
maka kita akan membuat mock nya dengan menggunakan mocktail
test/features/movie/domain/repositories/movie_contract_mock.dart
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class MovieContractMock extends Mock implements MovieContract {}
Trending Movie
Pertama kita akan menghandle untuk use case mendapatkan trending movie.
test/features/movie/domain/usecases/get_trending_movies.test
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/value_object.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
import 'package:movie_flutter/features/movie/domain/usecases/get_trending_movies.dart';
import '../repositories/movie_contract_mock.dart';
void main() {
late GetTrendingMovies usecase;
late MovieContractMock contract;
late List<Movie> movies;
setUp(
() {
contract = MovieContractMock();
usecase = GetTrendingMovies(contract);
movies = <Movie>[
Movie(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
),
Movie(
id: UniqueId(2),
title: Title('World'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(7),
trailer: Trailer(null),
),
];
},
);
group(
'GetTrendingMovie use case',
() {
test(
'should return list of movies if success',
() async {
// arrange
when(() => contract.getTrendingMovies())
.thenAnswer((_) async => right<Failure, List<Movie>>(movies));
// act
final response = await usecase();
final data = response.toOption().toNullable();
// assert
verify(
() => contract.getTrendingMovies(),
).called(1);
assert(data != null, 'Response cannot be null here.');
expect(response.isRight(), true);
expect(data!.length, 2);
expect(data.first, movies.first);
},
);
test(
'should return failure if failed',
() async {
// arrange
when(() => contract.getTrendingMovies()).thenAnswer(
(_) async => left<Failure, List<Movie>>(
const Failure.serverFailure(message: 'Not Found'),
),
);
// act
final response = await usecase();
final data = response.toOption().toNullable();
// assert
verify(() => contract.getTrendingMovies()).called(1);
expect(response.isLeft(), true);
expect(data, null);
},
);
},
);
}
lib/features/movie/domain/usecase/get_trending_movies.dart
import 'package:dartz/dartz.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class GetTrendingMovies {
const GetTrendingMovies(this._contract);
final MovieContract _contract;
Future<Either<Failure, List<Movie>>> call() {
return _contract.getTrendingMovies();
}
}
Karena kita akan membuat usecase lain dengan struktur yang serupa, kita bisa mengabstraksi setiap use case untuk extends dari abstract class.
lib/core/domain/use_case.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
class NoParam extends Equatable {
@override
List<Object?> get props => [];
}
Jangan lupa tambahkan equatable: ^2.0.3
di pubspec.yaml
ya.
Lalu Usecase pertama kita refactor menjadi
import 'package:dartz/dartz.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/use_case.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class GetTrendingMovies extends UseCase<List<Movie>, NoParam> {
GetTrendingMovies(this._contract);
final MovieContract _contract;
@override
Future<Either<Failure, List<Movie>>> call(NoParam params) {
return _contract.getTrendingMovies();
}
}
dan file test
nya menjadi
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/use_case.dart';
import 'package:movie_flutter/core/domain/value_object.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
import 'package:movie_flutter/features/movie/domain/usecases/get_trending_movies.dart';
import '../repositories/movie_contract_mock.dart';
void main() {
late GetTrendingMovies usecase;
late MovieContractMock contract;
late List<Movie> movies;
setUp(
() {
contract = MovieContractMock();
usecase = GetTrendingMovies(contract);
movies = <Movie>[
Movie(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
),
Movie(
id: UniqueId(2),
title: Title('World'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(7),
trailer: Trailer(null),
),
];
},
);
group(
'GetTrendingMovie use case',
() {
test(
'should return list of movies if success',
() async {
// arrange
when(() => contract.getTrendingMovies())
.thenAnswer((_) async => right<Failure, List<Movie>>(movies));
// act
final response = await usecase(NoParam()); // <- changed
final data = response.toOption().toNullable();
// assert
verify(
() => contract.getTrendingMovies(),
).called(1);
assert(data != null, 'Response cannot be null here.');
expect(response.isRight(), true);
expect(data!.length, 2);
expect(data.first, movies.first);
},
);
test(
'should return failure if failed',
() async {
// arrange
when(() => contract.getTrendingMovies()).thenAnswer(
(_) async => left<Failure, List<Movie>>(
const Failure.serverFailure(message: 'Not Found'),
),
);
// act
final response = await usecase(NoParam()); // <- changed
final data = response.toOption().toNullable();
// assert
verify(() => contract.getTrendingMovies()).called(1);
expect(response.isLeft(), true);
expect(data, null);
},
);
},
);
}
Melihat List Movie berdasarkan Genre
Untuk use case ini, kita membutuhkan parameter dari pengguna berupa Genre
.
test/features/movie/domain/usecases/get_movies_by_genre_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/value_object.dart';
import 'package:movie_flutter/features/movie/domain/entities/genre.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
import 'package:movie_flutter/features/movie/domain/usecases/get_movies_by_genre.dart';
import '../repositories/movie_contract_mock.dart';
void main() {
late GetMoviesByGenre systemUnderTest;
late MovieContractMock contract;
late List<Movie> movies;
late Genre genre;
setUp(() {
contract = MovieContractMock();
systemUnderTest = GetMoviesByGenre(contract);
movies = <Movie>[
Movie(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
),
Movie(
id: UniqueId(2),
title: Title('World'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(7),
trailer: Trailer(null),
),
];
genre = Genre(id: UniqueId(1), name: StringSingleLine('Action'));
});
group('Get Movie by Genre Use Case', () {
test(
'should return list of movie when success',
() async {
// arrange
when(
() => contract.getMoviesByGenre(genre),
).thenAnswer(
(_) async => right<Failure, List<Movie>>(movies),
);
// act
final result = await systemUnderTest(
GetMoviesByGenreParams(genre),
);
final data = result.toOption().toNullable();
// assert
verify(() => contract.getMoviesByGenre(genre)).called(1);
expect(data != null, true);
expect(result.isRight(), true);
expect(data!.length, 2);
expect(data.first, movies.first);
},
);
test(
'should return failure when failed',
() async {
// arrange
when(
() => contract.getMoviesByGenre(genre),
).thenAnswer(
(_) async => left<Failure, List<Movie>>(
const Failure.serverFailure(message: 'Not Found'),
),
);
// act
final result = await systemUnderTest(
GetMoviesByGenreParams(genre),
);
final data = result.toOption().toNullable();
// assert
verify(() => contract.getMoviesByGenre(genre)).called(1);
expect(data, null);
expect(result.isLeft(), true);
},
);
});
}
dan implementasi nya di
lib/features/movie/domain/usecases/get_movies_by_genre.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/use_case.dart';
import 'package:movie_flutter/features/movie/domain/entities/genre.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class GetMoviesByGenre extends UseCase<List<Movie>, GetMoviesByGenreParams> {
GetMoviesByGenre(this._contract);
final MovieContract _contract;
@override
Future<Either<Failure, List<Movie>>> call(
GetMoviesByGenreParams params,
) async {
return _contract.getMoviesByGenre(params.genre);
}
}
class GetMoviesByGenreParams extends Equatable {
const GetMoviesByGenreParams(this.genre);
final Genre genre;
@override
List<Object?> get props => [genre];
}
Mencari Movie Berdasarkan Judul
Disini kita akan membuat usecase untuk pencarian movies.
test/features/movie/domain/usecases/search_movies_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/value_object.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/search_query.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
import 'package:movie_flutter/features/movie/domain/usecases/search_movies.dart';
import '../repositories/movie_contract_mock.dart';
void main() {
late SearchMovies systemUnderTest;
late MovieContractMock contract;
late List<Movie> movies;
late SearchQuery searchQuery;
setUp(() {
contract = MovieContractMock();
systemUnderTest = SearchMovies(contract);
movies = <Movie>[
Movie(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
),
Movie(
id: UniqueId(2),
title: Title('World'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(7),
trailer: Trailer(null),
),
];
searchQuery = SearchQuery(title: Title('Hello'));
});
group('Search Movie use case', () {
test(
'should return list of movie when success',
() async {
// arrange
when(
() => contract.searchMovies(searchQuery),
).thenAnswer(
(_) async => right<Failure, List<Movie>>(movies),
);
// act
final result = await systemUnderTest(
SearchMoviesParams(searchQuery),
);
final data = result.toOption().toNullable();
// assert
verify(() => contract.searchMovies(searchQuery)).called(1);
expect(data != null, true);
expect(result.isRight(), true);
expect(data!.length, 2);
expect(data.first, movies.first);
},
);
test(
'should return failure when failed',
() async {
// arrange
when(
() => contract.searchMovies(searchQuery),
).thenAnswer(
(_) async => left<Failure, List<Movie>>(
const Failure.serverFailure(message: 'Not Found'),
),
);
// act
final result = await systemUnderTest(
SearchMoviesParams(searchQuery),
);
final data = result.toOption().toNullable();
// assert
verify(() => contract.searchMovies(searchQuery)).called(1);
expect(data, null);
expect(result.isLeft(), true);
},
);
});
}
lib/features/movie/domain/usecases/search_movies.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/use_case.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/search_query.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class SearchMovies extends UseCase<List<Movie>, SearchMoviesParams> {
SearchMovies(this._contract);
final MovieContract _contract;
@override
Future<Either<Failure, List<Movie>>> call(SearchMoviesParams params) async {
return _contract.searchMovies(params.searchQuery);
}
}
class SearchMoviesParams extends Equatable {
const SearchMoviesParams(this.searchQuery);
final SearchQuery searchQuery;
@override
List<Object?> get props => [searchQuery];
}
Melihat Detail Movie
test/features/movie/domain/usecases/get_movie_detail_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/value_object.dart';
import 'package:movie_flutter/features/movie/domain/entities/genre.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie_detail.dart';
import 'package:movie_flutter/features/movie/domain/entities/value_objects.dart';
import 'package:movie_flutter/features/movie/domain/usecases/get_movie_detail.dart';
import '../repositories/movie_contract_mock.dart';
void main() {
late GetMovieDetail systemUnderTest;
late MovieContractMock contract;
late Movie movie;
late MovieDetail movieDetail;
setUp(() {
contract = MovieContractMock();
systemUnderTest = GetMovieDetail(contract);
movie = Movie(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
);
movieDetail = MovieDetail(
id: UniqueId(1),
title: Title('Hello'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(8),
trailer: Trailer(null),
synopsis: 'Synopsis',
duration: const Duration(minutes: 145),
genres: <Genre>[
Genre(
id: UniqueId(1),
name: StringSingleLine('Action'),
),
],
relatedMovies: <Movie>[
Movie(
id: UniqueId(2),
title: Title('World'),
poster: Poster(null),
releaseDate: DateTime.now().subtract(
const Duration(days: 360),
),
rating: Rating(7),
trailer: Trailer(null),
),
],
);
});
group('Get Movie Detail Use case', () {
test(
'should return movie detail if success',
() async {
// arrange
when(() => contract.getMovieDetail(movie)).thenAnswer(
(_) async => right<Failure, MovieDetail>(movieDetail),
);
// act
final result = await systemUnderTest(
GetMovieDetailParams(movie),
);
final data = result.toOption().toNullable();
// assert
verify(
() => contract.getMovieDetail(movie),
).called(1);
expect(result.isRight(), true);
expect(data != null, true);
expect(data, movieDetail);
},
);
test(
'should return failure if failed',
() async {
// arrange
when(() => contract.getMovieDetail(movie)).thenAnswer(
(_) async => left<Failure, MovieDetail>(
const Failure.serverFailure(message: 'Not Found'),
),
);
// act
final result = await systemUnderTest(GetMovieDetailParams(movie));
final data = result.toOption().toNullable();
// assert
verify(() => contract.getMovieDetail(movie)).called(1);
expect(result.isLeft(), true);
expect(data, null);
},
);
});
}
lib/features/movie/domain/usecases/get_movie_detail.dart
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:movie_flutter/core/domain/failures/failure.dart';
import 'package:movie_flutter/core/domain/use_case.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie.dart';
import 'package:movie_flutter/features/movie/domain/entities/movie_detail.dart';
import 'package:movie_flutter/features/movie/domain/repositories/movie_contract.dart';
class GetMovieDetail extends UseCase<MovieDetail, GetMovieDetailParams> {
GetMovieDetail(this._contract);
final MovieContract _contract;
@override
Future<Either<Failure, MovieDetail>> call(GetMovieDetailParams params) async {
return _contract.getMovieDetail(params.movie);
}
}
class GetMovieDetailParams extends Equatable {
const GetMovieDetailParams(this.movie);
final Movie movie;
@override
List<Object?> get props => [movie];
}
Sampai di sini dulu, selanjutnya kita akan mengerjakan use case yang membutuhkan data local.