原文链接:Flutter bloc for beginners - 原文作者 Ana Polo

本文采用意译的方式

Flutter Bloc 是什么?

flutter BlocFlutter 应用的其中一个状态管理。我们可以通过它很容易处理应用中所有可能的状态。

Flutter Bloc 很容易使用,因为我们和我们团队可以很快明白相关的概念,不管你是什么水平,该库有非常好的文档和很多的案例,它在 Flutter 社区中是广泛使用的那个,所以我们如果有任何问题,我们都可以在网络上通过简单的搜索找到对应的解决方案。

它很强大,因为它可以帮助你创建所有类型的应用,比如,你可以创建以学习为目的的应用,或者创建在生产环境中使用的复杂的应用,Flutter Bloc 都可以应用。

这个库另一个很重要的方面是,它可以帮助我们很容易测试 Bloc 的逻辑。

更多的内容,我们可以直接到官网上查看:bloclibrary.dev/#/gettingst…。

我们怎么开始 Flutter Bloc?

首先,我们应该通过官方文档,阅读相关的基础内容,在本文中,我们尝试解析这些基础点,如果需要深入了解,推荐去看官方文档。

它是怎么工作的?

当我们使用 Flutter Bloc,我们要在应用中创建事件触发交互,然后 Bloc 会发射 emit 请求数据,存在在 state 中,在真实的场景中,它会像这样:

  1. 用户点击按钮来获取游戏列表
  2. 事件被触发,然后它会告知 Bloc 用户想获取游戏列表
  3. Bloc 将会请求数据(比如从一个存储库,该存储库负责连接到 API 来获取数据)
  4. Bloc 有数据,它将决定数据是否成功,然后 emit 发射一个状态 state
  5. 视图 view 将监听所有 Bloc 发射 emit 成功的状态 state 并作出反应。比如,如果 Bloc 发射一个成功的状态,视图将根据返回的游戏列表重新构建,但是如果返回的状态是错误的,视图会根据错误信息或者我们要展示的其他内容来重新构建。

初学者的 Flutter bloc_ide

完美,现在,我们知道主要的概念,了解了 Flutter Bloc 是怎么工作!现在,是时候知道怎么去使用它。

假设我们想创建一个关于游戏的 Bloc 逻辑,我们需要下面三个类:

  • games_bloc.dart
  • games_state.dart
  • games_event.dart

正如你所看到的,我们将需要一个 bloc, 一个 state 和一个 event 类。在每个类中,我们将管理所需的信息,别担心,我们将会讲解它们,但是现在,我们先解析关于 bloc 挂件的基本概念。

Bloc Widgets

这个库提供了我们需要掌握所有可能类型的挂件,比如,添加一个事件,监听一个状态,发射一个状态,根据状态重新构建页面等等。

BlocProvider/MultiBlocProvider

BlocProviders 控制给其子挂件提供一个 bloc。在使用它之前,需要初始化 bloc

初学者的 Flutter bloc_API_02

如果我们需要不止一个 bloc,我们可以使用 MultiBlocProvider 来获取不同的 providers

初学者的 Flutter bloc_ide_03

BlocListener

这个挂件,我们可以监听 listenbloc 中发射 emit 出来的不同状态,并作出反应,比如,展示 snackbar,对话框,或者导航到另一个页面... 这个挂件不会重新构建视图,它只会监听。

初学者的 Flutter bloc_程序员_04

BlocBuilder

通过这个挂件,我们能够根据它们的状态重新构建我们的挂件。

初学者的 Flutter bloc_API_05

BlocConsumer

当我们需要控制 bloc 状态去重新构建挂件或者导航或者展示对话框等,BlocConsumer 这个挂件很有用。这个挂件有 listenerbuilder 函数,所以我们可以一起使用。

初学者的 Flutter bloc_ide_06

BlocSelector

这个挂件允许开发者基于当前 bloc 状态选择一个新的值指定更新。

初学者的 Flutter bloc_前端_07

这些解析都是高等级的,有很多使用它们的方式。更多的内容,我们应该查看官网。

我们了解这些后,下面可以应用到案例中 🙌

在真实项目中使用 Flutter Bloc

在这个项目中,我们将从 games API 消费数据,获取关于游戏的信息并在页面中展示出来。

API 我们选择的是 RAWG。为了使用它,我们需要创建一个 API Key

本文我们不会介绍存储库和服务部分,但是如果你感兴趣,可以参考文本的代码。

下面是我完成的应用效果。

初学者的 Flutter bloc_前端_08

该首页有不同的部分,我们看下。

Header

这是个简单的挂件,我们展示了两行文本和一个圆形的头像。

初学者的 Flutter bloc_前端_09

Category 挂件

展示通过调用 getGenres 方法 API 返回的不同的类型。这个挂件有四种可能的状态:

  • 成功:真实分类列表
  • 错误:展示错误信息
  • 加载:展示一个 CircularProgressIndicator 挂件
  • 选中:更改选中类别的大小和颜色

初学者的 Flutter bloc_前端_10

Game by category widget

通过 genres 额外参数调用 getGames 方法,展示 API 返回的过滤游戏数据。它有三个可能的状态:

  • 成功:通过分类展示游戏列表
  • 错误:展示错误信息
  • 加载:展示一个 CircularProgressIndicator 挂件

初学者的 Flutter bloc_前端_11

All games widget

不通过过滤获取游戏列表。这个挂件只有在它的 bloc 发射成功一个状态后才展示出来,它有三个状态:

  • 成功:展示游戏列表
  • 错误:展示一个错误信息
  • 加载:展示一个 CircularProgressIndicator 挂件

初学者的 Flutter bloc_前端_12

项目结构

这个案例中,我们创建下面代码结构:

初学者的 Flutter bloc_前端_13

正如我们在 home 挂件文件夹中所看到之前提及的那样。每个挂件都有自己的 bloc,所以这会更加干净和可维护。

首页

这个页面很重要,所以这里我们使用了两个 Bloc 挂件:

MultiBlocProviderRepositoryProvider

当页面被初始化后,这个页面中所有的 bloc 准备就绪,所以,我们需要做的是使用一个 RepositoryProvider 来包裹子挂件,以为所有的 bloc 提供一个存储库,而且,我们需要通过一个 MultiBlocProvider 来初始化所有的 bloc

// home_page_games.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/game_repository.dart';
import 'package:infogames/repository/service/game_service.dart';
import 'package:infogames/ui/home/pages/home_layout.dart';
import 'package:infogames/ui/home/widgets/category/category_barrel.dart';
import 'package:infogames/ui/home/widgets/all_games_list_widget/all_games_barrel.dart';
import 'package:infogames/ui/home/widgets/games_by_category/games_by_category.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.deepOrangeAccent,
      body: RepositoryProvider(
        create: (context) => GameRepository(service: GameService()),
        child: MultiBlocProvider(
          providers: [
            BlocProvider<AllGamesBloc>(
              create: (context) => AllGamesBloc(
                gameRepository: context.read<GameRepository>(),
              )..add(
                  GetGames(),
                ),
            ),
            BlocProvider<CategoryBloc>(
              create: (context) => CategoryBloc(
                gameRepository: context.read<GameRepository>(),
              )..add(
                  GetCategories(),
                ),
            ),
            BlocProvider<GamesByCategoryBloc>(
              create: (context) => GamesByCategoryBloc(
                gameRepository: context.read<GameRepository>(),
              ),
            ),
          ],
          child: HomeLayout(),
        ),
      ),
    );
  }
}

实际上,正如我们所看到的代码,在开始的时候添加两个 bloc 分别对应两个事件:

  • GetGames
  • GetCategories

这是其中一个方法 - 添加时间来通知它的 bloc 我们需要一些数据。至此,这个首页有三个 blocs 和两个事件触发。现在,我们看看首页布局 HomeLayout

HomeLayout

正如上面所提及,这个类有三个主要的挂件,包含视图的骨架。

// home_layout_games.dart
import 'package:flutter/material.dart';
import 'package:infogames/ui/home/widgets/all_games_widget/all_games_widget.dart';
import 'package:infogames/ui/home/widgets/category_widget/categories_widget.dart';
import 'package:infogames/ui/home/widgets/games_by_category_widget/games_by_category_widget.dart';
import 'package:infogames/ui/home/widgets/header_title/header_title.dart';
import 'package:infogames/ui/widgets/container_body.dart';

class HomeLayout extends StatelessWidget {
  const HomeLayout({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 80.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          HeaderTitle(),
          const SizedBox(height: 40.0),
          ContainerBody(
            children: [
              CategoriesWidget(),
              GamesByCategoryWidget(),
              AllGamesWidget(title: 'All games'),
            ],
          )
        ],
      ),
    );
  }
}

下一步是进一步看看这些挂件,我们从挂件 CateoriesWidget 开始。

CategoriesWidget

Category event

这里我们为这个挂件创建所有需要的事件。

  • GetCategories:获取分类的事件
  • SelectCategories:当分类被选中的事件
// category_event.dart
part of 'category_bloc.dart';

class CategoryEvent extents Equatable {
  @override
  List<Object?> get props => [];
}

class GetCategories extends CategoryEvent {}

class SelectCategory extends CategoryEvent {
  SelectCategory({
    required this.idSelected;
  });
  final int idSelected;

  @override
  List<Object?> get props => [idSelected];
}
Category state

这个类包含 bloc 能够发射的不同状态。我们尽量用简短和清晰的方式来处理视图中的所有可能。

我们使用 Equatable 库来比较 Dart 中不同的对象,如果你们不知道这些知识,我们推荐你阅读下 文档

// category_state.dart
part of 'category_bloc.dart';

enum CategoryStatus { initial, success, error, loading, selected }

extension CategoryStatusX on CategoryStatus {
  bool get isInitial => this == CategoryStatus.initial;
  bool get isSuccess => this == CategoryStatus.success;
  bool get isError => this == CategoryStatus.error;
  bool get isLoading => this == CategoryStatus.loading;
  bool get isSelected => this == CategoryStatus.selected;
}

class CategoryState extends Equatable {
  const CategoryState({
    this.status = CategoryStatus.initial,
    List<Genre>? categories,
    int idSelected = 0,
  })  : categories = categories ?? const [],
        idSelected = idSelected;

  final List<Genre> categories;
  final CategoryStatus status;
  final int idSelected;

  @override
  List<Object?> get props => [status, categories, idSelected];

  CategoryState copyWith({
    List<Genre>? categories,
    CategoryStatus? status,
    int? idSelected,
  }) {
    return CategoryState(
      categories: categories ?? this.categories,
      status: status ?? this.status,
      idSelected: idSelected ?? this.idSelected,
    );
  }
}
Category bloc

在这里,我们需要处理所有的事件。正如你所看到的,在下面 on<GetCatogories>(_mapGetCategoriesEventToState)on<SelectCategory>(_mapSelectCategoryEventToState) 这两行代码中,我们检查事件是否是一个或另一个以创建其方法。

  • mapGetCategoriesEventToState:这个方法调用一个存储库从 API 获取数据。当存储库返回数据或者抛出错误,bloc 会发射对应状态。
  • mapSelectCategoryEventToState:这个方法将发射状态,比如 selected
// category_bloc.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/game_repository.dart';
import 'package:infogames/repository/models/model_barrel.dart';

part 'category_event.dart';
part 'category_state.dart';

class CategoryBloc extends Bloc<CategoryEvent, CategoryState> {
  CategoryBloc({
    required this.gameRepository,
  }) : super(const CategoryState()) {
    on<GetCategories>(_mapGetCategoriesEventToState);
    on<SelectCategory>(_mapSelectCategoryEventToState);
  }
  final GameRepository gameRepository;

  void _mapGetCategoriesEventToState(
      GetCategories event, Emitter<CategoryState> emit) async {
    emit(state.copyWith(status: CategoryStatus.loading));
    try {
      final genres = await gameRepository.getGenres();
      emit(
        state.copyWith(
          status: CategoryStatus.success,
          categories: genres,
        ),
      );
    } catch (error, stacktrace) {
      print(stacktrace);
      emit(state.copyWith(status: CategoryStatus.error));
    }
  }

  void _mapSelectCategoryEventToState(
      SelectCategory event, Emitter<CategoryState> emit) async {
    emit(
      state.copyWith(
        status: CategoryStatus.selected,
        idSelected: event.idSelected,
      ),
    );
  }
}

至此,我们怎么检查页面中的状态呢?

嗯,当一个状态被发射,我们想要根据对应的数据重新构建视图。为了实现这个,在我们视图中添加了 BlocBuilder

在这个案例中,我们只想在当前状态成功后重新构建视图,所以我们使用 buildWhen() 来实现。

// categories_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/ui/home/widgets/category_widget/category_barrel.dart';

class CategoriesWidget extends StatelessWidget {
  const CategoriesWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CategoryBloc, CategoryState>(
      buildWhen: (previous, current) => current.status.isSuccess,
      builder: (context, state) {
        return CategoriesSuccessWidget();
      },
    );
  }
}

很棒,当这个挂件被展示出来,用户可以点击其中一个分类,当这个发生,我们将添加两个事件:

  • GetGamesByCategory:获取按类型过滤游戏。这将通过另一个 blocGameByCategoryBloc。我们后面将讲解这个 bloc
  • SelectCategory:更改视图中选中项的颜色和大小。我们将在同一个 blocCategoryBloc 中处理

下面是完整的类。

// categories_success_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/models/genre.dart';
import 'package:infogames/ui/home/widgets/category_widget/category_barrel.dart';
import 'package:infogames/ui/home/widgets/games_by_category_widget/games_by_category_barrel.dart';

class CategoriesSuccessWidget extends StatelessWidget {
  const CategoriesSuccessWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CategoryBloc, CategoryState>(
      builder: (context, state) {
        return SizedBox(
          height: MediaQuery.of(context).size.height * .15,
          child: ListView.separated(
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return CategoryItem(
                key: ValueKey('${state.categories[index].name}$index'),
                category: state.categories[index],
                callback: (Genre categorySelected) {
                  context.read<GamesByCategoryBloc>().add(
                        GetGamesByCategory(
                          idSelected: categorySelected.id,
                          categoryName: categorySelected.name ?? '',
                        ),
                      );
                  context.read<CategoryBloc>().add(
                        SelectCategory(
                          idSelected: categorySelected.id,
                        ),
                      );
                },
              );
            },
            scrollDirection: Axis.horizontal,
            separatorBuilder: (_, __) => SizedBox(
              width: 16.0,
            ),
            itemCount: state.categories.length,
          ),
        );
      },
    );
  }
}

state.isSelected 被改变的时候,我们看看视图发生了什么。

我们使用一个 BlocSelector 来控制这情形,当用户点击其中一个分类,事件将会被触发并且 bloc 将发射一个选中分类的 id 状态 isSelected,所以在 bloc selector 中,我们必须检查这些状态是 true 才能使用一个新的尺寸和颜色重新构建视图。

// category_item.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/models/genre.dart';
import 'package:infogames/ui/home/widgets/category_widget/category_barrel.dart';

typedef CategoryCLicked = Function(Genre categorySelected);

class CategoryItem extends StatelessWidget {
  const CategoryItem({
    Key? key,
    required this.category,
    required this.callback,
  }) : super(key: key);

  final Genre category;
  final CategoryCLicked callback;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => callback(category),
      child: BlocSelector<CategoryBloc, CategoryState, bool>(
        selector: (state) =>
            (state.status.isSelected && state.idSelected == category.id)
                ? true
                : false,
        builder: (context, state) {
          return Column(
            children: [
              AnimatedContainer(
                duration: const Duration(milliseconds: 400),
                curve: Curves.easeInOutCirc,
                padding: const EdgeInsets.symmetric(horizontal: 2.0),
                alignment: Alignment.center,
                height: state ? 70.0 : 60.0,
                width: state ? 70.0 : 60.0,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: state ? Colors.deepOrangeAccent : Colors.amberAccent,
                ),
                child: Icon(
                  Icons.gamepad_outlined,
                ),
              ),
              SizedBox(height: 4.0),
              Container(
                width: 60,
                child: Text(
                  category.name ?? '',
                  style: TextStyle(
                      fontSize: 10.0,
                      fontWeight: FontWeight.bold,
                      color: Colors.black87),
                  textAlign: TextAlign.center,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              )
            ],
          );
        },
      ),
    );
  }
}

GameByCategoryWidget

GameByCategoryEvent

这里,我们创建一个事件来获取所有通过分类过滤的游戏,并将分类名字作为列表的标题。

// games_by_category_event.dart
part of 'games_by_category_bloc.dart';

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

class GetGamesByCategory extends GamesByCategoryEvent {
  GetGamesByCategory({
    required this.idSelected,
    required this.categoryName,
  });
  final int idSelected;
  final String categoryName;

  @override
  List<Object?> get props => [idSelected, categoryName];
}
GameByCategoryState

和之前状态类相似,这里我们添加了一个扩展,来检查不同的状态和通过一个 copyWith 的方法来创建游戏列表的副本。

// games_by_category_state.dart
part of 'games_by_category_bloc.dart';

enum GamesByCategoryStatus { initial, success, error, loading }

extension GamesByCategoryStatusX on GamesByCategoryStatus {
  bool get isInitial => this == GamesByCategoryStatus.initial;
  bool get isSuccess => this == GamesByCategoryStatus.success;
  bool get isError => this == GamesByCategoryStatus.error;
  bool get isLoading => this == GamesByCategoryStatus.loading;
}

class GamesByCategoryState extends Equatable {
  const GamesByCategoryState({
    this.status = GamesByCategoryStatus.initial,
    List<Result>? games,
    String? categoryName,
  })  : games = games ?? const [],
        categoryName = categoryName ?? '';

  final List<Result> games;
  final GamesByCategoryStatus status;
  final String categoryName;

  @override
  List<Object?> get props => [status, games, categoryName];

  GamesByCategoryState copyWith({
    List<Result>? games,
    GamesByCategoryStatus? status,
    String? categoryName,
  }) {
    return GamesByCategoryState(
      games: games ?? this.games,
      status: status ?? this.status,
      categoryName: categoryName ?? this.categoryName,
    );
  }
}
GameByCategoryBloc

在这个 bloc 总,我们将通过方法 GetGamesByCategory 来调用存储库以获取满足该分类 id 的所有游戏数据。当存储库返回有效数据,bloc 将返回放射成功信息,比如状态或者一份列表的副本或者分类名字,相反的,如果结果无效,bloc 需要返回错误的状态。

// game_by_category_bloc.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/game_repository.dart';
import 'package:infogames/repository/models/model_barrel.dart';

part 'games_by_category_event.dart';
part 'games_by_category_state.dart';

class GamesByCategoryBloc
    extends Bloc<GamesByCategoryEvent, GamesByCategoryState> {
  GamesByCategoryBloc({
    required this.gameRepository,
  }) : super(const GamesByCategoryState()) {
    on<GetGamesByCategory>(_mapGetGamesByCategoryEventToState);
  }
  final GameRepository gameRepository;

  void _mapGetGamesByCategoryEventToState(
      GetGamesByCategory event, Emitter<GamesByCategoryState> emit) async {
    try {
      emit(state.copyWith(status: GamesByCategoryStatus.loading));

      final gamesByCategory =
          await gameRepository.getGamesByCategory(event.idSelected);
      emit(
        state.copyWith(
          status: GamesByCategoryStatus.success,
          games: gamesByCategory,
          categoryName: event.categoryName,
        ),
      );
    } catch (error) {
      emit(state.copyWith(status: GamesByCategoryStatus.error));
    }
  }
}

完美,下一步是在视图中检查状态并做出响应。

// games_by_category_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/ui/widgets/error_widget.dart';
import 'games_by_category_barrel.dart';

class GamesByCategoryWidget extends StatelessWidget {
  const GamesByCategoryWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<GamesByCategoryBloc, GamesByCategoryState>(
      builder: (context, state) {
        return state.status.isSuccess
            ? GameByCategorySuccessWidget(
                categoryName: state.categoryName,
                games: state.games,
              )
            : state.status.isLoading
                ? Center(
                    child: CircularProgressIndicator(),
                  )
                : state.status.isError
                    ? ErrorGameWidget()
                    : const SizedBox();
      },
    );
  }
}

正如所看到的那样,我们根据状态的三个选项来处理视图:

  • 错误:暂时公共的错误挂件
  • 加载:展示 CircularProgressIndicator
  • 成功:展示 GameByCategorySuccessWidget。这个挂件控制通过分类游戏列表的展示。下面是这个挂件:
// games_by_category_success_widget.dart
import 'package:flutter/material.dart';
import 'package:infogames/repository/models/result.dart';
import 'package:infogames/ui/home/widgets/games_by_category_widget/games_by_category_barrel.dart';

class GameByCategorySuccessWidget extends StatelessWidget {
  const GameByCategorySuccessWidget({
    Key? key,
    required this.categoryName,
    required this.games,
  }) : super(key: key);

  final String categoryName;
  final List<Result> games;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.only(
            left: 24.0,
            bottom: 16.0,
          ),
          child: Text(
            categoryName,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 18.0,
            ),
          ),
        ),
        Container(
          height: MediaQuery.of(context).size.height * .2,
          child: ListView.separated(
            padding: const EdgeInsets.only(
              left: 24.0,
              right: 24.0,
            ),
            scrollDirection: Axis.horizontal,
            itemBuilder: (context, index) {
              return GameByCategoryImage(
                name: games[index].name ?? 'No data',
                backgroundImage: games[index].backgroundImage ?? '',
              );
            },
            separatorBuilder: (_, __) => SizedBox(
              width: 25.0,
            ),
            itemCount: games.length,
          ),
        ),
      ],
    );
  }
}

最后关于首页布局的那部分,是 AllGamesWidget

AllGamesWidget

AllGamesEvent

我们创建一个从 API 获取所有游戏的事件。

// all_games_event.dart
part of 'all_games_bloc.dart';

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

class GetGames extends AllGamesEvent {
  @override
  List<Object?> get props => [];
}
AllGamesState

像之前那样,我们创建一个扩展来处理所有可能的状态,并且有一个 copyWith 方法来创建一个我们所需的对象副本。

// all_games_state.dart
part of 'all_games_bloc.dart';

enum AllGamesStatus { initial, success, error, loading }

extension AllGamesStatusX on AllGamesStatus {
  bool get isInitial => this == AllGamesStatus.initial;
  bool get isSuccess => this == AllGamesStatus.success;
  bool get isError => this == AllGamesStatus.error;
  bool get isLoading => this == AllGamesStatus.loading;
}

class AllGamesState extends Equatable {
  const AllGamesState({
    this.status = AllGamesStatus.initial,
    Game? games,
  }) : games = games ?? Game.empty;

  final Game games;
  final AllGamesStatus status;

  @override
  List<Object?> get props => [status, games];

  AllGamesState copyWith({
    Game? games,
    AllGamesStatus? status,
  }) {
    return AllGamesState(
      games: games ?? this.games,
      status: status ?? this.status,
    );
  }
}
AllGamesBloc

这里我们调用存储库,当有可用的数据的时候,bloc 发射一个游戏列表副本的成功值,相反的,如果存储库返回无效值,bloc 会发射一个错误的状态。

// all_games_bloc.dart
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/repository/game_repository.dart';
import 'package:infogames/repository/models/model_barrel.dart';

part 'all_games_event.dart';
part 'all_games_state.dart';

class AllGamesBloc extends Bloc<AllGamesEvent, AllGamesState> {
  AllGamesBloc({
    required this.gameRepository,
  }) : super(const AllGamesState()) {
    on<GetGames>(_mapGetGamesEventToState);
  }

  final GameRepository gameRepository;

  void _mapGetGamesEventToState(
      GetGames event, Emitter<AllGamesState> emit) async {
    try {
      emit(state.copyWith(status: AllGamesStatus.loading));
      final games = await gameRepository.getGames();
      emit(
        state.copyWith(
          status: AllGamesStatus.success,
          games: games,
        ),
      );
    } catch (error) {
      emit(state.copyWith(status: AllGamesStatus.error));
    }
  }
}
AllGamesWidget

下面是所有游戏的挂件。这里,我们有一个 BlocBuilder 基于状态来重新构建视图。

// all_games_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infogames/ui/home/widgets/all_games_widget/all_games_barrel.dart';
import 'package:infogames/ui/widgets/error_widget.dart';

class AllGamesWidget extends StatelessWidget {
  const AllGamesWidget({
    Key? key,
    required this.title,
  }) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AllGamesBloc, AllGamesState>(
      builder: (context, state) {
        return state.status.isSuccess
            ? AllGamesSuccessWidget(
                title: title,
                games: state.games.results,
              )
            : state.status.isLoading
                ? Center(
                    child: CircularProgressIndicator(),
                  )
                : state.status.isError
                    ? ErrorGameWidget()
                    : const SizedBox();
      },
    );
  }
}

这里有状态的三种类型:

  • 错误:展示公共的错误挂件
  • 加载:展示 CircularProgressIndicator 挂件
  • 成功:展示 AllGamesSuccessWidget。这个挂件控制展示游戏列表。下面是该挂件的内容:
// all_games_success_widget.dart
import 'package:flutter/material.dart';
import 'package:infogames/repository/models/result.dart';
import 'package:infogames/ui/home/widgets/all_games_widget/all_games_barrel.dart';

class AllGamesSuccessWidget extends StatelessWidget {
  const AllGamesSuccessWidget({
    Key? key,
    required this.games,
    required this.title,
  }) : super(key: key);

  final List<Result> games;
  final String title;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Padding(
          padding: const EdgeInsets.only(left: 24.0),
          child: Text(
            title,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 18.0,
            ),
          ),
        ),
        Container(
          height:
              ((100 * games.length) + MediaQuery.of(context).size.width) + 24.0,
          child: ListView.separated(
            physics: NeverScrollableScrollPhysics(),
            padding: const EdgeInsets.only(
              left: 24.0,
              right: 24.0,
              top: 24.0,
            ),
            itemBuilder: (context, index) {
              return AllGamesItem(
                game: games[index],
              );
            },
            separatorBuilder: (_, __) => SizedBox(
              height: 20.0,
            ),
            itemCount: games.length,
          ),
        ),
      ],
    );
  }
}

额外内容 🙌

如果我们想要一个日志,来知道当前状态和下个我们添加的事件,我们需要一个 Bloc Observer 类。我们应该创建这个类并在主类中初始化它。

// bloc_observer.dart
class AppBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    if (bloc is Cubit) print(change);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }
}

// main class

void main() async {
  BlocOverrides.runZoned(
    () => runApp(const MyApp()),
    blocObserver: AppBlocObserver(),
  );
}

总结

为我们的 Flutter 应用程序使用一个好的状态管理器是必要的。Flutter bloc 是一个很好的选择,正如你所看到的,它并不复杂并且很容易理解怎么使用它的核心概念。并且,它提供了很多方法来管理我们的视图和挂件。

个人观点,我们更喜欢创建小而美的 blocs 来使得我们的代码更加干净和可维护性,而不是使用大文件 bloc 来管理很多的事情,但是你的逻辑要求你那么做,你那么做会更好。

Github 仓库

该代码是开源的,我们可以通过 github.com/AnnaPS/info… 来获取。

感谢阅读 👏

初学者的 Flutter bloc_程序员_14