There are many frameworks for Flutter state management, such as Flutter_bloc, MobX, GetX, etc. Today, I will talk about flutter_bloc, the first state management framework I used after LEARNING Flutter. This framework also has the most stars on Github, with 6.9K + up to now. It can be seen that this framework is highly recognized by everyone. In general, I think it is necessary to spend some time learning the basic knowledge of Flutter_bloc in the early stage, such as Bloc, cubit, BlocProvider, BlocListener, etc., and know when they are used respectively. How do I use it? Flutter_bloc!

Next, I will write a Starter project. While explaining the basic knowledge of Flutter_bloc, I will improve the Starter project to make it easy for everyone to follow my rhythm and thinking. Even if you have not learned Flutter_bloc, you only need to have the basic knowledge of Flutter & Dart. Hopefully I can achieve this goal.

1. What is flutter_bloc? How does it work?

The goal of the library is to facilitate testability and reusability by making it easy to separate presentation from business logic. A predictable and controlled state library to implement a design pattern for processing inter-component business logic (BLoC). The graph below succinctly illustrates what The Flutter_bloc did.

Also, we use flutter_bloc to implement the following counter example from the Flutter website.

A. Home Page We complete the main.dart file and we create a CounterApp Widget.

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';

import 'app.dart';
import 'counter_observer.dart';

void main() {
  runApp(const CounterApp());
}
Copy the code

Let’s implement CounterApp.

B. CounterApp is a MaterialApp and specifies Home as CounterPage.

import 'package:flutter/material.dart';

import 'counter/counter.dart';

/// {@template counter_app}
/// A [MaterialApp] which sets the `home` to [CounterPage].
/// {@endtemplate}
class CounterApp extends MaterialApp {
  /// {@macro counter_app}
  const CounterApp({Key? key}) : super(key: key, home: const CounterPage());
}
Copy the code

Let’s implement CounterPage.

C. The task of a CounterPage is to create a CounterCubit (which we will cover next) and provide a CounterVIew as its child. What is a Cubit? Cubit.

import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '.. /counter.dart'; import 'counter_view.dart'; /// {@template counter_page} /// A [StatelessWidget] which is responsible for providing a /// [CounterCubit] instance to  the [CounterView]. /// {@endtemplate} class CounterPage extends StatelessWidget { /// {@macro counter_page} const CounterPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterCubit(), child: CounterView(), ); }}Copy the code

D. Counter Cubit.

Counter Cubit will expose two methods: increment – Current state + 1; Decrement: Indicates the current state state-1.

Here we define the state as an int and set the initial state to 0.

import 'package:bloc/bloc.dart';

/// {@template counter_cubit}
/// A [Cubit] which manages an [int] as its state.
/// {@endtemplate}
class CounterCubit extends Cubit<int> {
  /// {@macro counter_cubit}
  CounterCubit() : super(0);

  /// Add 1 to the current state.
  void increment() => emit(state + 1);

  /// Subtract 1 from the current state.
  void decrement() => emit(state - 1);
}
Copy the code

Next, let’s look at the CounterView, which is responsible for consuming the aforementioned state and interacting with the CounterCubit.

e. Counter View

CounterView is responsible for the UI rendering of the current count and contains two FloatingActionButtons for increment/ Decrement counter.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../counter.dart';

/// {@template counter_view}
/// A [StatelessWidget] which reacts to the provided
/// [CounterCubit] state and notifies it in response to user input.
/// {@endtemplate}
class CounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Text('$state', style: textTheme.headline2);
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            key: const Key('counterView_increment_floatingActionButton'),
            child: const Icon(Icons.add),
            onPressed: () => context.read<CounterCubit>().increment(),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            key: const Key('counterView_decrement_floatingActionButton'),
            child: const Icon(Icons.remove),
            onPressed: () => context.read<CounterCubit>().decrement(),
          ),
        ],
      ),
    );
  }
}
Copy the code

Note that the BlocBuilder is used to wrap the Text Widget in order to update the Text value synchronously when the state of the CounterCubit changes. In addition, context.read() is used to get the latest instance of the CounterCubit.

All right, you’re done! We decoupled the UI presentation layer from the business logic layer. The UI presentation layer doesn’t know anything except that it triggers some event, and it doesn’t know what the event actually does. The business logic is completely decoupled by the CounterCubit. Let’s get straight to our topic, the Flutter_bloc Boilerplate project!

2. Project introduction & Structure.

This project mainly completes the following functions:

A. Splash page, similar to the welcome page, is mainly used to determine whether the user login has expired. If it has expired, enter the login & Registration selection page; otherwise, enter the Home page directly.

B. Login & Registration page: If there are existing users, click to enter the login page; if there are no users, enter the registration page to register users;

C. Home page: the page entered after successful login, which contains the bottom multi-tab page of general mobile app. For example, the bottom of the boring APP is similar. The current Starter project home TAB displays user list and ME TAB displays single user information, while other tabs are temporarily a placeholder.

Post a screenshot of the finished project below:

The basic folder structure of the project is as follows, which is convenient for everyone to be familiar with the project as a whole. This structure is also gradually summarized by me in my own projects.

Lib / | - API - global Restful API requests, including request interceptor | - interceptor - interceptor, Including the request, response, err intercept | - API. The dart - Restful API export file | | - blocs - BLoC processing business logic - auth - auth login module processing & registration | - home - such as business logic Home module loading user information such as | - blocs. Dart - BLoC export file | - models - various structural entity class, Divided into two types, the request and the response entity | - models. The dart - entity class export file folder | | - modules - business module - auth - login & registration module | | - splash - - home - homepage module Splash module | - modules. The dart routing module - module export file | - routes - | | - modules - each module of the routing configuration information - i_app_route. Dart - abstract routing classes | - App_routes. Dart - routing name | - app_routes_factory. Dart - routing factory class, Processing a variety of routing configuration module | - route_path. Dart - the name of the routing static class | - routes. The dart - routing export file | - Shared - global Shared folder, Including static variables, global services, utils, global Widget | - Shared. Dart - global Shared export file - theme folder | | - theme - app. Dart - | - the main global app file. The dart - Main entry fileCopy the code

Ok, so let’s start coding.

3. API module construction.

Unlike the GetX state management framework, which provides GetConnect to help classes implement their own httpClient, Flutter_bloc doesn’t have a Restful API library package, so here we use Dio, one of the most popular HTTP wrapped libraries.

First, we implemented a simple API Provider class. We made the provider a singleton so that we would need the new instance several times later, but we didn’t need to generate a new one each time, because our Dio only instantiated in this one. The following code snippet will make your class singleton.

static final ApiProvider _singleton = new ApiProvider._internal(); static final dio = Dio(); factory ApiProvider() { return _singleton; } ApiProvider._internal() { dio .. options.baseUrl = 'https://reqres.in' .. options.receiveTimeout = 15000 .. options.responseType = ResponseType.json .. interceptors.add(ApiInterceptors()) .. interceptors.add(LogInterceptor( request: true, requestBody: true, responseBody: true, requestHeader: true, )); }Copy the code

The complete code for api_provider.dart is shown below, simply using GET and POST to invoke several interfaces.

import 'package:dio/dio.dart'; import 'package:flutter_bloc_boilerplate/models/models.dart'; import 'interceptor/api_interceptor.dart'; class ApiProvider { static final ApiProvider _singleton = new ApiProvider._internal(); static final dio = Dio(); factory ApiProvider() { return _singleton; } ApiProvider._internal() { dio .. options.baseUrl = 'https://reqres.in' .. options.receiveTimeout = 15000 .. options.responseType = ResponseType.json .. interceptors.add(ApiInterceptors()) .. interceptors.add(LogInterceptor( request: true, requestBody: true, responseBody: true, requestHeader: true, )); } Future<Response> login(String path, LoginRequest data) { return dio.post(path, data: data); } Future<Response> register(String path, RegisterRequest data) { return dio.post(path, data: data); } Future<Response> getUsers(String path) { return dio.get(path); }}Copy the code

Dart encapsulates the provider by creating an instance of the injected API Provider. Here we see that when we use repository to invoke Restful apis, each time we create a new instance of the provider, But don’t worry, our API provider is singleton.

import 'dart:async';

import 'package:flutter_bloc_boilerplate/models/models.dart';
import 'package:flutter_bloc_boilerplate/models/response/users_response.dart';

import 'api.dart';

class ApiRepository {
  ApiRepository({this.apiProvider});

  final ApiProvider apiProvider;

  Future<LoginResponse> login(LoginRequest data) async {
    final res = await apiProvider.login('/api/login', data);
    if (res.statusCode == 200) {
      return LoginResponse.fromJson(res.data);
    }

    return Future.error(res.statusCode);
  }

  Future<RegisterResponse> register(RegisterRequest data) async {
    final res = await apiProvider.register('/api/register', data);
    if (res.statusCode == 200) {
      return RegisterResponse.fromJson(res.data);
    }

    return Future.error(res.statusCode);
  }

  Future<UsersResponse> getUsers() async {
    final res = await apiProvider.getUsers('/api/users?page=1&per_page=12');
    if (res.statusCode == 200) {
      return UsersResponse.fromJson(res.data);
    }

    return Future.error(res.statusCode);
  }
}
Copy the code

Dio provides an InterceptorsWrapper for modifying request Headers, storing tokens, modifying Response, etc. We simply implement the onRequest, onResponse and onError methods. As you can see from the following code, we have also added loading when calling the interface.

import 'package:dio/dio.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'loading_apis.dart'; class ApiInterceptors extends InterceptorsWrapper { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { print("REQUEST[${options?.method}] => PATH: ${options? .path}"); // var prefs = await SharedPreferences.getInstance(); // var accessToken = prefs.getString('access_token'); // if (accessToken ! = null) { // options.headers.putIfAbsent('Authorization', () => 'Bearer $accessToken'); // } if (isInLoadingApis(options? .path)) { EasyLoading.show(status: 'loading... '); } handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) async { print( "RESPONSE[${response?.statusCode}] => PATH: ${response? .realUri? .path}"); // _refreshAccessToken(response); if (isInLoadingApis(response? .realUri? .path)) { EasyLoading.dismiss(); } handler.next(response); } @override void onError(DioError err, ErrorInterceptorHandler handler) { print( "ERROR[${err?.response?.statusCode}] => PATH: ${err? .requestOptions? .path}"); if (isInLoadingApis(err? .requestOptions? .path) || err? .response == null) { EasyLoading.dismiss(); } if (err? .response? .statusCode == 401) { print('TODO: go to auth page'); EasyLoading.dismiss(); } handler.next(err); } // _refreshAccessToken(Response response) async { // var prefs = await SharedPreferences.getInstance(); // var accessToken = response.headers.map['access_token'] ! = null // ? response.headers.map['access_token'][0] // : null; // if (accessToken ! = null) { // prefs.setString('access_token', accessToken); //} //Copy the code

4. Route Route module.

Anonymous Route, Named Route and Generated Route are commonly used in Flutter projects. We use Generated Route here because it is very convenient and easy to use.

Here are the three routes:

A. An Anonymous Route is pushed directly into the navigator, like the following, so that every time you want to enter the Route, you have to write it again.

Navigator.of(context).push(
  MaterialPageRoute<CounterPage>(
    builder: CounterPage(),
  ),
);
Copy the code

The MaterialApp contains a parameter routes, which can define a key/value pair object:

return MaterialApp(
  title: 'Flutter Demo',
  routes: {
    '/': (context) => BlocProvider.value(
          value: _counterBloc,
          child: HomePage(),
        ),
    '/counter': (context) => BlocProvider.value(
          value: _counterBloc,
          child: CounterPage(),
        ),
  },
);
Copy the code

Then we can access the route as follows. Isn’t that much easier?

Navigator.of(context).pushNamed('/counter')
Copy the code

C. Generated Route, Generated Route, the same MaterialApp has another parameter onGenerateRoute, passed in a method, through this method to build Route, can be expected to provide method, then we can do more things, This is what we call easy to use and easy to use, so we used this route.

Route onGenerateRoute(RouteSettings settings) { switch (settings.name) { case '/': return MaterialPageRoute( builder: (_) => BlocProvider.value( value: _counterBloc, child: HomePage(), ), ); case '/counter': return MaterialPageRoute( builder: (_) => BlocProvider.value( value: _counterBloc, child: CounterPage(), ), ); default: return null; }}Copy the code

Use time and named routing can, but we found a problem, as the project is more and more big, the switch will appear a lot of case, on the last method is more and more big, also began to chaos, so whether we have other ways to avoid this problem, the answer is yes, I thought of a maybe not the best, But I think it’s more comfortable to maintain than this, which is to use the factory method instead of the switch/case method.

How do we use this factory method? We can only simulate subrouting because the routes in a Flutter are associated with the page, and everything in a Flutter is nested with widgets. If you have subrouting, The page of the child route is included in the parent route, which does not have the nested style of Angular or Vue routes.

So we divide routes by modules. First we define a routing base class that distinguishes modules, so we define a variable names that contains all the routing names under this module, and a method associated with the onGenerateRoute method.

import 'package:flutter/material.dart';

abstract class IAppRoute {
  List<String> names;

  Route routes(RouteSettings settings);
}
Copy the code

Next, we implement the following Auth routing module, the Auth module contains auth, login and register3 pages.

import 'package:flutter/material.dart'; import 'package:flutter_bloc_boilerplate/modules/auth/auth.dart'; import 'package:flutter_bloc_boilerplate/modules/auth/register_screen.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; import 'i_app_route.dart'; class AuthRoutes implements IAppRoute { static final String key = RoutePath.auth; @override List<String> names = [RoutePath.auth, RoutePath.login, RoutePath.register]; @override Route routes(RouteSettings settings) { switch (settings.name) { case RoutePath.auth: return MaterialPageRoute( builder: (_) => AuthScreen(), ); case RoutePath.login: return MaterialPageRoute( builder: (_) => LoginScreen(), ); case RoutePath.register: return MaterialPageRoute( builder: (_) => RegisterScreen(), ); default: return MaterialPageRoute( builder: (_) => AuthScreen(), ); }}}Copy the code

We then create a route to the home module. Notice that we initialize a HomeBloc (more on that later), because the HomeBloc is only used in the home module, so our initialized instance of the Bloc is just inside this. In this way, all routes in the home module can obtain an instance of the HomeBloc via BlocProvider. Of

(context), without having to re-instantiate a new bloc.

import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_boilerplate/blocs/home/home.dart'; import 'package:flutter_bloc_boilerplate/modules/modules.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; import 'i_app_route.dart'; class HomeRoutes implements IAppRoute { static final String key = RoutePath.home; final HomeBloc _homeBloc = new HomeBloc(); @override List<String> names = [RoutePath.home]; @override Route routes(RouteSettings settings) { switch (settings.name) { case RoutePath.home: return MaterialPageRoute( builder: (_) => BlocProvider.value( value: _homeBloc, child: HomeScreen(), ), ); default: return MaterialPageRoute( builder: (_) => AuthScreen(), ); }}}Copy the code

Route module we defined, the rest of the route factory class, factory mode we need to define a Map to store the route of each module, so that when onGenerateRoute needs, we can use this Map to find the corresponding route module, so that we can jump to the corresponding page.

import 'package:flutter/material.dart'; import 'modules/i_app_route.dart'; class AppRoutesFactory { Map<String, IAppRoute> routesMap = new Map<String, IAppRoute>(); Route routes(RouteSettings settings) { return routesMap[settings.name].routes(settings); } void registerRoutes(String key, IAppRoute route) { if (! routesMap.containsKey(key)) { route.names.forEach((name) { routesMap[name] = route; }); }}}Copy the code

Next, we define an AppRoutes. Dart class that uses the factory class to pass in the Parameter onGenerateRoute to the MaterialApp. Here we also use the singleton pattern to ensure that there are no repeated instances of the factory class and only one routing factory instance in the entire app.

import 'package:flutter/material.dart'; import 'package:flutter_bloc_boilerplate/modules/modules.dart'; import 'package:flutter_bloc_boilerplate/routes/app_routes_factory.dart'; import 'package:flutter_bloc_boilerplate/routes/modules/home_routes.dart'; import 'package:flutter_bloc_boilerplate/routes/route_path.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; class AppRoutes { static final AppRoutes _singleton = new AppRoutes._internal(); factory AppRoutes() { return _singleton; } AppRoutes._internal() { _appRoutesFactory = new AppRoutesFactory(); _appRoutesFactory.registerRoutes(AuthRoutes.key, new AuthRoutes()); _appRoutesFactory.registerRoutes(HomeRoutes.key, new HomeRoutes()); print('AppRoutes._internal()'); } AppRoutesFactory _appRoutesFactory; Route routes(RouteSettings settings) { if (settings.name == RoutePath.root) { return MaterialPageRoute( builder: (_) => SplashScreen(), ); } return _appRoutesFactory.routes(settings); }}Copy the code

Dart initializes an AppRoutes instance and passes _approuter.routes. Perfect!

import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_boilerplate/blocs/auth/auth.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; import 'package:flutter_bloc_boilerplate/theme/theme.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; class App extends StatefulWidget { @override _AppState createState() => _AppState(); } class _AppState extends State<App> { final _appRouter = AppRoutes(); @override Widget build(BuildContext context) { return BlocProvider( // AuthBloc act as a global bloc use create: (context) => AuthBloc(), child: MaterialApp( title: 'flutter flutter_bloc boilerplate', theme: ThemeConfig.lightTheme, onGenerateRoute: _appRouter.routes, builder: EasyLoading.init(), ), ); }}Copy the code

To sum up, the implementation of our routing module is complete. Later, when we add a new routing module, we only need to modify a file app_routes.dart to register the new routing module and add the new module. Each module should be in a separate file, not in switch/case. We cut the routing module, remember the home routing module up there? That’s right. We can also manage Bloc easily, so bloc can be managed globally as well as locally.

How do I use bloc in the Widgets tree and control the extent to which bloc is used? Reference.

5. Define BLoC – Business Logic of Component.

We wrote the API module, defined the route, and then started to write the business logic, which is the core of this time – Flutter_bloc. It is not difficult to see from the routing definition that we use two bloc, AuthBloc and HomeBloc. The former is a global bloc and is responsible for login, registration and logout related businesses. The latter handles the business processing of the home module after a successful login – loading the user list.

A. AuthBloc module. We introduced at the beginning that Bloc connects UI with Bloc through event and state, so our AuthBloc module includes auth_bloc. Dart, auth_event. There is also an Auth.dart export class.

First we define what an Auth event is, the default initialization event is one, then the registration event, login event, and logout event, so auth_Event. dart looks like this, where we inherit from the Equatable base class.

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc_boilerplate/models/models.dart';

abstract class AuthEvent extends Equatable {
  const AuthEvent();

  @override
  List<Object> get props => [];
}

class AuthAppInitEvent extends AuthEvent {}

class AuthRegisterEvent extends AuthEvent {
  final RegisterRequest registerRequest;

  const AuthRegisterEvent({@required this.registerRequest});

  @override
  List<Object> get props => [registerRequest];
}

class AuthLoginEvent extends AuthEvent {
  final LoginRequest loginRequest;

  const AuthLoginEvent({@required this.loginRequest});

  @override
  List<Object> get props => [loginRequest];
}

class AuthSignoutEvent extends AuthEvent {}
Copy the code

We need to define pending states for each event. In general, we need to define two states for each event, one successful state and one failed state. After all, the request API can either succeed or fail, but it doesn’t have to be two. Loading state, depending on your business needs. Dart: Notice that some of our states also have parameter variables, which are sent to THE UI for use, and the event above also has parameter variables that the UI sends to bloc. As simple as that, state and event build a two-way bridge between Bloc and UI.

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

abstract class AuthState extends Equatable {
  const AuthState();

  @override
  List<Object> get props => [];
}

class AuthInitState extends AuthState {}

class AuthSuccessState extends AuthState {}

class AuthFailState extends AuthState {}

class AuthRegisterSuccessState extends AuthState {}

class AuthRegisterFailState extends AuthState {
  final String message;

  AuthRegisterFailState({@required this.message});

  @override
  List<Object> get props => [message];
}

class AuthLoginSuccessState extends AuthState {}

class AuthLoginFailState extends AuthState {
  final String message;

  AuthLoginFailState({@required this.message});

  @override
  List<Object> get props => [message];
}

class AuthSignoutState extends AuthState {}

class AuthAppFailureState extends AuthState {
  final String message;

  AuthAppFailureState({@required this.message});

  @override
  List<Object> get props => [message];
}
Copy the code

When it’s time to write bloc, we separately process all the defined events and the state returned to the UI after processing those events. The AuthBloc class inherits the Bloc base class, passing in AuthEvent and AuthState. Then we override its mapEventToState method. The function of this method is to process the incoming event, and then according to the corresponding event and the parameters brought by the event, Return the corresponding state to the UI. Async & await & yield* Async & await & yield* Async The specific business logic in auth_bloc. Dart is written in the code itself and won’t be described here.

import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_boilerplate/api/api.dart'; import 'package:flutter_bloc_boilerplate/shared/shared.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'auth.dart'; class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc() : super(AuthInitState()); final ApiRepository _apiRepository = ApiRepository(apiProvider: new ApiProvider()); @override Stream<AuthState> mapEventToState(AuthEvent event) async* { if (event is AuthAppInitEvent) { yield* _mapAuthAppInitState(event); } if (event is AuthRegisterEvent) { yield* _mapAuthRegisterState(event); } if (event is AuthLoginEvent) { yield* _mapAuthLoginState(event); } if (event is AuthSignoutEvent) { yield* _mapAuthSignoutState(event); } } Stream<AuthState> _mapAuthAppInitState(AuthAppInitEvent event) async* { try { await Future.delayed(Duration(milliseconds: 2000)); // a simulated delay final SharedPreferences sharedPreferences = await prefs; if (sharedPreferences.getString(StorageConstants.token) ! = null) { yield AuthSuccessState(); } else { yield AuthFailState(); } } catch (e) { yield AuthAppFailureState( message: e.message ?? 'An unknown error occurred'); } } Stream<AuthState> _mapAuthRegisterState(AuthRegisterEvent event) async* { try { final SharedPreferences sharedPreferences = await prefs; final res = await _apiRepository.register(event.registerRequest); if (res.token.isNotEmpty) { sharedPreferences.setString(StorageConstants.token, res.token); yield AuthRegisterSuccessState(); } else { yield AuthRegisterFailState(message: 'AuthRegisterFailState'); } } catch (e) { yield AuthAppFailureState( message: e.message ?? 'An unknown error occurred'); } } Stream<AuthState> _mapAuthLoginState(AuthLoginEvent event) async* { try { final SharedPreferences sharedPreferences = await prefs; final res = await _apiRepository.login(event.loginRequest); if (res.token.isNotEmpty) { sharedPreferences.setString(StorageConstants.token, res.token); yield AuthLoginSuccessState(); } else { yield AuthLoginFailState(message: 'AuthLoginFailState'); } } catch (e) { yield AuthAppFailureState( message: e.message ?? 'An unknown error occurred'); } } Stream<AuthState> _mapAuthSignoutState(AuthSignoutEvent event) async* { try { final SharedPreferences sharedPreferences = await prefs; sharedPreferences.clear(); yield AuthSignoutState(); } catch (e) { yield AuthAppFailureState( message: e.message ?? 'An unknown error occurred'); }}}Copy the code

We have finished writing the AuthBloc module. The HomeBloc module is almost the same with minor differences. Please refer to the source code given at the end of the article.

6. UI interaction.

When it comes to our UI link, UI is the simplest link as long as we have the foundation of Flutter and use it in combination with Flutter_bloc. In our project, we only need to know a few widgets in Flutter_bloc. Which ones are there? Let’s take a look at each of them.

A. BlocProvider, we can use it to create a new Bloc, or we can use it to get the latest instance of a bloc that has been created.

Create a new Bloc – Remember our AuthBloc is a global bloc? Yes, the global Bloc needs to be created in the companion node of the Widget tree to achieve the effect of the global Bloc. The bloc instance can be used in both the widget that creates the bloc and its child widgets, so we can create the AuthBloc in app.dart. Dart build method, create an AuthBloc instance, and pass the MaterialApp Widget in the Child argument. This allows our MaterialApp and its child widgets to use the AuthBloc instance.

import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_boilerplate/blocs/auth/auth.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; import 'package:flutter_bloc_boilerplate/theme/theme.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; class App extends StatefulWidget { @override _AppState createState() => _AppState(); } class _AppState extends State<App> { final _appRouter = AppRoutes(); @override Widget build(BuildContext context) { return BlocProvider( // AuthBloc act as a global bloc use create: (context) => AuthBloc(), child: MaterialApp( title: 'flutter flutter_bloc boilerplate', theme: ThemeConfig.lightTheme, onGenerateRoute: _appRouter.routes, builder: EasyLoading.init(), ), ); }}Copy the code

Get the AuthBloc instance created above, BlocProvider. Of

(context).

B. BlocListener is a widget. What is a widget used for? Yes, the UI. So what in Flutter_bloc needs to be listened on by the UI? Yes, the state state, which is the AuthState we’re using here. It’s easy to know how to use the code. From the login_screen.dart code below we can see that BlocListener we use 3 arguments, bloc passed to the AuthBloc instance we created earlier (note that we will not re-create AuthBloc here), The listener parameter is a method with two parameters, context and state. in this method, we can listen to what state is returned after the current event is triggered. After the state is returned, we need to perform operations, such as jump, toast message, etc. The last argument, child, is passed to the UI we need to render.

import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_boilerplate/blocs/auth/auth.dart'; import 'package:flutter_bloc_boilerplate/models/models.dart'; import 'package:flutter_bloc_boilerplate/routes/routes.dart'; import 'package:flutter_bloc_boilerplate/shared/shared.dart'; class LoginScreen extends StatelessWidget { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); @override Widget build(BuildContext context) { return BlocListener<AuthBloc, AuthState>( bloc: BlocProvider.of<AuthBloc>(context), listener: (context, state) { if (state is AuthLoginSuccessState) { Navigator.pushNamed(context, RoutePath.home); } if (state is AuthLoginFailState) { CommonWidget.toast(state.message); } }, child: _buildWidget(context), ); } Widget _buildWidget(BuildContext context) { return Stack( children: [ GradientBackground(), Scaffold( backgroundColor: Colors.transparent, appBar: CommonWidget.appBar( context, 'Sign In', Icons.arrow_back, Colors.white, ), body: Container( alignment: Alignment. Center, PADDING: EdgeInsets. Symmetric (Horizontal: 35.0), Child: _buildForms(context),),],); } Widget _buildForms(BuildContext context) { return Form( key: _formKey, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ InputField( controller: _emailController, keyboardType: TextInputType.text, labelText: 'Email address', placeholder: 'Enter Email Address', validator: (value) { if (!Regex.isEmail(value)) { return 'Email format error.'; } if (value.isEmpty) { return 'Email is required.';  } return null; }, ), CommonWidget.rowHeight(), InputField( controller: _passwordController, keyboardType: TextInputType.emailAddress, labelText: 'Password', placeholder: 'Enter Password', password: true, validator: (value) { if (value.isEmpty) { return 'Password is required.'; } if (value.length < 6 || value.length > 15) { return 'Password should be 6~15 characters'; } return null; }, ), CommonWidget.rowHeight(height: 80), BorderButton( text: 'Sign In', backgroundColor: Colors.white, onPressed: () { BlocProvider.of<AuthBloc>(context).add( AuthLoginEvent( loginRequest: LoginRequest( email: _emailController.text, password: _passwordController.text, ), ), ); }, ), ], ), ), ); }}Copy the code

In the above BlocListener, we monitored the state state change, so how did the state state come from? Of course, it was triggered by the event, as we said in the previous AuthBloc, how did the EVENT event be triggered in the UI? In the following code snippet, we first get the Bloc instance we created using BlocProvider and then call the Add method of bloc instance to add the event to be triggered. Here is the AuthLoginEvent event. At the same time, we can pass in the parameter email and password required by the login event (of course, the password needs to be encrypted and transmitted in the real project), so that the mapEventToState method in our Bloc will be executed, and then return the corresponding state state. BlocListener in UI will listen to this state. Perform the jump, Toast Message, etc., so our UI, Event/State, Bloc all work together seamlessly, perfect!

BlocProvider.of<AuthBloc>(context).add(
  AuthLoginEvent(
    loginRequest: LoginRequest(
      email: _emailController.text,
      password: _passwordController.text,
    ),
  ),
);
Copy the code

So far, the main function points of our project are all said. To sum up, we wrote API module, routing module, bloc module, and finally UI, and how UI and Bloc interact seamlessly through event/ State, and realize the purpose of decoupling UI and business through Bloc. In addition to these, there are shared modules, models modules and so on in our project, which have little to do with the use of Flutter_bloc. You can read the code by yourself. Finally on the source code, welcome to put forward comments and suggestions!

7. The source code:flutter_bloc_boilerplate.