前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初学者的 Flutter bloc

初学者的 Flutter bloc

作者头像
Jimmy_is_jimmy
发布2024-05-07 09:17:00
770
发布2024-05-07 09:17:00
举报
文章被收录于专栏:call_me_Rcall_me_R

原文链接: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 是怎么工作!现在,是时候知道怎么去使用它。

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

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

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

Bloc Widgets

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

BlocProvider/MultiBlocProvider

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

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

BlocListener

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

BlocBuilder

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

BlocConsumer

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

BlocSelector

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

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

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

在真实项目中使用 Flutter Bloc

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

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

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

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

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

Header

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

Category 挂件

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

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

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

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

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

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

项目结构

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

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

首页

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

MultiBlocProviderRepositoryProvider

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

代码语言:javascript
复制
// 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

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

代码语言:javascript
复制
// 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:当分类被选中的事件
代码语言:javascript
复制
// 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 中不同的对象,如果你们不知道这些知识,我们推荐你阅读下 文档

代码语言:javascript
复制
// 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
代码语言:javascript
复制
// 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() 来实现。

代码语言:javascript
复制
// 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 中处理

下面是完整的类。

代码语言:javascript
复制
// 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 才能使用一个新的尺寸和颜色重新构建视图。

代码语言:javascript
复制
// 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

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

代码语言:javascript
复制
// 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 的方法来创建游戏列表的副本。

代码语言:javascript
复制
// 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 需要返回错误的状态。

代码语言:javascript
复制
// 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));
    }
  }
}

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

代码语言:javascript
复制
// 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。这个挂件控制通过分类游戏列表的展示。下面是这个挂件:
代码语言:javascript
复制
// 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 获取所有游戏的事件。

代码语言:javascript
复制
// 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 方法来创建一个我们所需的对象副本。

代码语言:javascript
复制
// 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 会发射一个错误的状态。

代码语言:javascript
复制
// 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 基于状态来重新构建视图。

代码语言:javascript
复制
// 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。这个挂件控制展示游戏列表。下面是该挂件的内容:
代码语言:javascript
复制
// 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 类。我们应该创建这个类并在主类中初始化它。

代码语言:javascript
复制
// 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… 来获取。

感谢阅读 ?

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Flutter Bloc 是什么?
  • 我们怎么开始 Flutter Bloc?
    • 它是怎么工作的?
      • Bloc Widgets
      • 在真实项目中使用 Flutter Bloc
        • Header
          • Category 挂件
            • Game by category widget
              • All games widget
              • 项目结构
                • 首页
                  • HomeLayout
                    • CategoriesWidget
                      • Category event
                      • Category state
                      • Category bloc
                    • GameByCategoryWidget
                      • GameByCategoryEvent
                      • GameByCategoryState
                      • GameByCategoryBloc
                    • AllGamesWidget
                      • AllGamesEvent
                      • AllGamesState
                      • AllGamesBloc
                      • AllGamesWidget
                  • 额外内容 ?
                  • 总结
                  • Github 仓库
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                  http://www.vxiaotou.com