Flutter - flutter_bloc状态管理

       继上一篇写了Flutter - GetX状态管理,会发现其实Flutter的状态管理的框架还是比较多的,用的比较多的有flutter_bloc、MobX、GetX等,今天我就来谈一谈我学习Flutter之后最早用的状态管理框架flutter_bloc,这个框架也是github上面star最多的了,截止目前6.9k+,可以看出大家对这个框架的认可度非常高。总得来说,我觉得flutter_bloc前期需要花一些时间学习他的一些基础知识,比如bloc、cubit、BlocProvider、BlocListener等等,知道他们分别在什么时候用?该怎么用?了解了这些之后,融会贯通,才能说入门了flutter_bloc!

       接下来我会写一个Starter项目,一边讲解flutter_bloc的基础知识,一边完善Starter项目,使得大家容易跟上我的节奏思路,即使没有学习过flutter_bloc,只要有Flutter & Dart的基础知识即可,希望我能够达到这个目标。

1. flutter_bloc是什么?怎么用?

       该库的目标是使表示与业务逻辑的分离变得容易,从而促进可测试性和可重用性。一个可预测并控制状态的库来实现 处理组件间业务逻辑(BLoC)的设计模式. 下面的一张图简洁的道出了flutter_bloc所要做的事情。

Flutter - flutter_bloc状态管理_第1张图片

同样的,我们使用flutter_bloc来实现以下Flutter官网的计数器的例子。

a. 首页我们完成main.dart文件,我们创建了一个CounterApp Widget。

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

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

void main() {
  runApp(const CounterApp());
}

下面我们来实现CounterApp。

b. CounterApp是一个MaterialApp,并且指定home为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());
}

下面我们来实现CounterPage。

c. CounterPage的任务是创建一个CounterCubit (接下来我们会说到),并且提供一个CounterVIew作为它的child。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(),
    );
  }
}

d. Counter Cubit 。

Counter Cubit将会暴露出两个方法:increment - 当前状态state + 1;decrement:当前状态state - 1。

这里的状态state我们定义为一个int类型,并且初始状态设置为0。

import 'package:bloc/bloc.dart';

/// {@template counter_cubit}
/// A [Cubit] which manages an [int] as its state.
/// {@endtemplate}
class CounterCubit extends Cubit {
  /// {@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);
}

接下来,我们看一下CounterView,它将要负责消费上述的state并且和CounterCubit进行交互。

e. Counter View

CounterView负责UI渲染出当前的count并且包含2个FloatingActionButtons用来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(
          builder: (context, state) {
            return Text('$state', style: textTheme.headline2);
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton(
            key: const Key('counterView_increment_floatingActionButton'),
            child: const Icon(Icons.add),
            onPressed: () => context.read().increment(),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            key: const Key('counterView_decrement_floatingActionButton'),
            child: const Icon(Icons.remove),
            onPressed: () => context.read().decrement(),
          ),
        ],
      ),
    );
  }
}

        注意,这里的BlocBuilder被用来包裹Text Widget是为了当CounterCubit的state改变时同步更新text的值。除此之外,context.read()是用来获取最新的CounterCubit的实例。

       好了,大功告成!我们解耦了UI展现层和业务逻辑层。UI展现层什么都不知道,只知道触发一些event,至于这个event到底后面干了什么完全不知道,这些业务逻辑完全由CounterCubit来负责,从而达到了解耦。接下来我们就直接进入我们的主题,flutter_bloc boilerplate项目!

2. 项目介绍 & 结构。

此项目主要完成以下功能点:

a. Splash页面,类似于欢迎页,主要功能是在此处判断用户登录是否过期,如过期则进入登录 & 注册选择页面,否则直接进入home页面;

b. 登录 & 注册页面:已有用户可点击进入登录页面,没有用户则进入注册页面注册用户;

c. home页面:登录成功后进入的页面,此页面包含一般移动端app的底部多tab页,例如掘进的app底部类似,当前Starter项目home tab展示用户列表和 me tab展示单个用户信息,其他tab暂时是一个placeholder。

下面贴上项目完成后的截图:

Flutter - flutter_bloc状态管理_第2张图片Flutter - flutter_bloc状态管理_第3张图片

Flutter - flutter_bloc状态管理_第4张图片Flutter - flutter_bloc状态管理_第5张图片

Flutter - flutter_bloc状态管理_第6张图片Flutter - flutter_bloc状态管理_第7张图片

       项目的基础文件夹结构如下,方便大家从整体上熟悉这个项目,这个结构也是我在自己做过的项目中慢慢的总结出来的。

lib/
|- api - 全局Restful api请求,包括请求拦截器等
   |- interceptor - 拦截器,包括request、response、err拦截
   |- api.dart - Restful api导出文件
|- blocs - BLoC处理业务逻辑
   |- auth - auth模块处理登录&注册等业务逻辑
   |- home - home模块加载用户信息等
   |- blocs.dart - BLoC导出文件
|- models - 各种结构化实体类,分为request和response两种类型的实体
   |- models.dart - 实体类导出文件
|- modules - 业务模块文件夹
   |- auth - 登录&注册模块
   |- home - 首页模块
   |- splash - splash模块
   |- modules.dart - 模块导出文件
|- routes - 路由模块
   |- modules - 每个模块的路由配置信息
      |- i_app_route.dart - 抽象路由类
   |- app_routes.dart - 路由名称
   |- app_routes_factory.dart - 路由工厂类,处理各种模块路由配置
   |- route_path.dart - 路由名称静态类
   |- routes.dart - 路由导出文件
|- Shared - 全局共享文件夹,包括静态变量、全局services、utils、全局Widget等
   |- shared.dart - 全局共享导出文件
|- theme - 主题文件夹
|- app.dart - 全局app文件
|- main.dart - main入口文件

好,接下来我们就开始码代码了。

3. api模块构建。

       不像GetX状态管理框架提供了GetConnect帮助类实现了自己的httpClient,flutter_bloc并没有Restful api相关的类库包,所以这里我们使用Dio,这个也是最受欢迎的http封装的库之一。

       首先我们实现了一个简单的api provider类,我们这里使得这个provider是单例的目的是为了后面我们需要多次new这个实例,但是我们并不需要每次new都产生一个新的,因为我们的Dio只在这个里面实例化的。下面这个代码片段就可以使得你的类单例化。

  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,
      ));
  }

api_provider.dart完整代码如下,简单的使用了get和post调用了几个接口。

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 login(String path, LoginRequest data) {
    return dio.post(path, data: data);
  }

  Future register(String path, RegisterRequest data) {
    return dio.post(path, data: data);
  }

  Future getUsers(String path) {
    return dio.get(path);
  }
}

       接下来我们创建api_repository.dart来封装一下provider,通过构造注入api provider实例,这里就会出现当我们使用repository来调用Restful api的时候,每次都会new一个provider实例,不过不用担心,我们的api provider是单例的~_~。

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 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 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 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);
  }
}

       那么,通过项目结构 & 介绍里面,我们还缺一个interceptor来处理一些类似于修改request header,存储token,修改response等的操作,这里Dio为我们提供了InterceptorsWrapper,我们只要实现里面的onRequest、onResponse和onError方法就可以了。从下面的代码看出,我们还添加了调用接口时loading的功能。

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);
  //   }
  // }
}

4. 路由Route模块。

       一般Flutter项目中使用的是Anonymous Route、Named Route和Generated Route,我们这里使用的是Generated Route,因为它非常方便、易用,就这么简单!

这里简单说一下这3种路由使用方法:

a. Anonymous Route,没有名称的路由,直接push到navigator中去,像下面这个样子,这样每次要进入这个路由都要重新写一次,比较麻烦。

Navigator.of(context).push(
  MaterialPageRoute(
    builder: CounterPage(),
  ),
);

b. Named Route,命名的路由,MaterialApp中有一个参数routes,可以定义一个键值对对象:

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

然后我们就可以通过如下方式访问此路由了,这样是不是方便了许多?

Navigator.of(context).pushNamed('/counter')

c. Generated Route,生成命名路由,同样的MaterialApp中还有另外一个参数onGenerateRoute,传入一个方法,通过这个方法构建路由,可以想象提供了方法,那么我们可以做的事情就多了,这也是我们说的易用同样也简单,所以我们采用了此种路由。

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;
  }
}

          使用的时候和命名路由一样即可,但是我们发现一个问题,随着项目越来越庞大,这里的switch就会出现很多很多的case,到了最后方法越来越大,也就开始混乱不堪,所以我们是否有其他办法来避免这个问题呢,答案是有的,我想到了一个也许不是最好的,但是自认为维护起来比这个舒服多了,那就是使用工厂方法来替代这个switch/case的写法。

       我们如何使用这个工厂方法?一般的web端是有路由/子路由这种情况的,但是Flutter这里不好实现这种东西,我们只能模拟子路由,因为Flutter中路由是和页面关联的,并且Flutter里所有的东西都是Widget嵌套的,如果有了子路由的情况,子路由所对应的页面就包含在父路由了,他没有类似Angular、Vue路由里的那种嵌套写法。

       所以我们就以模块为单位划分路由,首先定义一个路由基类,基类要能区分模块,所以我们定义一个变量names包含此模块下的所有路由名称,另外定义一个方法关联onGenerateRoute方法。

import 'package:flutter/material.dart';

abstract class IAppRoute {
  List names;

  Route routes(RouteSettings settings);
}

接着,我们实现以下auth路由模块,auth模块包含auth、login和register3个页面。

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 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(),
        );
    }
  }
}

       然后创建home模块的路由,注意到,这里初始化了一个HomeBloc(后面会详细说到),因为HomeBloc只会在home模块使用,所以我们初始化的bloc实例也就在这个里面即可,这样在home模块所有的路由都可以通过BlocProvider.of(context)来拿到HomeBloc的实例,而不需要重新实例化一个新的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 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(),
        );
    }
  }
}

路由模块我们定义好了,剩下路由工厂类了,工厂模式中我们需要定义一个Map来存储每个模块的路由,这样当onGenerateRoute需要的时候,我们可以通过这个Map找到对应的路由模块,从而跳转到对应的页面。

import 'package:flutter/material.dart';

import 'modules/i_app_route.dart';

class AppRoutesFactory {
  Map routesMap = new Map();

  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;
      });
    }
  }
}

        接下来,我们定义一个使用工厂类的AppRoutes.dart类,方便传入MaterialApp的参数onGenerateRoute,这里我们也使用了单例模式,保证不会重复实例化工厂类实例,整个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);
  }
}

最终在app.dart中,我们初始化一个AppRoutes实例,传入_appRouter.routes即可,完美!

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 {
  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(),
      ),
    );
  }
}

        总结一下,我们的路由模块实现完成,后面我们添加新的路由模块时,只需要修改一个文件app_routes.dart来注册新的路由模块和添加此新模块,每个模块在一个单独的文件里面,不要在switch/case中找了,另外一个好处就是,我们切割了路由模块,还记得上面的home路由模块吗?对的,我们同时也方便管理bloc了,使得bloc可以全局管理也可以局部使用。

       如何在Widgets树中使用bloc以及控制bloc的使用范围?参考。

5. 定义BLoC - Business Logic of Component。

       我们写完了api模块,定义好了路由,接下来开始写业务逻辑,也就是这次的核心内容 - flutter_bloc。不难看出从路由定义中,我们使用了2个bloc,AuthBloc和HomeBloc,前者是一个全局的bloc,负责登录、注册、登出相关的业务;后者负责处理登录成功后home模块的业务处理 - 加载用户列表。

a. AuthBloc模块,开始我们就介绍了bloc是通过event和state来衔接UI和bloc的,所以我们的AuthBloc模块包含auth_bloc.dart、auth_event.dart、auth_state.dart,另外还有一个auth.dart导出类。

      首先我们定义auth event有哪些,初始化事件默认给一个,接着注册事件、登录事件和登出事件,所以auth_event.dart长成下面这个样子,这里我们继承了Equatable基类。

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 get props => [];
}

class AuthAppInitEvent extends AuthEvent {}

class AuthRegisterEvent extends AuthEvent {
  final RegisterRequest registerRequest;

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

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

class AuthLoginEvent extends AuthEvent {
  final LoginRequest loginRequest;

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

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

class AuthSignoutEvent extends AuthEvent {} 
  

       有了event,我们需要定义相应的state,一般情况下,一个event定义2个相应的state即可,一个成功状态,一个失败状态,毕竟请求api接口要么成功要么失败,当然不一定非要是2个,你也可以定义pending state,loading state,根据你业务的需要随便你喜欢。auth_state.dart如下:注意到我们有的state里面也有参数变量,这个是给传给UI使用的,同样上面的event也有参数变量是UI传给bloc的,就这么简单,state和event搭建了bloc和UI的一个双向桥梁。

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

abstract class AuthState extends Equatable {
  const AuthState();

  @override
  List 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 get props => [message];
}

class AuthLoginSuccessState extends AuthState {}

class AuthLoginFailState extends AuthState {
  final String message;

  AuthLoginFailState({@required this.message});

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

class AuthSignoutState extends AuthState {}

class AuthAppFailureState extends AuthState {
  final String message;

  AuthAppFailureState({@required this.message});

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

       到了编写bloc的时候了,我们分别处理所有定义的事件以及处理这些事件之后返回给UI的state状态。AuthBloc类继承了Bloc基类,传入了AuthEvent、AuthState这2个“桥梁”,然后我们重载他的mapEventToState方法,这个方法的作用就是处理传入的event事件,然后根据相应的事件以及事件带入的参数,返回对应的状态给UI即可。这里需要掌握的知识点async & await & yield*异步编程,参考。auth_bloc.dart中具体的业务逻辑自行看代码,这里就不赘述了。

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 {
  AuthBloc() : super(AuthInitState());

  final ApiRepository _apiRepository =
      ApiRepository(apiProvider: new ApiProvider());

  @override
  Stream 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 _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 _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 _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 _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');
    }
  }
}

AuthBloc模块我们编写完毕,HomeBloc模块也是大同小异,可参考文章结尾给的源码即可。

6. UI交互。

       到了我们的UI环节,只要有Flutter的基础,UI是最简单的一个环节,和flutter_bloc联合使用,在我们的这个项目中我们只需要知道flutter_bloc中的几个Widget就完全没问题了,哪几个?下面逐一介绍。

a. BlocProvider,我们可以用它来创建一个新的bloc,也可以通过他来获取最新的已经创建的bloc实例。

       创建新bloc --- 还记得我们的AuthBloc是一个全局的bloc吗?是的,全局bloc需要在widget tree的跟节点创建bloc,这样才能达到全局bloc的效果,因为bloc实例可以在创建bloc的widget以及它的子widget中都可以使用,所以我们在app.dart中创建AuthBloc即可。可以看到我们在app.dart的build方法中直接返回了BlocProvider这个widget,在它的create参数中创建AuthBloc实例,child参数中传入MaterialApp widget,这样我们的MaterialApp以及它的子widget都可以使用AuthBloc实例了。

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 {
  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(),
      ),
    );
  }
}

        获取上面创建的AuthBloc实例,BlocProvider.of(context),对,就这么简单的一行代码就可以返回AuthBloc实例了。

b. BlocListener,看名称我们猜测它是跟监听相关的,而且BlocListener是一个widget,widget用在哪里?对,UI。那么flutter_bloc中什么需要被UI监听?对,状态state,也就是这里使用的AuthState。上代码我们就很容易知道如何使用的了。从下面的login_screen.dart的代码中我们看到BlocListener我们使用了3个参数,bloc传入我们之前创建的AuthBloc实例(注意,这里我们不会再重新创建AuthBloc了),listener参数是一个方法携带context和state2个参数,我们就可以在这个方法中监听当前事件触发之后返回的是什么状态了,返回状态之后我们需要执行的操作,比如跳转、toast message等,最后一个参数child传入我们需要渲染的UI即可。

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 _formKey = GlobalKey();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocListener(
      bloc: BlocProvider.of(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(context).add(
                  AuthLoginEvent(
                    loginRequest: LoginRequest(
                      email: _emailController.text,
                      password: _passwordController.text,
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

       上面的BlocListener中我们监听了状态state变化,那么这个状态state是怎么来的,当然是event触发的,我们前面在说AuthBloc中就说过了,UI中如何触发event事件?很简单,下方代码片段中,我们首先通过BlocProvider拿到我们之前创建的bloc实例,接着调用bloc实例的add方法添加需要触发的event,这里是AuthLoginEvent事件,同时传入登录事件需要的参数邮箱和密码(当然真实项目中密码是需要加密传输的)即可,这样我们的bloc中的mapEventToState方法就会执行,然后返回对应的状态state,UI中的BlocListener监听到这个状态,执行跳转、toast message等操作,这样我们的UI、event/state、bloc就无缝衔接上了,完美!

BlocProvider.of(context).add(
  AuthLoginEvent(
    loginRequest: LoginRequest(
      email: _emailController.text,
      password: _passwordController.text,
    ),
  ),
);

       至此,我们项目的主要功能点全部说完了,总结一下,我们编写了api模块、路由模块、接着bloc模块,最后编写了UI,以及UI、bloc是如何通过event/state来交互的天衣无缝的,同时也达到了UI与业务通过bloc达到了解耦的目的。除了这些我们的项目中还有shared模块,models模块等等,这些与flutter_bloc的使用关系不大,大家自行看代码就好了。最后上源码,欢迎大家提出意见和建议!

7. 源码:flutter_bloc_boilerplate。

你可能感兴趣的:(前端,flutter,flutter,dart,flutter_bloc,bloc,cubit)