Introduction to Flutter BLoC state management | Practical example

June 30, 2022

One of the first big steps in writing Flutter apps is grasping the idea of state management. There are many different approaches for managing states, such as Riverpod, Provider, Inherited Widget, or even the most straightforward – simple SetState(). However, when you scroll through job boards, BLoC emerges as the most widely commercially used state management tool in the Flutter framework.

It doesn’t matter whether you are an experienced mobile developer or just starting your adventure with Flutter apps – at some point, you’ve either already learned BLoC state management or will do it soon. This article is here to help you get the general idea 😊

Why BLoC?

BLoC stands for Business Logic Component. The whole idea is to provide a Stream of events from your UI to BLoC and if something happens (when an event occurs) – return a new state back to the UI. The continuous flow of data makes it accessible and easy to manage.

BLoC overview

Practical Use

BLoC Set-up

Let’s work with an example – imagine you have UI designed for creating an account for whatever type of app. Typically for convenience and to maintain clean architecture, we should split BLoC into three files – event, state, and bloc. So let’s split our directory.

BLoC directory structure

Assuming that our form should have five input text fields (name, last name, email, password, nationality) and one checkbox (for accepting privacy policy) - let’s set up state components for this screen. In file register_state.dart, create a simple class with initial default values for each property and basic getter:

part of 'register_bloc.dart';

class RegisterState {
  final String name;
  final String lastName;
  final String email;
  final String password;
  final String nationality;
  final bool privacyChecked;

  RegisterState({
    this.name = '',
    this.lastName = '',
    this.email = '',
    this.password = '',
    this.nationality = '',
    this.privacyChecked = false,
  });

  List<Object> get properties => [
    name,
    lastName,
    email,
    password,
    nationality,
    privacyChecked,
  ];
}

To modify the state, we need to design events in the register_event.dart file. They can be understood as ‘containers’ for data transportation. In fact, they are just a bunch of simple classes:

part of 'register_bloc.dart';

abstract class RegisterEvent {}

class RegisterEventName extends RegisterEvent {
  RegisterEventName(this.name);
  final String name;
} 

class RegisterEventLastname extends RegisterEvent {
  RegisterEventLastname(this.lastName);
  final String lastName;
}

class RegisterEventEmail extends RegisterEvent {
  RegisterEventEmail(this.email);
  final String email;
}

class RegisterEventPassword extends RegisterEvent {
  RegisterEventPassword(this.password);
  final String password;
}

class RegisterEventNationality extends RegisterEvent {
  RegisterEventNationality(this.nationality);
  final String nationality;
}

class RegisterEventAgreement extends RegisterEvent {
  RegisterEventAgreement(this.privacyChecked);
  final bool privacyChecked;
}

class RegisterEventSend extends RegisterEvent {
  RegisterEventSend();
}

In the register_bloc.dart file, we need to provide a function that accepts an event and maps it to a state object, which is later emitted back to the UI:

import 'package:flutter_bloc/flutter_bloc.dart';

part 'register_state.dart';
part 'register_event.dart';

class RegisterBloc extends Bloc<RegisterEvent, RegisterState> {
  RegisterBloc() : super(RegisterState()) {
    on<RegisterEvent>(mapEventToState);
  }

  String _name = '';
  String _lastName = '';
  String _email = '';
  String _password = '';
  String _nationality = '';
  bool _privacyChecked = false;

  void mapEventToState(RegisterEvent event, Emitter<RegisterState> emit) 
    async {

  if (event is RegisterEventName) {
    _name = event.name;
  }

  if (event is RegisterEventLastname) {
    _lastName = event.lastName;
  }

  if (event is RegisterEventEmail) {
    _email = event.email;
  }

  if (event is RegisterEventPassword) {
    _password = event.password;
  }

  if (event is RegisterEventNationality) {
    _nationality = event.nationality;
  }

  if (event is RegisterEventAgreement) {
    _privacyChecked = event.privacyChecked;
  }

  if (event is RegisterEventSend) {
    // do some action for example try to create an 
    // account in the database based on input
  }
    emit(getBlocState());
  }

  RegisterState getBlocState() {
    return RegisterState(
      name: _name,
      lastName: _lastName,
      email: _email,
      password: _password,
      nationality: _nationality,
      privacyChecked: _privacyChecked,
    );
  }
}

Now, when all three files are set up, we need to find a way to trigger events to let BLoC know it should process some data.

Flow of Data

To track a piece of information through its lifecycle, we need to implement BlocProvider in the screen_register.dart file first. It is required to provide context for RegisterBloc. Then as a child, we can pass a view that implements BlocBuilder with an indication to RegisterBloc and RegisterState.

Side note: RegLogTextField() is a custom text-field, which is essentially a variation of Flutter TextFormField(). Property shown in the code - textListener: is an equivalent of onChanged: from the original TextFormField().

For simplification in the example only one text-field is shown. Remaining text-fields can be implemented in a similar way.

@override Widget build(BuildContext context) { 
    return Scaffold( 
       appBar: const RegLogAppBar( title: 'readme...'), 
       body: GestureDetector( 
           onTap: () => FocusScope.of(context).unfocus(), 
           child: Theme(
                 data: RegLogTheme().provideTheme(RegLogThemeVariant.main), 
                  child: SafeArea( 
                        child: BlocProvider( 
                         create: (context) => RegisterBloc(), 
                         child: const RegisterView(), 
                      ), 
                   ), 
               ), 
          )); 
       } 
    } 
    
class RegisterView extends StatelessWidget { 
const RegisterView({Key? key}) : super(key: key); 

@override Widget build(BuildContext context) { 
    return BlocBuilder<RegisterBloc, RegisterState>( 
      builder: ((context, state) => SingleChildScrollView( 
          child: Column(children: [ 
              const SizedBox(height: 20, ), 
              const RegLogText(text: 
              'Please provide Your basic information:'),
              RegLogTextField( 
                textListener: (value) => context 
                                        .read<RegisterBloc>() 
                                        .add(RegisterEventName(value)),      
                hint: 'name:',), 
              RegLogTextField( ...

In the example above, each time we provide some value to the text field, a new RegisterEventName(value) is added to BLoC.

BLoC emits events

In register_bloc.dart, function mapEventToState() is triggered and executes code surrounded by if() statement for this particular type of event (in this case RegisterEventName). After running the code, we map processed data to state object, with the help of getBlocState() function, and then, we emit state back to UI calling emit().

BLoC passes state

Summary

And that would be it! I recommend getting familiar with as many Flutter state management approaches as possible to get the best understanding of the concept. It’s best to develop simple apps to learn in practice. Thank you for reaching the end of this short article. I hope it was helpful 🙂

What You have to say about this - don't hesitate to comment :)

comments powered by Disqus