5

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.

spuriousGeek
  • 131
  • 1
  • 3
  • 9

1 Answers1

4

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!"
                : "",
          ),
        );
      },
    );
  }
}
stewemetal
  • 138
  • 8
  • This is exactly the sort of thing I was looking for, thank you. On the back of your answer, would you recommend using the RxDart library in conjunction with BlocBuilder, or does BlocBuilder provide sufficient functionality? – spuriousGeek Oct 21 '20 at 14:32
  • Also (apologies for double posting), how is the event data actually passed in to the bloc in your example? Could you show what this would look like in the UI code? – spuriousGeek Oct 21 '20 at 14:52
  • Using Rx highly depends on your use-case. I would definitely use Rx *inside* a Bloc to merge streams coming from the data layer (network + database) for example. I can't think of a use-case for Rx on the UI side while using Bloc, but that just may be because of my lack of imagination. – stewemetal Oct 23 '20 at 16:56
  • On the UI side, I would use a [BlocProvider](https://pub.dev/packages/flutter_bloc#bloc-widgets) to set up a Bloc in the Widget tree, then access it via `BlocProvider.of(context)`. You can call `add(event)` on the result (which is the requested Bloc of course). – stewemetal Oct 23 '20 at 17:02