Best practice for BLoC pattern in Dart/Flutter

Viewed 2561

When using a the BLoC pattern in Flutter, what is considered good programming practice when it comes to structuring your application code?

This is a loose question so I will try to give an example. Let's say that you define a BLoC class to handle the validation for a specific Widget and you want to update validation messages by emitting various events as a form is populated.

As I understand it, your BLoC might look something like the below:

import 'dart:async';
import 'package:myproject/bloc/base_bloc.dart';
import 'package:rxdart/rxdart.dart';

class SignUpBloc extends Bloc {
    BehaviorSubject<String> _emailSubject;
    BehaviorSubject<String> _nameSubject;
    BehaviorSubject<String> _phoneSubject;
    BehaviorSubject<String> _signUpSubject;

    SignUpBloc() {
        _emailSubject = new BehaviorSubject<String>.seeded('');
        _nameSubject = new BehaviorSubject<String>.seeded('');
        _phoneSubject = new BehaviorSubject<String>.seeded('');
        _signUpSubject = new BehaviorSubject<String>.seeded('');
    }

    void nameChanged(String content) {
        if (content?.isEmpty ?? true) {
          _nameSubject.emit('Name is required for the sign-up process');
        } else {
           _nameSubject.emit('');
        }
    }

    void emailChanged(String content) {
        if (!_validEmail(content)) {
          _emailSubject.emit(
              'Please enter a valid email address (e.g. example@mydomain.com');
        } else {
          _emailSubject.emit('');
          _email = content;
        }
    }

    .
    .
    // Other functions are used to emit various messages here
    .
    .

    Stream<String> get emailStream => _emailSubject.stream;
    Stream<String> get nameStream => _nameSubject.stream;
    Stream<String> get phoneStream => _phoneSubject.stream;
    Stream<String> get signUpStream => _signUpSubject.stream;
}

...and then inside your ui/view you might do something like:

StreamBuilder<String>(
      stream: _bloc.emailStream,
      builder: (context, snapshot) {
        return Padding(
          padding: EdgeInsets.only(top: 5.0),
          child: Text(
            snapshot?.data ?? '',
          ),
        );
      },

Now, to me this code gives off a bad code smell...in particular, it seems kinda messy to keep defining BehaviorSubjects and then writing methods to enable access to their corresponding streams. All of the examples that I have found online define an approach very similar to this, and whilst it does allow business logic to be kept apart from the UI, it feels like there is a lot of duplicated effort to keep defining and exposing subjects.

1 Answers

While you approach can be reasonable in certain circumstances, using Bloc like this feels redundant.

A Bloc itself comes with 2 streams (built in, abstracted away), one for events and one for states.

Following your example, a more idiomatic approach with Bloc would look something like this:

State:

@immutable
abstract class FormState {
  final String email;
  final String name;
  final String phone;
  final String signUp;

  final String emailError;
  final String nameError;
  final String phoneError;
  final String signUpError;

  FormState({
    this.email = "",
    this.name = "",
    this.phone = "",
    this.signUp = "",
    this.emailError = "",
    this.nameError = "",
    this.phoneError = "",
    this.signUpError = "",
  });
}

Event:

@immutable
abstract class FormChangedEvent {
  final String email;
  final String name;
  final String phone;
  final String signUp;

  FormChangedEvent({
    this.email = "",
    this.name = "",
    this.phone = "",
    this.signUp = "",
  });
}

Bloc:

class FormBloc extends Bloc<FormChangedEvent, FormState> {
  FormBloc() : super(FormState());

  @override
  Stream<FormState> mapEventToState(FormChangedEvent event) async* {
    yield FormState(
      email: event.email,
      name: event.name,
      phone: event.phone,
      signUp: event.signUp,
      emailError: (event.email == null || event.email.isEmpty)
          ? "This field should not be empty!"
          : "",
      nameError: (event.name == null || event.name.isEmpty)
          ? "This field should not be empty!"
          : "",
      phoneError: (event.phone == null || event.phone.isEmpty)
          ? "This field should not be empty!"
          : "",
      signUpError: (event.signUp == null || event.signUp.isEmpty)
          ? "This field should not be empty!"
          : "",
    );
  }
}

With that set up, you can use a BlocBuilder to listen to state changes of your Bloc. In this example, you would receive instances of FormState which contain the state of your whole form.

Further down the line, you can create event and state subclasses and use is (state is Loading, state is FormReady) to render the different states of your page depending on your needs.

I hope this helps. :]

Update for Bloc 8.x.x

Bloc has a bit clearer event/state mapping logic since 8.0.0:

class FormBloc extends Bloc<FormChangedEvent, FormState> {
  FormBloc() : super(FormState()) {
    on<FormChangedEvent>(
      (event, emit) async {
        emit(
          FormState(
            email: event.email,
            name: event.name,
            phone: event.phone,
            signUp: event.signUp,
            emailError: (event.email == null || event.email.isEmpty)
                ? "This field should not be empty!"
                : "",
            nameError: (event.name == null || event.name.isEmpty)
                ? "This field should not be empty!"
                : "",
            phoneError: (event.phone == null || event.phone.isEmpty)
                ? "This field should not be empty!"
                : "",
            signUpError: (event.signUp == null || event.signUp.isEmpty)
                ? "This field should not be empty!"
                : "",
          ),
        );
      },
    );
  }
}
Related