This article is based on Google I/O Talk. Build reactive mobile apps with Flutter

  • Widget state passing
  • InheritedWidget & ScopedModel
  • RxDart & Widget
  • redux and more

Widget state passing

Let’s start with a simple example. This is an adder. Templates are provided to you when we create a new Flutter app project. The usual approach is to extract components, such as FAB in the example.

.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: newIncrementer(_increment), ); }}class Incrementer extends StatefulWidget {
  final Function increment;

  Incrementer(this.increment);

  @override
  IncrementerState createState() {
    return newIncrementerState(); }}class IncrementerState extends State<Incrementer> {
  @override
  Widget build(BuildContext context) {
    return new FloatingActionButton(
      onPressed: widget.increment,
      tooltip: 'Increment',
      child: newIcon(Icons.add), ); }}Copy the code

While this kind of passing can be used, one problem is that when we build large applications, the entire View tree needs to pass state, which is obviously not reasonable.

InheritedWidget & ScopedModel

Flutter provides us with a component, the InheritedWidget. He can solve the problem of state passing.

There’s a problem though, when we only use the InheritedWidget, all the state is final. Immutable, which is not applicable in most complex scenarios. Therefore, another component is provided to solve the problem of data ScopedModel.

The ScopedModel has the following features:

  • External extension pack
  • Based on the InheritedWidget
  • Use, upgrade & change

Here is an example of a shopping cart

These include several features:

  • Click on the product to add to cart
  • The shopping cart button updates the amount in the basket in real time
  • The shopping cart page allows you to view the added list

We need to prepare a 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}}Copy the code

Based on the previous page, we need to wrap the ScopedModel at the top of the View Tree and replace the widgets that need to use the Model with the ScopedModelDescendant

.

.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.//hartbuilder: (context, child, model) => ProductSquare( product: product, onTap: () => model.add(product), ), ); }).toList(), ); }}Copy the code

Note that the rebuildOnChange: false property, which defaults to true, rebuilds widgets when Model changes. If we don’t want to rebuild every time, we need to set it to false.

ScopedModel solves the problem of data transmission, but we need to care about which widgets need to be rebuilt and which ones do not, which is also a tedious work.

RxDart & Widget

For those of you who have written Java, ReactiveX is designed to solve a similar problem. The other good news is that Dart Core Lib supports stream. Stream&ReactiveX = RxDart.

Here are the official architectural patterns.

Business logic is the bridge to input&output and is crucial to the overall pattern.

  • Sink packaging input
  • Stream corresponding output
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); }}); }}Copy the code

Another example is that when we do localization, we can easily change the totalCost according to the locale provided by Sink.

class CartBloc {
  Sink<Product> addition;
  Sink<Locale> locale;
  Stream<int> iteamCount; //if locale: China => "¥600.00"; The locale: US = > "$100.00"
  Stream<String> totalCost;
}
Copy the code

This mode is strongly recommended by the authorities, especially for complex apps. See: Github: bloc_complex

reudx and more

An unofficial version of Redux is available, and the dart version is available on Github. Corresponding to the sample.

Other ways: see repo: Github from the official little brother.

Other resources:

cookbook

awesome-flutter

Flutter official Chinese website