Learning TDD with Clean Architecture for Flutter Part IV

safalshrestha109

Safal Shrestha

Posted on January 4, 2023

Learning TDD with Clean Architecture for Flutter Part IV

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)));
  }
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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 sources

sl.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(),
  );

}
Enter fullscreen mode Exit fullscreen mode

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.

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(),
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Happy Coding!!

💖 💪 🙅 🚩
safalshrestha109
Safal Shrestha

Posted on January 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related