阅读 1403

[译]使用Flutter构建响应式应用

这篇文章基于 Google I/O talk。Build reactive mobile apps with Flutter

  • widget状态传递
  • InheritedWidget & ScopedModel
  • RxDart & Widget
  • redux and more

widget状态传递

先看一个简单的例子,这是一个加法器。当我们创建一个新的flutter app工程的时候,模版就提供给你了。我们通常的做法是把组件提取出来,比如例子中的FAB。

...
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter Demo Home Page')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.display1),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _counter += 1;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

...

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter += 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new Incrementer(_increment),
    );
  }
}

class Incrementer extends StatefulWidget {
  final Function increment;

  Incrementer(this.increment);

  @override
  IncrementerState createState() {
    return new IncrementerState();
  }
}

class IncrementerState extends State<Incrementer> {
  @override
  Widget build(BuildContext context) {
    return new FloatingActionButton(
      onPressed: widget.increment,
      tooltip: 'Increment',
      child: new Icon(Icons.add),
    );
  }
}
复制代码

虽然这种传递是可以使用的, 但是有一个问题,当我们构建大型应用的时候,整个view tree都需要传递状态,这显然是不合理的。

InheritedWidget & ScopedModel

Flutter给我们提供了一个组件,InheritedWidget。他可以解决状态传递的问题。

但是有一个问题,当我们只使用InheritedWidget的时候,所有的状态是final的。不可改变,这在大多数复杂场景下并不是适用。因此官方提供了另一个组件来解决数据数据的问题ScopedModel

ScopedModel有以下几个特性:

  • 外部扩展包
  • 基于InheritedWidget
  • 使用,升级&改变

下面是一个购物车的例子

其中包括几个功能:

  • 点击产品添加到购物车中
  • 购物车按钮实时更新篮子中的数量
  • 购物车页面可以查看已添加的列表

我们需要准备一个CartModel

class CartModel extends Model {
  final _cart = Cart();

  List<CartItem> get items => _cart.items;

  int get itemCount => _cart.itemCount;

  void add(Product product) {
    _cart.add(product);
    notifyListeners(); //notify all data update
  }
}
复制代码

在之前页面的基础上,我们需要使用ScopedModel包装在view tree最上层,使用ScopedModelDescendant<CartModel>替换需要使用Model的Widget。

...
class CatalogHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scoped Model'),
        actions: <Widget>[
          // SCOPED MODEL: Wraps the cart button in a ScopdeModelDescendent to access
          // the nr of items in the cart
          ScopedModelDescendant<CartModel>(
            builder: (context, child, model) => CartButton(
                  itemCount: model.itemCount,
                  onPressed: ...,
                ),
          )
        ],
      ),
      body: ProductGrid(),
    );
  }
}

class ProductGrid extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 2,
      children: catalog.products.map((product) {
        // SCOPED MODEL: Wraps items in the grid in a ScopedModelDecendent to access
        // the add() function in the cart model
        return ScopedModelDescendant<CartModel>(
          rebuildOnChange: false, //hart
          builder: (context, child, model) => ProductSquare(
                product: product,
                onTap: () => model.add(product),
              ),
        );
      }).toList(),
    );
  }
}
复制代码

需要注意的是:rebuildOnChange: false 这个属性,默认是true,即Model改变时会rebuild widget。如果我们不想每次都rebuild,那需要设置为false。

ScopedModel虽然解决了数据的传递的问题,但是在我们使用时需要关心哪些widget需要rebuild,那些不需要,这也是一个繁琐的工作。

RxDart & Widget

写过Java的小伙伴一定非常熟悉了,ReactiveX就是为了解决类似的问题。还有一个好消息是dart core lib支持stream。Stream&ReactiveX = RxDart。

下面是官方推荐的架构模式(architectural pattern)。

business logic最为结合input&output的桥梁,在整个模式中至关重要。

  • Sink 包装输入
  • Stream 对应输出
class CartAddition {
  final Product product;
  final int count;

  CartAddition(this.product, [this.count = 1]);
}

class CartBloc {
  final Cart _cart = Cart();

  final BehaviorSubject<List<CartItem>> _items =
      BehaviorSubject<List<CartItem>>(seedValue: []);

  final BehaviorSubject<int> _itemCount =
      BehaviorSubject<int>(seedValue: 0);

  final StreamController<CartAddition> _cartAdditionController =
      StreamController<CartAddition>();
  
  Sink<CartAddition> get cartAddition => _cartAdditionController.sink;

  Stream<int> get itemCount => _itemCount.stream;

  Stream<List<CartItem>> get items => _items.stream;
  
  CartBloc() {
    _cartAdditionController.stream.listen((addition) {
      int currentCount = _cart.itemCount;
      _cart.add(addition.product, addition.count);
      _items.add(_cart.items);
      int updatedCount = _cart.itemCount;
      if (updatedCount != currentCount) {
        _itemCount.add(updatedCount);
      }
    });
  }
}
复制代码

另一个例子是,当我们要做本地化的时候,我们可以很方便的根据Sink提供的locale对totalCost进行修改。

class CartBloc {
  Sink<Product> addition;
  Sink<Locale> locale;
  Stream<int> iteamCount; //if locale: China => "¥ 600.00"; locale: US => "$100.00"
  Stream<String> totalCost;
}
复制代码

这个模式是官方大力推荐的,尤其是复杂APP,推荐使用此种模式。 详见:Github: bloc_complex

reudx and more

非官方提供了Redux的实现,简单讲就是dart的实现版本详见:Github。对应sample

其他方式:详见来自官方小哥哥的repo: Github

其他资源:

cookbook

awesome-flutter

flutter官方中文站