极简Flutter状态管理库:consumer

若你觉得 Provider 等状态管理太繁琐, consumer 就是为你准备的一款高性能、极简的状态管理库。

consumer 是一个参考 react-consumer 方式的状态管理, 使用 dart 的 Stream 做发布订阅.

类 react 项目,当项目到一定程度,必不可少需要一个状态管理器,flutter 有着不少状态管理库,BLOC、Provider、redux 等等;但是他们现有的问题是没有给出很便捷的状态管理优化方案。

consumer 的特点是仅仅是发布订阅模式加 StateFulWidget,这比市面上基于 InheritedWidget 进行封装的状态管理器的优势是它不需要一个顶层的提供者模式的包裹。基于此,consumer 可以让项目更简单创建子模块的独立的状态管理,当然你也可以使用 consumer 的单一模式管理整个项目的状态。

在这个前提下,我们会发现若项目足够大,我们需要切分多个子状态管理,或者一些局部的状态管理,这样可以有效减少事件派发的影响范围,从而提高性能;consumer 另一个特点是强制使用者描述每个订阅所使用的对象,这样 consumer 可以帮助优化性能,拦截不必要的更新。

Feature

  • 仅更新数据变化的局部
  • 不需要一个顶层的 Provider 包裹对象
  • 可以很轻松的给子模块设置独立的状态管理
  • 非常易于使用, 仅有 2 个 API: setStatebuild

API 文档:

安装 consumer

修改 pubspec.yaml:

dependencies:
  consumer: ^2.2.0

入门指南

这是一个 Flutter 默认的初始化项目,我们使用 consumer 改造它,移除 StateFulWidget,替换成 StatelessWidget:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

// *** 定义一个类,描述状态 ***
class ExampleState {
  int counter = 0;
}

// *** 创建一个 consumer ***
var consumer = Consumer(ExampleState());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Consumer Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  _incrementCounter() {
    // *** 使用 setState ,触发订阅的组件更新 ***
    consumer.setState((state) => state.counter++);
  }

  @override
  Widget build(BuildContext context) {
    print('整个对象仅更新一次,更新时仅更新订阅的组件');

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            // *** 使用 consumer.build 订阅一个组件 ***
            consumer.build((ctx, state) {
              return Text(
                state.counter.toString(),
                style: Theme.of(context).textTheme.display1,
              );
            }, memo:()=>[state.counter]),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

参数 memo 的作用是什么?

从 v2.2.0 版本开始 memo 参数是必传的,这是因为作者认为与其等性能出现问题再去优化,不如从编写的时候就强制开发者编写性能已优化的代码。

如果你项目有着非常多的状态订阅,使用 memo 可以大幅度提高性能.

memo 的概念是来自于 react.Hooks, 它用来描述监听变化的对象,仅有监听对象变化时,才会派发更新。

一个原则是,我们在 builder 对象中需要使用什么属性,memo 返回的数组就定义什么属性, 我们这里有一些例子:

如果我们由 consumer.build 创建的两个 widget:

// *** definition a state ***
class ExampleState {
  List<String> animates = [];
  int age = 0;
  String name = 'dog';
}

// *** create a consumer ***
var consumer = Consumer(ExampleState());

Column(
  children: <Widget>[
    consumer.build((ctx, state) {
        print('Update when state.age change');
        return Text(
          '$state.age',
          style: Theme.of(context).textTheme.display1,
        );
      },
      memo: (state) => [state.age, state.animates],
    ),
    consumer.build((ctx, state) {
        print('Update when state.name change');
        return Text(
          state.name,
          style: Theme.of(context).textTheme.display1,
        );
      },
      memo: (state) => [state.name],
    ),
  ],
);

然后我们更新 state.name:

consumer.setState((state){
  state.name = 'cat';
});

此时,当我们更新 state.name,只有订阅了 memo: (state) => [state.name] 的 widget 会更新,其他 Widget 的更新都会被 consumer 拦截。

完整的使用 consumer 配合 memo 拦截更新的例子

一般来说使用状态管理都会涉及到跨组件更新,consumer 建议您把相关组件使用的状态放到一个文件中,在不同的组件中进行引用:

lib/consumer.dart: 声明状态和状态消费者

import 'package:consumer/consumer.dart';

class ExampleState {
  int counter = 0;
  String time = DateTime.now().toString();
}

var consumer = Consumer(ExampleState());

lib/main.dart: 使用状态消费者,绘制需要被状态接管的组件

import 'package:flutter/material.dart';
import './consumer.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      theme: ThemeData(primaryColor: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Text("hello"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('counter:'),
              consumer.build(
                (ctx, state) {
                  print("update state.counter");
                  return Text(
                    state.counter.toString(),
                    style: Theme.of(ctx).textTheme.headline4,
                  );
                },
                memo: (state) => [state.counter],
              ),
              Container(
                child: TextButton(
                  onPressed: () {
                    consumer.setState((state) {
                      state.counter += 1;
                    });
                  },
                  child: Text("Only Change counter",
                      style: TextStyle(fontSize: 24)),
                ),
                margin: EdgeInsets.only(top: 20, bottom: 40),
              ),
              Text('time:'),
              consumer.build(
                (ctx, state) {
                  print("update state.time");
                  return Text(
                    state.time.toString(),
                    style: Theme.of(ctx).textTheme.headline4,
                  );
                },
                memo: (state) => [state.time],
              ),
              Container(
                child: TextButton(
                  onPressed: () {
                    consumer.setState((state) {
                      state.time = DateTime.now().toString();
                    });
                  },
                  child:
                      Text("Only Change time", style: TextStyle(fontSize: 24)),
                ),
                margin: EdgeInsets.only(top: 20),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

为什么我的使用了 consumer.setState 之后 Widget 并没有更新?

或许你在 builder 中使用了 state.name, 不过 memo 返回的数组未包含 state.name:

Center(
  child: consumer.build((ctx, state) {
      return Text(
        state.name,
        style: Theme.of(context).textTheme.display1,
      );
    },
    memo: (state) => [state.age],
  ),
);

或许你的 memo 未监听任何对象:

Center(
  child: consumer.build((ctx, state) {
      return Text(
        state.name,
        style: Theme.of(context).textTheme.display1,
      );
    },
    memo: (state) => [],
  ),
);

或许你仅仅是改变了 List 或 Map 内的对象,但是没有重新设定一个新的 List 或 Map:

class ExampleState {
  List<String> names = ['dog', 'cat'];
}

var consumer = Consumer(ExampleState());

Center(
  child: consumer.build((ctx, state) {
      return Text(
        state.names[0],
        style: Theme.of(context).textTheme.display1,
      );
    },
    memo: (state) => [state.names],
  ),
);

// 错误的更新:
Consumer.setState((state){
  state.names[0] = 'fish'
});

// 正确的更新:
Consumer.setState((state){
  List<String> names = [...state.names];
  names[0] = 'fish'
  state.names = names;
});

State 小技巧

如果你需要在更新之前做一些计算, 或者更方便处理数组之类的更新,你可以创建一些函数属性给 State:

这里有一个修改 List 数据的例子:

class ExampleState {
  int lastChangeNamesIndex;
  List<String> names = ['dog', 'cat'];

  changeNameAt(int index, String name) {
    lastChangeNamesIndex = index;
    List<String> nextNames = [...names];
    nextNames[index] = name;
    names = nextNames;
  }
}

var consumer = Consumer(ExampleState());

Center(
  child: consumer.build((ctx, state) {
      return Text(
        state.names[state.lastChangeNamesIndex],
        style: Theme.of(context).textTheme.display1,
      );
    },
    memo: (state) => [state.names, state.lastChangeNamesIndex],
  ),
);

// 轻松更新 names 和 lastChangeNamesIndex
consumer.setState((state){
  state.changeNameAt(0, 'monkey');
})

That's all

感谢你阅读本文档和使用 consumer.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容