Dart Flutter Article

Business Logic Component [4/4]

In this final article of the BLoC series, we will look at code examples and practical implementation of several popular cases. The first case - creating your own simple bloc based on a change notifier. In the second example, we implement pagination using the bloc package.
Plague Fox 6 min read
Business Logic Component [4/4]

In this final article of the BLoC series, we will look at code examples and practical implementation of several popular cases.

The first case - creating your own simple bloc based on a change notifier.

In the second example, we implement pagination using the bloc package.


Creating your own BLoC using ChangeNotifier

This can be useful if you want your own implementation without using streams and libraries, for example, in other languages or for your package.

Just in case, the implementation of change notification is straightforward, but of course, with flutter, you should use ChangeNotifier from package:flutter/foundation.dart

mixin ChangeNotifier {
  final List<VoidCallback> _listeners = List<VoidCallback>.empty();
  bool get hasListeners => _listeners.isNotEmpty;
  void addListener(VoidCallback listener) => _listeners.add(listener);
  void removeListener(VoidCallback listener) => _listeners.remove(listener);
  void notifyListeners() => _listeners.forEach((listener) => listener.call());
  void dispose() => _listeners.clear();
}

Let's create a base "BLoC" class based on ChangeNotifier:

import 'package:flutter/foundation.dart'
    show Listenable, ValueListenable, VoidCallback, ChangeNotifier;
import 'package:meta/meta.dart';

typedef SetState<State extends Object> = void Function(State state);

/// Selector from [BLoC]
typedef BLoCSelector<BLoC extends Listenable, Value> = Value Function(
  BLoC bloc,
);

/// Filter for [BLoC]
typedef BLoCFilter<State> = bool Function(State prev, State next);

abstract class BLoC<State extends Object> with ChangeNotifier {
  BLoC(State initialState) : _$state = initialState;

  /// The current state of the bloc
  @nonVirtual
  State get state => _$state;
  State _$state;

  /// Whether the bloc is currently handling a request
  @nonVirtual
  bool get isProcessing => _$isProcessing;
  bool _$isProcessing = false;

  @nonVirtual
  @protected
  void setState(State state) {
    _$state = state;
    notifyListeners();
  }

  @nonVirtual
  @protected
  Future<void> handle(
    Future<void> Function(SetState<State> setState) handler,
  ) async {
    // For throttling handle calls
    // also you can implement a queue for handle calls or something else
    if (_$isProcessing) return;
    _$isProcessing = true;
    notifyListeners();
    try {
      await handler(setState);
    } on Object {
      // TODO: Rethrow all errors to global observer,
      // and handle them in a single place
    } finally {
      _$isProcessing = false;
      notifyListeners();
    }
  }

  @protected
  @nonVirtual
  @override
  void notifyListeners() => super.notifyListeners();

  /// Transform [BLoC] in to [ValueListenable]
  @nonVirtual
  ValueListenable<Value> select<Value>(
    BLoCSelector<BLoC<State>, Value> selector, [
    BLoCFilter<Value>? test,
  ]) =>
      _BLoCView<BLoC<State>, Value>(this, selector, test);
}

@sealed
class _BLoCView<BLoC extends Listenable, Value>
    with ChangeNotifier
    implements ValueListenable<Value> {
  _BLoCView(
    BLoC bloc,
    BLoCSelector<BLoC, Value> selector,
    BLoCFilter<Value>? test,
  )   : _bloc = bloc,
        _selector = selector,
        _test = test;

  final BLoC _bloc;
  final BLoCSelector<BLoC, Value> _selector;
  final BLoCFilter<Value>? _test;

  @override
  Value get value => hasListeners ? _$value : _selector(_bloc);

  late Value _$value;

  void _update() {
    final newValue = _selector(_bloc);
    if (identical(_$value, newValue)) return;
    if (!(_test?.call(_$value, newValue) ?? true)) return;
    _$value = newValue;
    notifyListeners();
  }

  @override
  void addListener(VoidCallback listener) {
    if (!hasListeners) {
      _$value = _selector(_bloc);
      _bloc.addListener(_update);
    }
    super.addListener(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    super.removeListener(listener);
    if (!hasListeners) _bloc.removeListener(_update);
  }

  @override
  void dispose() {
    _bloc.removeListener(_update);
    super.dispose();
  }
}

Thus, we created a base class and, in addition, made it possible to convert it to ValueListenable, for use in the ValueListenableBuilder.

Let's now write an example implementation:

@freezed
class SampleState with _$SampleState {
  const SampleState._();

  /// Idling state
  const factory SampleState.idle({
    required final Data? data,
    @Default('Idle') final String message,
  }) = IdleSampleState;

  /// Processing
  const factory SampleState.processing({
    required final Data? data,
    @Default('Processing') final String message,
  }) = ProcessingSampleState;

  /// Successful
  const factory SampleState.successful({
    required final Data data,
    @Default('Successful') final String message,
  }) = SuccessfulSampleState;

  /// An error has occurred
  const factory SampleState.error({
    required final Data? data,
    @Default('An error has occurred') final String message,
  }) = ErrorSampleState;

  /// Data
  Data? get data => map<Data?>(
        idle: (state) => state.data,
        processing: (state) => state.data,
        successful: (state) => state.data,
        error: (state) => state.data,
      );

  /// Has data
  bool get hasData => data != null;

  /// If an error has occurred
  bool get hasError => maybeMap<bool>(orElse: () => false, error: (_) => true);

  /// Is in progress state
  bool get isProcessing => maybeMap<bool>(orElse: () => false, processing: (_) => true);
}

class SampleBLoC extends BLoC<SampleState> {
  SampleBLoC({required ISampleRepository repository})
      : _repository = repository,
        super(const SampleState.idle());

  final ISampleRepository _repository;

  Future<void> fetch({required String id}) => handle((setState) async {
        try {
          setState(SampleState.processing(data: state.data));
          final newData = await _repository.fetch(id: id);
          setState(SampleState.successful(data: newData));
        } on Object catch (error) {
          setState(SampleState.error(data: state.data, message: ErrorUtil.formatMessage(error)));
          rethrow;
        } finally {
          setState(SampleState.idle(data: state.data));
        }
      });
}

That's it, and now we have our own implementation of BLoC on ChangeNotifier.

Making your own implementation "on the streams" is also not a difficult task. These are two stream controllers (events and states), subscribing to events at the constructor and converting events to states (for example, using asyncExpand or switchMap stream transformer).


BLoC implementation with pagination

Let's try to implement typical pagination using a cursor (you can use any other method instead of a cursor, the primary approach will be the same).

First of all, let's create our data classes for the model, pagination chunk, and repository's interface:

typedef TweetID = String;

@immutable
abstract class Tweet with Comparable<Tweet> {
  factory Tweet.fromJson(Map<String, Object?> json) => throw UnimplementedError();

  abstract final TweetID id;

  Map<String, Object?> toJson();

  @override
  int compareTo(Tweet other) => id.compareTo(other.id);

  @override
  bool operator ==(Object other) => identical(other, this) || other is Tweet && id == other.id;

  @override
  int get hashCode => id.hashCode;
}

@immutable
abstract class TweetsChunk {
  abstract final List<Tweet> tweets;
  abstract final String? cursor;
  abstract final bool endOfList;
}

abstract class ITweetsRepository {
  @useResult
  Future<TweetsChunk> paginate({String? cursor});
}

Now time to create our states for pagination:

/// Business Logic Component Tweets States
@freezed
class TweetsState with _$TweetsState {
  const TweetsState._();

  /// Idling state
  const factory TweetsState.idle({
    required final List<Tweet> tweets,
    required final String? cursor,
    required final bool endOfList,
    @Default('Idle') final String message,
  }) = IdleTweetsState;

  /// Processing
  const factory TweetsState.processing({
    required final List<Tweet> tweets,
    required final String? cursor,
    required final bool endOfList,
    @Default('Processing') final String message,
  }) = ProcessingTweetsState;

  /// Successful
  const factory TweetsState.successful({
    required final List<Tweet> tweets,
    required final String? cursor,
    required final bool endOfList,
    @Default('Successful') final String message,
  }) = SuccessfulTweetsState;

  /// An error has occurred
  const factory TweetsState.error({
    required final List<Tweet> tweets,
    required final String? cursor,
    required final bool endOfList,
    @Default('An error has occurred') final String message,
  }) = ErrorTweetsState;

  static const TweetsState initialState = TweetsState.idle(tweets: <Tweet>[], cursor: null, endOfList: false);

  /// If an error has occurred
  bool get hasError => maybeMap<bool>(orElse: () => false, error: (_) => true);

  /// Is in progress state
  bool get isProcessing => maybeMap<bool>(orElse: () => false, processing: (_) => true);

  /// Has more data to fetch
  bool get hasMore => !endOfList;
}

And, of course, events (let there be only one).
It's a good idea to add mixins that release the next state based on the current state and the event. This will significantly reduce the amount of duplicate code in the BLoC implementation.

/// Business Logic Component Tweets Events
@freezed
class TweetsEvent with _$TweetsEvent {
  const TweetsEvent._();

  /// Fetch & paginate
  @With<_ProcessingStateEmitter>()
  @With<_SuccessfulStateEmitter>()
  @With<_ErrorStateEmitter>()
  @With<_IdleStateEmitter>()
  const factory TweetsEvent.paginate() = _PaginateTweetsEvent;
}

mixin _ProcessingStateEmitter on TweetsEvent {
  TweetsState processing({
    required final TweetsState state,
    final String? message,
  }) =>
      TweetsState.processing(
        tweets: state.tweets,
        cursor: state.cursor,
        endOfList: state.endOfList,
        message: message ?? 'Processing',
      );
}

mixin _SuccessfulStateEmitter on TweetsEvent {
  TweetsState successful({
    required final TweetsState state,
    required final TweetsChunk chunk,
    final String? message,
  }) =>
      TweetsState.successful(
        // Append new tweets to the existing ones
        // you can use more tricky logic to get rid of duplicates
        // (for example, added from the cache)
        tweets: <Tweet>[...state.tweets, ...chunk.tweets]..sort(),
        cursor: chunk.cursor,
        endOfList: chunk.endOfList,
        message: message ?? 'Successful',
      );
}

mixin _ErrorStateEmitter on TweetsEvent {
  TweetsState error({
    required final TweetsState state,
    final String? message,
  }) =>
      TweetsState.error(
        tweets: state.tweets,
        cursor: state.cursor,
        endOfList: state.endOfList,
        message: message ?? 'An error has occurred',
      );
}

mixin _IdleStateEmitter on TweetsEvent {
  TweetsState idle({
    required final TweetsState state,
    final String? message,
  }) =>
      TweetsState.idle(
        tweets: state.tweets,
        cursor: state.cursor,
        endOfList: state.endOfList,
        message: message ?? 'Idle',
      );
}

It's Business Logic Component time.
Please note that I register all events with one transformer handler on.
And also pay attention to the fact that I do not create additional mutable variables for the BLoC, all data is stored in the state, and new states are released due to the data of the previous one and event.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart' as bloc_concurrency;

/// Business Logic Component TweetsBLoC
class TweetsBLoC extends Bloc<TweetsEvent, TweetsState> implements EventSink<TweetsEvent> {
  TweetsBLoC({
    required final ITweetsRepository repository,
    final TweetsState? initialState,
  })  : _repository = repository,
        super(initialState ?? TweetsState.initialState) {
    // In most cases, all events must be recorded within one stream transformer
    on<TweetsEvent>(
      (event, emit) => event.map<Future<void>>(
        paginate: (event) => _paginate(event, emit),
      ),
      // Choose the transformer that suits your currency needs
      transformer: bloc_concurrency.droppable(),
      //transformer: bloc_concurrency.sequential(),
      //transformer: bloc_concurrency.restartable(),
      //transformer: bloc_concurrency.concurrent(),
    );
  }

  final ITweetsRepository _repository;

  /// Fetch event handler
  Future<void> _paginate(_PaginateTweetsEvent event, Emitter<TweetsState> emit) async {
    if (state.isProcessing || state.endOfList) return;
    try {
      emit(event.processing(state: state));
      final chunk = await _repository.paginate(cursor: state.cursor);
      emit(event.successful(state: state, chunk: chunk));
    } on Object {
      emit(event.error(state: state));
      rethrow;
    } finally {
      emit(event.idle(state: state));
    }
  }
}

It's simple, isn't it?
You got currency, immutability, error handling, four states, and pagination without any boilerplate.

Homework: add restoration from cache and filtering.


This completes the series of articles about the bloc.
I will try to update articles occasionally and keep them up to date.

Good luck in mastering the endless expanses of dart and flutter.

Share
Comments
More from Plague Fox
Microbenchmarks are experiments
Dart Flutter Article

Microbenchmarks are experiments

Benchmarks are not just about numbersโ€”they are experiments that need interpretation. This post dissects a Dart vs JavaScript microbenchmark, illustrating why cool animations often mask the real value: insightful analysis. Numbers without context are just as meaningful as numerology
Vyacheslav Egorov 12 min read

Plague Fox

Engineer by day, fox by night. Talks about Flutter & Dart.

Great! Youโ€™ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Plague Fox.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.