原文链接:Flutter bloc for beginners - 原文作者 Ana Polo
本文采用意译的方式
Flutter Bloc 是什么?
flutter Bloc
是 Flutter
应用的其中一个状态管理。我们可以通过它很容易处理应用中所有可能的状态。
Flutter Bloc
很容易使用,因为我们和我们团队可以很快明白相关的概念,不管你是什么水平,该库有非常好的文档和很多的案例,它在 Flutter
社区中是广泛使用的那个,所以我们如果有任何问题,我们都可以在网络上通过简单的搜索找到对应的解决方案。
它很强大,因为它可以帮助你创建所有类型的应用,比如,你可以创建以学习为目的的应用,或者创建在生产环境中使用的复杂的应用,Flutter Bloc
都可以应用。
这个库另一个很重要的方面是,它可以帮助我们很容易测试 Bloc
的逻辑。
更多的内容,我们可以直接到官网上查看:bloclibrary.dev/#/gettingst…。
我们怎么开始 Flutter Bloc?
首先,我们应该通过官方文档,阅读相关的基础内容,在本文中,我们尝试解析这些基础点,如果需要深入了解,推荐去看官方文档。
它是怎么工作的?
当我们使用 Flutter Bloc
,我们要在应用中创建事件触发交互,然后 Bloc
会发射 emit
请求数据,存在在 state
中,在真实的场景中,它会像这样:
- 用户点击按钮来获取游戏列表
- 事件被触发,然后它会告知
Bloc
用户想获取游戏列表 Bloc
将会请求数据(比如从一个存储库,该存储库负责连接到API
来获取数据)- 当
Bloc
有数据,它将决定数据是否成功,然后emit
发射一个状态state
- 视图
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
这个挂件,我们可以监听 listen
从 bloc
中发射 emit
出来的不同状态,并作出反应,比如,展示 snackbar
,对话框,或者导航到另一个页面... 这个挂件不会重新构建视图,它只会监听。
BlocBuilder
通过这个挂件,我们能够根据它们的状态重新构建我们的挂件。
BlocConsumer
当我们需要控制 bloc
状态去重新构建挂件或者导航或者展示对话框等,BlocConsumer
这个挂件很有用。这个挂件有 listener
和 builder
函数,所以我们可以一起使用。
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
挂件:
MultiBlocProvider
和 RepositoryProvider
。
当页面被初始化后,这个页面中所有的 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:获取按类型过滤游戏。这将通过另一个
bloc
: GameByCategoryBloc。我们后面将讲解这个bloc
。 - SelectCategory:更改视图中选中项的颜色和大小。我们将在同一个
bloc
:CategoryBloc 中处理
下面是完整的类。
// 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… 来获取。
感谢阅读 👏