Flutter TDD Clean Architecture Course [10] – Bloc Scaffolding & Input Conversion

7  comments

Presentation layer contains the UI in the form of Widgets and also the presentation logic holders, which can be implemented as a ChangeNotifier, Bloc, Reducer, ViewModel, MobX Store... You name it! In the case of our Number Trivia App though, we're going to use the flutter_bloc package to help us with implementing the BLoC pattern.

TDD Clean Architecture Course
This post is just one part of a tutorial series. See all of the other parts here and learn to architect your Flutter apps!

Setting Up the IDE

Before creating the files and classes needed for the Bloc, it's wise to hand over the repetitive work to a VS Code extension or an IntelliJ plugin (click on the links!). While you can totally create all the files yourself, it's much better to just click a button and let the extension do the job for you.

In addition to adding a simple way to create Bloc files, this extension/plugin adds handy code snippets to use from the Widgets when building the UI.

Events, States, Bloc and More

Bloc, also written as BLoC is an abbreviation for Business Logic Component. Following the Clean Architecture, it should rather be called a PLoC (Presentation Logic Component) but I think we'll stick to the original naming convention ?. All of the business logic is in the domain layer, after all.

If you're unfamiliar with Bloc, you should really check out the tutorial below to get a full, in-depth explanation. It's for an older version of the package, but the essence remains the same.

In a nutshell, Bloc is a reactive state management pattern where data flows only in one direction. It can all be separated into three core steps:

  1. Events (such as "get concrete number trivia") are dispatched from the UI Widgets
  2. Bloc receives Events and executes appropriate business logic (calling the Use Cases, in the case of Clean Architecture). 
  3. States (containing a NumberTrivia instance, for example) are emitted from the Bloc back to the UI Widgets, which display the newly arrived data.

Data flowing in one way without any side effects

Creating the Files

In VS Code and with the extension installed, right click on the bloc folder and select "Bloc: New Bloc" from the menu.

Give the files a prefix "number_trivia".

Finally choose to use Equatable to make the generated Events and States have value equality right from the start.

The extension will now generate 3 regular files, from which each one contains a basic class for the Bloc, Event and State respectively. The 4th file called simply bloc.dart is the so-called "barrel file" which just exports all the other ones. This makes for easier imports in other parts of the presentation layer.

Events

The number_trivia_event.dart file currently contains only a base abstract class, from which all of our custom Events will inherit. What kinds of Events should the Widgets be able to dispatch to the Bloc? Well, by looking at the UI, there are only 2 buttons - one for showing trivia for a concrete number, another for a random number.

Therefore, it's a good idea to have two events. You guessed them correctly - GetTriviaForConcreteNumber and GetTriviaForRandomNumber. Don't worry though, as now there will acutally be some difference in how we'll handle those events inside the Bloc. You'll see why in just a second.

The random Event will be only an empty class. The concrete Event though has to contain a field for the number. What should be the type of the field? It may be shocking to some of you, but the type of the number field will be a String.

number_trivia_event.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class NumberTriviaEvent extends Equatable {
  NumberTriviaEvent([List props = const <dynamic>[]]) : super(props);
}

class GetTriviaForConcreteNumber extends NumberTriviaEvent {
  final String numberString;

  GetTriviaForConcreteNumber(this.numberString) : super([numberString]);
}

class GetTriviaForRandomNumber extends NumberTriviaEvent {}

Events are dispatched from the Widgets. The widget into which the user writes a number will be a TextField. A value held inside a TextField is always a String.

Converting a String to an int directly in the UI or even inside the Event class itself would go against what we've been trying to accomplish with Clean Architecture all along - maintainability, readability and testability. Oh, and we would also violate the first SOLID principle of separation of concerns.

Never put any business or presentation logic into the UI. Flutter apps are especially susceptible to this since the UI code is also written in Dart.

InputConverter

We will break our tradition a bit and create a class for doing the conversion, an InputConverter, without creating its abstract class contract first. Personally, I feel that creating contracts for simple utility classes such as this one isn't necessary. Plus, since every class in Dart can be implemented as an interface, mocking the InputConverter while testing the Bloc in the next part will still be as easy as mocking an abstract class.

It will live inside the presentation layer very much like the the NumberTriviaModel lives inside the data layer. The purpose of the converter will be the same as that of the model - not to let the domain layer get entangled in the outside world. Numbers aren't strings, much like NumberTrivia isn't JSON, after all!

We're going to create a file for it in a new folder core / util. If you wanted to be very strict in the separation of code into layers, you could, of course, put it under core / presentation / util.

Location of the file

It will have a single method called stringToUnsignedInteger. That's because in addition to just parsing strings, it will also make sure that the inputted number isn't negative.

To make testing easier, let's create an empty method together with the Failure which will be returned if the number is invalid.

input_converter.dart

import 'package:dartz/dartz.dart';

import '../error/failure.dart';

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    // TODO: Implement
  }
}

class InvalidInputFailure extends Failure {}

Inside the test file which is at the usual mirrored location, there won't be anything to mock, since the InputConverter doesn't have any dependencies. The first test will handle the case when everything goes smoothly and the input String is in fact an unsigned (a.k.a. positive) integer.

input_converter_test.dart

import 'package:clean_architecture_tdd_prep/core/util/input_converter.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  InputConverter inputConverter;

  setUp(() {
    inputConverter = InputConverter();
  });

  group('stringToUnsignedInt', () {
    test(
      'should return an integer when the string represents an unsigned integer',
      () async {
        // arrange
        final str = '123';
        // act
        final result = inputConverter.stringToUnsignedInteger(str);
        // assert
        expect(result, Right(123));
      },
    );
  });
}

Writing the least amount of code possible, we will simply return the parsed string wrapped in the Right side of Either.

input_converter.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  return Right(int.parse(str));
}

Of course, when the String isn't a number at all, but it instead contains characters such as 'abc' or even if it contains decimal places, the method should return an InvalidInputFailure.

test.dart

test(
  'should return a failure when the string is not an integer',
  () async {
    // arrange
    final str = 'abc';
    // act
    final result = inputConverter.stringToUnsignedInteger(str);
    // assert
    expect(result, Left(InvalidInputFailure()));
  },
);

Parsing an invalid String to an int throws a FormatException, so we'll want to catch that and turn it into the Failure.

implementation.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  try {
    return Right(int.parse(str));
  } on FormatException {
    return Left(InvalidInputFailure());
  }
}

Finally, we want to limit the user to only input positive integers or a zero. If the inputted integer is negative, we'll return an InvalidInputFailure too.

test.dart

test(
  'should return a failure when the string is a negative integer',
  () async {
    // arrange
    final str = '-123';
    // act
    final result = inputConverter.stringToUnsignedInteger(str);
    // assert
    expect(result, Left(InvalidInputFailure()));
  },
);

implementation.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  try {
    final integer = int.parse(str);
    if (integer < 0) throw FormatException();
    return Right(integer);
  } on FormatException {
    return Left(InvalidInputFailure());
  }
}

This is all the InputConverter will do in the Number Trivia App. We're going to use it from within the Bloc in the next part.

States

The States outputted by the Bloc is what controls the UI. There is already one concrete class generated in the number_trivia_state.dart file. Just rename it to Empty.

In our case there will be four states - Empty, Loading, Loaded and Error. Similar to how the Events carry data from the UI to the BlocStates carry data from the Bloc to the UI. The Loaded state will contain a NumberTrivia entity to display the data from, and the Error state will contain an error message.

number_trivia_state.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import '../../domain/entities/number_trivia.dart';

@immutable
abstract class NumberTriviaState extends Equatable {
  NumberTriviaState([List props = const <dynamic>[]]) : super(props);
}

class Empty extends NumberTriviaState {}

class Loading extends NumberTriviaState {}

class Loaded extends NumberTriviaState {
  final NumberTrivia trivia;

  Loaded({@required this.trivia}) : super([trivia]);
}

class Error extends NumberTriviaState {
  final String message;

  Error({@required this.message}) : super([message]);
}

What's next?

We've created the Events and States for the Bloc, together with the InputConverter class which contains the presentation logic for converting a String to an int.

Coming up in the next part is the Bloc implementation, of course! This means, we're going to be doing test-driven development with Streams, because that's what the BLoC pattern is built on top of. Subscribe below to grow your Flutter coding skills by getting important Flutter news sent right into your inbox on a weekly basis.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

  • very good your post, but have you thought about remaking showing with MobX? It would be very interesting …

  • Hey Matt thank your very much for such a nice tutorial! Never seen anything as complete as this!

    I wonder how you would structure your Presentation Logic Holder Layer when using Change Notifier instead of BloC?

    I assume you would use the MVV(M/W) pattern. But I am never sure where to react on user events means where to call Use Cases. The ViewModel (implementation of ChangeNotifier) seems to be something like the State in BloC, since it holds the state of the view. And I am not sure if it should also be responsible for handling user interaction.

    • never mind, I saw your tutorial about state management. Where you explained that you used BLoC because of these concerns. I think i will at least decouple the state from the ChangeNotifier. thx!

  • Thanks for your great tutorials.

    I wonder why you always make all parameters of constructors “Optional named parameter” using curly braces and then put @required before all of them?
    So they are not optional anymore

  • I ran into a problem when implementing “InvalidInputFailure()” as it looks like the method does not exist. So I added it to the failures.

    class InvalidInputFailure extends Failure {}

    Is that the proper implementation or am I missing something?

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >