Flutter: Authentication using BLoC Pattern

Flutter: Authentication using BLoC Pattern

if you are writing a flutter app, authentication is the most basic requirement. You can follow many patterns for managing state in your app: provider, BLoC or redux. In this article we will be going through the BLoC pattern.

Just a quick introduction for anyone who is new to BLoC pattern. BLoC stands for Business Logic Components. BLoC is the logical layer that sits between UI component and data sources.

image.png

Three are three main components in BLoC pattern:

  1. Events : Events are input to BLoC. It can be anything, a button click, a scroll or changing orientation. Events are sent to BLoC with relevant information, for example, in case of Sign In, a event will be sent with all the user information.

  2. States: States are output from BLoC. UI component always listens to the state changes and redraw every time there is some state change. For example, when user is authenticated, a new state UserAuthenticated is thrown, and based on that we can make decision afterwards.

  3. Stream: This is the place where all communication takes place. There is stream of Events from UI component and stream of States from BLoC. BLoC contains the logic to convert these event into States. UI component sends events to Sink and listen for any State changes.

Now, lets get started with the code. First we need to add dependency for using BLoC pattern.

Dependency

Add following dependency to pubsec.yaml

dependencies:
  flutter_bloc: ^6.0.0 
  bloc: ^6.0.0
  equatable: ^1.2.3

Events

There will be three events:

  1. AppStarted: It will be default event that will be sent to BLoC on app initiation.
  2. LoginEvent: When user clicks on the login button.
  3. LogoutEvent: When user clicks on logout button.
auth_events.dart

////// This will be the base class that our events will extend to
@immutable
abstract class AuthEvent extends Equatable {
  @override
  List<Object> get props => [];
}

Equatable helps to compare different objects in dart. We need Event and State classes to extend Equatable. We want UI to redraw itself only when something has changed. We need to define props, to define how to compare objects.

immutable annotation means that all the fields should be final. It wont throw any exception, we will just get warnings.

auth_events.dart

class LoginEvent extends AuthEvent {
/// User class holds all the information related to sign in information
  final User user;
  LoginEvent({@required this.user});
  @override
  List<Object> get props => [user];
}

class LogoutEvent extends AuthEvent {}

States

Let's define States for our authentication bloc. There will be 4 states:

  1. LoginInitialState
  2. LoginLoadingState
  3. AuthenticatedState
  4. UnauthenticatedState

These are pretty self-explanatory, no need to explain them.

auth_states.dart

abstract class AuthState extends Equatable {
  @override
  List<Object> get props => [];
}

class LoginInitialState extends AuthState {}
class LoginLoadingState extends AuthState {}

class AuthenticatedState extends AuthState {
   final User user;
  AuthenticatedState({@required this.user});
  @override
  List<Object> get props => [user];
}

class UnauthenticatedState extends AuthState {}

BLoC

Here comes the fun part, let's write the part where all stream of Events are collected, analyzed(logic) and returned as stream of States.

We have to override the method mapEventToState for mapping Event to States.

auth_bloc.dart

class AuthBloc extends Bloc<AuthEvent, AuthState> {
 final _authService = registry<AuthenticationService>();

  final _log = Logger('LoginComponent');

  LoginComponent(AuthEvent initialState) : super(initialState);

 @override
  Stream<AuthState> mapEventToState(AuthEvent event) async* {
    if (event is AppStarted) {
      yield* _mapAppStartedToState(event);
    }

    if (event is LoginEvent) {
      yield* _mapLoginEventToState(event);
    }

    if (event is LogoutEvent) {
      yield* _mapLogoutEventToState(event);
    }
  }

yield* is used to recursively call its Iterable or Stream function.

auth_bloc.dart

Stream<AuthState> _mapLoginEventToState(
      LoginEvent event) async* {
    try {
      yield LoginLoadingState();
      await _authService.signin(event.user);
      final user = await _authService.getCurrentuser();
      yield AuthenticatedState(user: user);
    } catch (e, s) {
      _log.shout(s);
      yield UnauthenticatedState();
    }
  }

Stream<AuthState> _mapAppStartedToState(
      AppStarted event) async* {
    try {
      final isSignedIn = await _authService.isUserLoggedIn();
      if (isSignedIn) {
        final user = await _authService.getCurrentuser();
        yield AuthenticatedState(user: user);
      } else {
        yield UnauthenticatedState();
      }
    } catch (e, s) {
      _log.shout(s);
      yield UnauthenticatedState();
    }
  }

Stream<AuthState> _mapLogoutEventToState(
LogoutEvent event) async* {
   yield UnauthenticatedState();
    _authService.logOut();
}

We can use service_registry package to provide IOC in dart env. I prefer to make singleton object of external dependencies which can be easily done using service_registry. Eg. registry.registerLazySingleton( () => AuthenticationServiceImpl());

UI

I will be making pretty simple UI to introduce BLoC components like BlocBuilder, BlocProvider and MultiBlocProvider.

BlocBuilder is a Flutter widget which requires a Bloc and a builder function. BlocBuilder handles building the widget in response to new states.

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    //// Everytime BlocA returns new state this will be rebuild 
  }
)

BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of(context). It is used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.

BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(), ///// ChildA as well as all its child in tree will have access to BlocA
);

MultiBlocProvider is BlocProvider where we can define multiple Blocs.

I personally prefer using MultiBlocProvider on top of the tree, that way we won't have to take care of using provider everywhere. Also, MultiBlocProvider initiates Blocs lazily so it wont affect the performance much.

Well, let's dive into code now. This blog is not about UI/UX so, we wont be looking at the form, just the part where State management is required.

main.dart

void main() => runApp(
  MyApp());

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    bool light = true;
    return MultiBlocProvider(
      providers: _fetch(context),
      child: 
        MaterialApp(
          title: 'Authentication Demo',
          home: LoginPage(),
        ),
    );
  }

    List<BlocProvider> _fetch(BuildContext context) {
      List<BlocProvider> providers;
      providers.addAll([
      BlocProvider<AuthBloc>(
          create: (context) =>
              AuthBloc(LoginInitialState())..add(AppStarted())),
    ]);
    return providers;
  }
}


  class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
          color: Colors.white,
          child: BlocBuilder<AuthBloc, AuthState>(
            builder: (context, state) {
              if (state is LoginInitialState) {
                return Column(
                  children: <Widget>[
                    //// Add your form here
                    FlatButton(
                      color: Colors.black,
                      child: Text("Send code"),
                      onPressed: () => {
                        BlocProvider.of<AuthBloc>(context)
                            .add(LoginEvent(user: User()))   
    ////// This will send the LoginEvent to bloc
                      },
                    ),
                  ],
                );
              } else if (state is AuthenticatedState) {
                ////// Action after user is authenticated
              } else if (state is UnauthenticatedState) {
                /////// retry here 
              } else if (state is LoginLoadingState) {
                ///// Show a circularprogression indicator.
              } 
              return Center(
                child: Text("Error"),
              );
            },
          )),
    );
  }

Happy coding !!

Did you find this article valuable?

Support Dhairya Verma by becoming a sponsor. Any amount is appreciated!