使用BLoC设计模式的Flutter状态管理

在开发Flutter应用程序时,您可能会遇到需要将一个大的UI组件分割成几个小的组件,以提高代码的可读性。对于多个组件,在它们之间实现有效的通信是至关重要的。所有的UI组件都应该在任何时候都知道应用程序的状态。这就是所谓的状态管理。

在Flutter中,你可以通过使用setState 来管理你的应用程序的状态。但是,虽然setState 可以成为你最好的朋友,但完全依赖它并不是一个好主意。在开发Flutter应用程序时,你还应该考虑许多其他因素,如架构、可扩展性、可读性、复杂性等。掌握一切需要一个有效的状态管理技术。

有无数的状态管理解决方案可用于Flutter,包括Provider、InheritedWidget和InheritedModel、Redux、BLoC、GetIt、MobX、Riverpod等。在本教程中,我们将重点介绍在Flutter中使用BLoC设计模式进行状态管理。我们将解释BLoC的含义,并演示如何用BLoC实现任何功能。

什么是BLoC?

业务逻辑组件(BLoC)允许你将业务逻辑与用户界面分开。用BLoC编写代码可以使测试的编写和重复使用更加容易。

简单地说,BLoC接受一个事件流,根据事件处理数据,并产生作为状态的输出。以下面这个简单的例子为例。

Process Events and Produces States

一旦旋转90°按钮被点击,RotateEvent ,就会被派发到BLoC,代表旋转的状态,即RotatedState ,就会被发射出来。三角形部件在收到来自BLoC的RotatedState ,就会进行自我旋转。同样,当点击 "将颜色改为红色"按钮时,圆形小组件也会改变其颜色。

由于BLoC处理旋转和改变颜色的操作,这两个操作可以在任何小组件上执行。这有利于代码的重用性。

重要的BLoC概念

在我们深入研究之前,让我们回顾一下BLoC的一些基本概念和术语,这样我们就会有一个共同的认识。

事件

事件告诉BLoC要做什么。一个事件可以从任何地方触发,例如从一个UI小部件。外部事件,如网络连接的变化、传感器读数的变化等,看起来像这样。

class RotateEvent {
  final double angle;

  const RotateEvent(this.angle);

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

复制代码

BLoC

BLoC是一个居中的人。所有的业务逻辑都位于BLoC文件内。它只是接受事件,执行逻辑,并输出状态。下面是它的样子。

class TransformationBloc
    extends Bloc<TransformationEvent, TransformationState> {
  TransformationBloc() : super(RotatedState(angle: 0);

  @override
  Stream<TransformationState> mapEventToState(
      TransformationEvent event) async* {
    if (event is RotateEvent) {
      yield RotatedState(angle: event.angle);
    }
  }
}

复制代码

状态

状态代表了要由任何部件处理的信息。一个widget根据状态来改变自己。

class RotatedState {
  final double angle;

  const RotatedState({@required this.angle});

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

复制代码

Cubit

Cubit是BLoC模式的一个更简单的版本。它消除了编写事件的需要。

Cubit暴露了直接的函数,这可以导致适当的状态。编写Cubit而不是BLoC也减少了模板代码,使代码更容易阅读。

这里有一个简单的例子。

class TransformCubit extends Cubit<TransformState> {
  TransformCubit() : super(RotatedState(angle: 0));

  void rotate(double angle) {
    emit(RotatedState(angle: angle));
  }

}

复制代码

在Flutter中用setState (无BLoC)管理状态

在我们强调用BLoC在Flutter中管理状态的好处之前,让我们来看看用setState 来代替状态管理的过程。

我们的例子Flutter应用程序将显示一个可用产品的列表。通过点击产品名称旁边的图标,可以将产品从购物车中添加或删除。购物车中的商品数量也会相应更新。

Cart Updating

通过setState ,整个UI被分解成三个类。

  1. home.dart 是容纳支架和AppBar的主文件。AppBar包含购物车图标小部件
  2. product_list.dart 显示产品的列表
  3. product_tile.dart 显示单个产品项目。

下面是它的外观。

UI Broken Into Three Classes

购物车中的项目列表从Home (顶部)一直传递到ProductTile (底部)部件,以检查某个项目是否退出了购物车。如果它是,购物车图标将被突出显示。

Cart Icon Highlighted

点击产品名称旁边的购物车图标,就可以将该商品添加到购物车中。在AppBar上刷新购物车图标的回调是由ProductTile (底部)到Home (顶部)。

Callback to Refresh Cart

的问题setState

Flutter中的setState 状态管理方法对于只有几个组件的简单应用来说效果不错。但对于更复杂的、具有深层部件树的实际Flutter应用程序,使用setState ,会导致以下问题。

  • 代码重复 - 数据必须从所有部件传递到底部部件,这使得代码难以阅读
  • 性能下降,因为将setState 提升到具有深层次结构的父部件而导致不必要的重绘

如何用BLoC在Flutter中管理状态

现在让我们用BLoC实现同样的功能。

首先,添加BLoC库

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_bloc: ^7.0.0

复制代码

接下来,创建并添加一个BLoC观察器。这可以帮助你确定已经发生的事件和状态的顺序,这对于调试应用程序是非常好的。

void main() {
  Bloc.observer = SimpleBlocObserver();
  runApp(MyApp());
}
import 'package:flutter_bloc/flutter_bloc.dart';

/// Custom [BlocObserver] which observes all bloc and cubit instances.
class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object event) {
    super.onEvent(bloc, event);
    print(event);
  }

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

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print(error);
    super.onError(bloc, error, stackTrace);
  }
}

复制代码

创建事件以从购物车项目列表中添加和删除产品。

import 'package:equatable/equatable.dart';

abstract class CartEvent extends Equatable {
  const CartEvent();

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

class AddProduct extends CartEvent {
  final int productIndex;
  const AddProduct(this.productIndex);
  @override
  List<Object> get props => [productIndex];
  @override
  String toString() => 'AddProduct { index: $productIndex }';
}

复制代码

现在,创建表示产品被添加和删除的状态。

import 'package:flutter/material.dart';

abstract class CartState {
  final List<int> cartItem;
  const CartState({@required this.cartItem});

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

class ProductAdded extends CartState {
  final List<int> cartItem;
  const ProductAdded({@required this.cartItem}) : super(cartItem: cartItem);

  @override
  List<Object> get props => [cartItem];
  @override
  String toString() => 'ProductAdded { todos: $cartItem }';
}

复制代码

编写业务逻辑,将产品添加和删除到cartItems ,并发出相应的状态。购物车中的实际物品列表是在BLoC级别维护的。

class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc() : super(ProductAdded(cartItem: []));

  final List<int> _cartItems = [];
  List<int> get items => _cartItems;

  @override
  Stream<CartState> mapEventToState(CartEvent event) async* {
    if (event is AddProduct) {
      _cartItems.add(event.productIndex);
      yield ProductAdded(cartItem: _cartItems);
    } else if (event is RemoveProduct) {
      _cartItems.remove(event.productIndex);
      yield ProductRemoved(cartItem: _cartItems);
    }
  }
}

复制代码

接下来,将脚手架部件包裹在 [BlocProvider](https://pub.dev/packages/flutter_bloc#blocprovider).

BlocProvider 是一个Flutter小组件,它使任何BLoC对它下面的整个小组件树可用。在我们的例子中,在Home (顶部)和ProductTile (底部)之间的任何widget都可以访问购物车,所以不需要将购物车数据从widget树的顶部传递到底部。

BlocProvider(
    create: (_) => CartBloc(),
    child: Scaffold(
      appBar: CartCounter(),
      body: ProductList(),
    ));

复制代码

将购物车图标和产品列表包裹在BlocBuilder 内。BlocBuilder 在收到BLoC的新状态时,只需重建其内部的widget。

// Cart icon
BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
  List<int> cartItem = cartState.cartItem;
  return Positioned(
    left: 30,
    child: Container(
      padding: EdgeInsets.all(5),
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(10),
          color: Colors.red),
      child: Text(
        '${cartItem.length}',
        style: TextStyle(fontWeight: FontWeight.bold),
      ),
    ),
  );
}),
//Product list
 BlocBuilder<CartBloc, CartState>(builder: (_, cartState) {
  List<int> cart = cartState.cartItem;
  return LayoutBuilder(builder: (context, constraints) {
    return GridView.builder(
      itemCount: 100,
      itemBuilder: (context, index) => ProductTile(
        itemNo: index,
        cart: cart,
      ),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: constraints.maxWidth > 700 ? 4 : 1,
        childAspectRatio: 5,
      ),
    );
  });
});

复制代码

注意:CartBlocBlocBuilder 只添加在两个地方,因为我们只想让这两个widget在CartBloc 发生变化时重建。这种只刷新需要的小部件的方法大大减少了不必要的重绘数量。

下一步是为添加和删除购物车中的物品向CartBloc 拍摄事件。BlocProvider.of<CartBloc>(context) 在widget树中找到最近的CartBloc 的实例并向其添加事件。

IconButton(
  key: Key('icon_$itemNo'),
  icon: cart.contains(itemNo)
      ? Icon(Icons.shopping_cart)
      : Icon(Icons.shopping_cart_outlined),
  onPressed: () {
    !cart.contains(itemNo)
        ? BlocProvider.of<CartBloc>(context).add(AddProduct(itemNo))
        : BlocProvider.of<CartBloc>(context).add(RemoveProduct(itemNo));
  },
)

复制代码

现在用BlocConsumer 替换BlocBuilderBlocConsumer 允许我们重建小组件并对状态做出反应。它应该只在你想重建小组件并执行一些动作时使用。

在我们的例子中,我们想刷新列表,并在有产品被添加或从购物车中删除时显示一个购物车。

BlocConsumer<CartBloc, CartState>(
listener: (context, state) { 
  Scaffold.of(context).showSnackBar(
    SnackBar(
      content: Text(
          state is ProductAdded ? 'Added to cart.' : 'Removed from cart.'),
      duration: Duration(seconds: 1),
    ),
  );
}, 
builder: (_, cartState) {
  List<int> cart = cartState.cartItem;
  return LayoutBuilder(builder: (context, constraints) {
    return GridView.builder();
  });
});

复制代码

另外,如果你想减少一些模板代码,而且状态的顺序对你来说并不重要,可以试试Cubit。下面是CartCubit 的样子。

class CartCubit extends Cubit<CartState> {
  CartCubit() : super(ProductAdded(cartItem: []));

  final List<int> _cartItems = [];
  List<int> get items => _cartItems;

  void add(int productIndex) {
    _cartItems.add(productIndex);
    emit (ProductAdded(cartItem: _cartItems));
  }

  void remove(int productIndex) {
    _cartItems.remove(productIndex);
    emit (ProductRemoved(cartItem: _cartItems));
  }
}

复制代码

注意:用CartCubit 替换整个代码中的CartBloc ,并按如下所示触发事件。

onPressed: () {
  !cart.contains(itemNo)
      ? BlocProvider.of<CartCubit>(context).add(itemNo)
      : BlocProvider.of<CartCubit>(context).remove(itemNo);
},

复制代码

输出是一样的,但是改进了状态管理。

Cart Updating

结论

有了一个坚实的BLoC架构,就可以很好地分离关注点。尽管使用BLoC模式比使用setState 需要更多的代码,但它使代码更易读、可扩展和可测试。

在本教程中,我们介绍了在Flutter中使用BLoC模式的基础知识,并通过一个实际的例子来强调在Flutter中使用BLoC进行状态管理比使用setState 的好处。

你可以在GitHub上找到这个例子的完整源代码。

The postState management in Flutter using the BLoC design patternappeared first onLogRocket Blog.

猜你喜欢

转载自juejin.im/post/7068212156369469471