Introducing rx_bloc: Part 2

Georgi Stanev
Prime Holding JSC
Published in
7 min readJan 20, 2021

The rx_bloc is a Dart package that helps to implement the BLoC (Business Logic Component) Design Pattern using the power of the reactive streams. In the previous article, introducing rx_bloc ecosystem: Part 1 (I recommend that you read it quickly before continuing with this article if you haven’t already), you learned what motivated us to build this package …now it is time to see how it works in action.

By definition, the BloC layer should contain only the business logic of your app. This means that it should be fully decoupled from the UI layer and should be loosely coupled with the data layer through Dependency Injection.

Thus, the UI layer communicates with the BloC by sending events and listens for state changes. Let’s see again what the Counter sample we want to build looks like and what the contracts will be:

Events

Users can basically do two things in the Counter sample, they can either increment or decrement the count. To achieve this, we need to create a new file counter_bloc.dart and to define the following Events contract.

/// A contract, containing all Counter BloC events
abstract class CounterBlocEvents {
/// Increment the count
void increment();

/// Decrement the count
void decrement();
}

As you can see, all events are just pure methods declared in one abstract class (CounterBlocEvents). This way, we can push events to the BloC by simply invoking those methods from the UI layer as follows:

RaisedButton(
onPressed: () => bloc.events.increment(),
...
)

States

When the user taps on the decrement or increment button, they should see a loading indicator while the API request is being executed. Once the API returns a response, the app should present either the updated count or a user-friendly error message. For this, we need to define the following States contract.

counter_bloc.dart

/// A contract, containing all Counter BloC states
abstract class CounterBlocStates {
/// The count of the Counter
///
/// It can be controlled by executing
/// [CounterBlocEvents.
increment] and
/// [CounterBlocEvents.decrement]
///
Stream<int> get count;

/// Loading state
Stream<bool> get isLoading;

/// User friendly error messages
Stream<String> get errors;
}

The same way as with the events, now you can see that we have declarations for multiple states grouped in one abstract class (CounterBlocStates). Depending on your goals, you can either have just one stream with all needed data or you can split it into individual streams. In the latter case, you can apply the Single-responsibility principle or any performance optimizations. We will see how we can take advantage of having multiple states per BloC in a minute.

Zero-boilerplate BloC

Once we have defined the states and events contracts, it’s time to implement the actual Counter BloC, where the business logic of the main feature of the app will reside. So let’s create the CounterBloc class in counter_bloc.dart (just after the contracts) as shown below counter_bloc.dart

...
@RxBloc()
class CounterBloc extends $CounterBloc {}

Android Studio Plugin

You can create the contracts along with the BloC class by yourself, but this seems to be a tedious task, isn’t it? So, to facilitate this we recommend using the RxBloC Plugin for Android Studio that helps effectively creating reactive BloCs.

By selecting New -> RxBloc Class the plugin will create the following files

  • ${name}_bloc.dart The file, where the business logic resides (the contracts ( events and states) along with the BloC itself).
  • ${name}_bloc.rxb.g.dart The file, where the autogenerated boilerplate code ($CounterBloc and CounterBlocType) resides.

Generator

The plugin creates just the initial state of the BloC. For all further updates, you will need a tool, which will be updating the generated file (${name}_bloc.rxb.g.dart) based on your needs. Here is where the rx_bloc_generator package helps, as automatically writes all the boring boilerplate code so you can focus on your business logic instead. You just need to add it to your pubspec.yaml file as follows:

dev_dependencies:
build_runner:
rx_bloc_generator:^2.0.0

Once added to the pubspec.yaml, just run flutter pub get to fetch the newly added dependencies, and then start the generator by executing this command flutter packages pub run build_runner watch --delete-conflicting-outputs.

So let’s implement the business logic of our Counter BloC.

counter_bloc.dart

/// A BloC responsible for count calculations
@RxBloc()
class CounterBloc extends $CounterBloc {
/// The default constructor injecting a repository through DI
CounterBloc(this._repository);
/// The repository used for data source communication
final CounterRepository _repository;

/// Map increment and decrement events to the `count` state
@override
Stream<int> _mapToCountState() => Rx.merge<Result<int>>([
// On increment
_$incrementEvent.flatMap((_) =>
_repository.increment().asResultStream()),
// On decrement
_$decrementEvent.flatMap((_) =>
_repository.decrement().asResultStream()),
])
// This automatically handles the error and loading state.
.setResultStateHandler(this)
// Provide "success" response only.
.whereSuccess()
//emit 0 as an initial value
.startWith(0);
...
}

As you can see, by extending $CounterBloc, we must implement Stream<int> _mapToCountState() , which is the method responsible for the events-to-state business logic. Furthermore, we have _$incrementEvent and _$decrementEvent, which are the subjects where the events will flow when increment() and decrement() methods are invoked from the UI Layer.

In the code above we declare that as soon as the user taps on the increment or decrement button, an API call will be executed. Since both _$incrementEvent and _$decrementEvent are grouped in one stream by the merge operator, the result of their API calls allows us to register our BloC as a state handler. We then extract only the “success” result and finally put an initial value of 0.

State Handlers

Most of the time when building mobile applications we have to communicate with external APIs, which means that we need to take care of:

  1. Error handling
  2. Loading state

Fortunately, rx_bloc has built-in tools that can help us with this.

As you can see, we set the current BloC as a state handler to a particular Result stream and then we get only the success state. But wait for a second, what about the error and loading handling? By using setResultStateHandler, both are handled by the BloC, as all errors are dispatched to the errorState stream and all loading activities are dispatched to the loadingState stream.

The advantage of this approach is that you have one central place (stream) for all loading activities and one central place (stream) for all errors that happen within the Counter BloC. In addition, if you need to execute API calls in a couple of event-to-state mappers within one BloC, the app would have to either show a loading indicator for all of them or handle all errors. In this case, the errorState stream and the loadingState stream would be particularly beneficial.

So far so good, going back to the implementation, the errorState and loadingState are not exposed yet, so we need to take care of them by simply writing mappers as follows:

counter_bloc.dart

@RxBloc()
class CounterBloc extends $CounterBloc {
...
@override
Stream<String> _mapToErrorsState() =>
errorState.map((Exception error) => error.toString());

@override
Stream<bool> _mapToIsLoadingState() => loadingState;
}

The advantage of manually exposing these states is that you have total control over your BloC states. For example, you can map an exception to a user-friendly error message.

Put it all together

/// This BloC and its event and state contracts usually
/// reside in counter_bloc.dart

/// A contract class containing all events.
abstract class CounterBlocEvents {
/// Increment the count
void increment();

/// Decrement the count
void decrement();
}

/// A contract class containing all states for our multi-state BloC.
abstract class CounterBlocStates {
/// The count of the Counter
///
/// It can be controlled by executing
/// [CounterBlocEvents.increment] and

/// [CounterBlocEvents.decrement]
///
Stream<int> get count;

/// Loading state
Stream<bool> get isLoading;

/// Error messages
Stream<String> get errors;
}

/// A BloC responsible for count calculations
@RxBloc()
class CounterBloc extends $CounterBloc {
/// Default constructor
CounterBloc(this._repository);

final CounterRepository _repository;

/// Map increment and decrement events to `count` state
@override
Stream<int> _mapToCountState() => Rx.merge<Result<int>>([
// On increment.
_$incrementEvent.flatMap((_) =>
_repository.increment().asResultStream()),
// On decrement.
_$decrementEvent.flatMap((_) =>
_repository.decrement().asResultStream()),
])
// This automatically handles the error and loading state.
.setResultStateHandler(this)
// Provide success response only.
.whereSuccess()
//emit 0 as an initial value
.startWith(0);

@override
Stream<String> _mapToErrorsState() =>
errorState.map((Exception error) => error.toString());

@override
Stream<bool> _mapToIsLoadingState() => loadingState;
}

Unit tests

One of the advantages of having a clean business layer is that it is unit-testable, right?

Let’s see how the rx_bloc_test package can reduce the development time for implementing unit tests and also make them easy to read and maintain.

rxBlocTest<CounterBloc, int>(
'Incrementing value',
build: () async => CounterBloc(CounterRepositoryMock()),
state: (bloc) => bloc.states.count,
act: (bloc) async => bloc.events.increment(),
expect: [1],
)

You can see that we specify the BloC type along with the type of state we want to test. We then create an instance of the BloC, specify the exact state, send some events to the BloC and finally assert that the actual result is the one we expect it to be.

For more information and examples check the package and the samples.

Conclusion

Using rx_bloc in combination with rx_bloc _generator makes the BloCs clean, easy to maintain, zero-boilerplate, and completely unit-testable. With rx_bloc_test we can ensure that our BloCs work as expected. But now the question is how do we expose our reactive BloCs to the UI layer? Since this is a fairly large topic, it deserves to be considered separately, so if you’re interested, check out the next article: Introducing flutter_rx_bloc: Part 3

--

--