Learning TDD with Clean Architecture for Flutter Part IV

Safalshrestha
codingmountain
Published in
5 min readNov 30, 2022

Part I here

Part II here

Part III here

This is the final part of the series. Let’s get write into it.

Presentation Layer

Presentation Layer

It is the UI part. Code is written to give life to the app. It is the part where users interact. It consists of pages, widgets, and state management (bloc, provider, etc).

Pages

It consists of the screens to be displayed.

Widgets

It consists of the widgets that are being used.

Bloc

Let’s create a bloc for the movie list state management.

Eg. Movie Bloc lib/features/movie/presentation/bloc/movie_bloc.dart

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:movie_show/core/usecases/usecase.dart';
import 'package:movie_show/features/movie/domain/entities/movie_entity.dart';
import 'package:movie_show/features/movie/domain/usecases/get_movie_list_usecase.dart';

part 'movie_event.dart';
part 'movie_state.dart';

class MovieBloc extends Bloc<MovieEvent, MovieState> {
final GetMovieListUsecase getMovieListUsecase;

MovieBloc({required this.getMovieListUsecase}) : super(MovieInitial()) {
on<MovieEvent>((event, emit) async => await _onGetMovies(emit));
}

Future<void> _onGetMovies(Emitter<MovieState> emit) async {
emit(MovieLoading());
final failureOrMovieList = await getMovieListUsecase.call(NoParams());
emit(failureOrMovieList.fold(
(failure) => const Error(message: "Server Failure"),
(movieList) => MovieLoaded(movieList: movieList)));
}
}

Eg. Movie Bloc State lib/features/movie/presentation/bloc/movie_state.dart

part of 'movie_bloc.dart';

abstract class MovieState extends Equatable {
const MovieState();

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

class MovieInitial extends MovieState {}

class MovieLoading extends MovieState {}

class MovieLoaded extends MovieState {
final List<MovieEntity> movieList;
const MovieLoaded({
required this.movieList,
});
@override
List<Object> get props => [movieList];
}

class Error extends MovieState {
final String message;
const Error({
required this.message,
});
@override
List<Object> get props => [message];
}

Eg. Movie Bloc Event lib/features/movie/presentation/bloc/movie_event.dart

part of 'movie_bloc.dart';

abstract class MovieEvent extends Equatable {
const MovieEvent();

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

class GetMovies extends MovieEvent {}

After creating the bloc we will test it.

Eg. Movie Bloc test: test/features/movie/presentation/bloc/movie_bloc_test.dart

import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_show/core/error/failure.dart';
import 'package:movie_show/core/usecases/usecase.dart';
import 'package:movie_show/features/movie/domain/entities/movie_entity.dart';
import 'package:movie_show/features/movie/domain/usecases/get_movie_list_usecase.dart';
import 'package:movie_show/features/movie/presentation/bloc/movie_bloc.dart';

class MockGetMovieListUsecase extends Mock implements GetMovieListUsecase {}

void main() {
late MovieBloc movieBloc;
late MockGetMovieListUsecase mockGetMovieListUsecase;
final tMovieList = [
const MovieEntity(
movieId: 'movieId',
title: 'title',
thumbnail: 'thumbnail',
movieUrl: 'movieUrl',
unlocked: true,
),
const MovieEntity(
movieId: 'moviesIds',
title: 'titles',
thumbnail: 'thumbnails',
movieUrl: 'movieUrls',
unlocked: false,
)
];
setUp(() {
mockGetMovieListUsecase = MockGetMovieListUsecase();
movieBloc = MovieBloc(getMovieListUsecase: mockGetMovieListUsecase);
});
group('Bloc Test', () {
test('initial state should be MovieInitial()', () async {
//Arrange
//Act
//Assert
expect(movieBloc.state, equals(MovieInitial()));
});
test('should get data from usecase', () async {
//Arrange
when(
() => mockGetMovieListUsecase.call(NoParams()),
).thenAnswer((_) async => Right(tMovieList));
//Act
movieBloc.add(GetMovies());
await untilCalled(() => mockGetMovieListUsecase.call(NoParams()));
//Assert
verify(() => mockGetMovieListUsecase.call(NoParams()));
});
test('should emit MovieLoaded when data is obtained succesfully', () async {
//Arrange
when(
() => mockGetMovieListUsecase.call(NoParams()),
).thenAnswer((_) async => Right(tMovieList));
//Assert Later
final expected = [
MovieLoading(),
MovieLoaded(movieList: tMovieList),
];
expectLater(movieBloc.stream, emitsInOrder(expected));
//Act
movieBloc.add(GetMovies());
// print(movieBloc.stream.runtimeType);
});
test('should emit Error when getting data fails', () async {
//Arrange
when(
() => mockGetMovieListUsecase.call(NoParams()),
).thenAnswer((_) async => Left(ServerFailure()));
//Assert Later
final expected = [
MovieLoading(),
const Error(message: 'Server Failure'),
];
expectLater(movieBloc.stream, emitsInOrder(expected));
//Act
movieBloc.add(GetMovies());
// print(movieBloc.stream.runtimeType);
});
});
}

First, we test the initial state of the bloc. During the GetMovies() event the bloc should emit two states MovieInitial() and MovieLoaded() or MovieLoading() or Error(). The state of the bloc is obtained as a stream. Here we are using the expectLater() as the bloc is executing later. So we should keep on listening for the stream of state. What about the page and widget? Shouldn’t we test it? Here we are doing the unit test, in order to test those we need to do a widget test. That’s a topic for another time. Now let’s create the UI i.e. pages and widgets. Wait a minute there arises a problem. The presentation layer only knows about the domain layer. It doesn’t know about the data layer. There seems to be no connection between those. In the presentation, we are only calling the use-case and the use-case only contains the abstract function. The UI has no way to access implementation. With all the work we have done and we can’t connect those dots.

We can solve these issues by using dependency injection. It connects all the things we have been doing so far. So let’s connect the dots using get_it.

Let’s create a file injection_container.dart

Eg . lib/injection_container.dart

final sl = GetIt.instance;
Future<void> init() async {
//bloc
// movie bloc
sl.registerFactory<MovieBloc>(
() => MovieBloc(
getMovieListUsecase: sl(),
),
);
//usecase
//movie use case
sl.registerLazySingleton<GetMovieListUsecase>(
() => GetMovieListUsecase(movieRepository: sl()));
//repository// movie repository
sl.registerLazySingleton<MovieRepository>(
() => MovieRepositoryImpl(
networkInfo: sl(),
movieListRemoteDataSource: sl(),
awsDataSource: sl(),
),
);
//data sourcessl.registerLazySingleton<MovieListRemoteDataSource>(
() => MovieListRemoteDataSourceImpl(provideFakeData: sl()),
)
//core//network info --> internet connected or not
sl.registerLazySingleton<NetworkInfo>(
() => NetworkInfoImpl(sl()),
);
//fake Data
sl.registerLazySingleton<ProvideFakeData>(() => ProvideFakeData());
//external
final sharedPreferences = await SharedPreferences.getInstance();
sl.registerLazySingleton<SharedPreferences>(
() => sharedPreferences,
);
sl.registerLazySingleton<Dio>(
() => Dio(),
);
sl.registerLazySingleton<Connectivity>(
() => Connectivity(),
);
}

The get_it package supports creating singletons and instance factories. Here, we’re going to register everything as required. We will register most of them as a singleton so that our app gets only one instance of the class per lifecycle except bloc which might need multiple instances.

To register, instantiate the class as usual and pass in sl() into every constructor parameter and define the constructor parameter in the same way and repeat until no constructor parameter is left.

Here we provide the implementation of the abstract classes should they appear in our code. Dependency injection was the missing link between the production and test codes, where we sort of injected the dependencies manually with mocked classes. Now that we’ve implemented the service locator, nothing can stop us from writing Flutter widgets which will utilize every bit of code we’ve written until now by showing a fully functional UI to the user.

Now you can initialize it in the main.dart file and start building your UI

Make sure to use the service locator that we created when accessing the bloc. To access movie bloc instance, you can use sl<MovieBloc>.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'injection_container.dart' as di;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.init();
runApp(
const MyApp(),
);
}

I won’t be doing the UI part. For reference go here and here(bloc)

Happy Coding!!

--

--

Safalshrestha
codingmountain

Curiosity killed the cat, but satisfaction brought it back.