Flutter实践:深入探索 flutter 中的状态管理方式(1)

利用 Flutter 内置的许多控件我们可以打造出一款不仅漂亮而且完美跨平台的 App 外壳,我利用其特性完成了类似知乎App的UI界面,然而一款完整的应用程序显然不止有外壳这么简单。填充在外壳里面的是数据,数据来源或从本地,或从云端,大量的数据处理很容易造成数据的混乱,耦合度提高,不便于维护,于是诞生了很多设计模式和状态管理的方式。

目前 Flutter 常用状态管理方式有如下几种:

  • ScopedModel
  • BLoC (Business Logic Component) / Rx
  • Redux

这篇文章暂且不提这些比较复杂的模式。我们简单的提出三个问题:

  • Flutter 中组件之间如何通信?
  • 更新 State 后组件以何种方式重新渲染?
  • 如何在路由转换之间保持状态同步?

初探 State

我以创建新项目 Flutter 给我们默认的计数器应用为例,通过路由我将其拆分为两部分 MyHomePagePageTwo

MyHomePage,持有一个_counter变量和一个增加计数的方法;PageTwo,接收两个参数(计数的至和增加计数的方法):

class PageTwo extends StatefulWidget {
  final int count;
  final Function increment;

  const PageTwo({Key key, this.count, this.increment}) : super(key: key);

  _PageTwoState createState() => _PageTwoState();
}

class _PageTwoState extends State<PageTwo> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Page Two"),
      ),
      body: Center(
        child: Text(widget.count.toString(), style: TextStyle(fontSize: 30.0),),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: widget.increment,
      ),
    );
  }
}

出现的状况是:我们在首页点击按钮触发计数器增加,路由到 PageTwo 后,数值正常显示,然而点击这个界面中的 add 按钮该页面的数值并未发生改变,通过观察父页面的 count 值确实发生了改变,因此再次通过路由到第二个界面界面才显示正常。解答上面三个问题:

  • Flutter 中组件之间如何通信?

    参数传递。

  • 更新 State 后组件以何种方式重新渲染?

    只渲染当前的组件(和子组件,这里暂未证明,但确实是触发 SetSate() 后,其所有子组件都将重新渲染。)

  • 如何在路由转换之间保持状态同步?

    父组件传递状态值到子组件,子组件拿到并显示,但却不能实时更改😀,我一时半会还正没想出什么解决方法,我相信即使能做到也不优雅。

证明触发 SetSate() 后,其所有子组件都将重新渲染:我在父组件中添加两个子组件,一旦触发渲染变打印相关数据:

TestStateless(),
TestStateful()

class TestStateless extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('build TestStateless');
    return Text('TestStateless');
  }
}

class TestStateful extends StatefulWidget {
  @override
  _TestStatefulState createState() => _TestStatefulState();
}

class _TestStatefulState extends State<TestStateful> {
  @override
  Widget build(BuildContext context) {
    print('build TestStateful');
    return Text('_TestStatefulState');
  }
}

此时到 PageTwo 触发 add 事件,日志出来:

image

通过这种简单的方式已经可以说明一个问题,即以最简单的方式我们已经可以完成状态传递和组件渲染,而路由间保持状态一致还不能解决。

image

InheritedWidget

Google 官方给我们的解决方案是 InheritedWidget,怎么理解他,我们可以称它为“状态树”,它使得所有的 widget 的 State 来源统一,这样一旦有一处触发状态改变,Flutter 以某种方式感应到了(有个监听器),砍掉它,长出一个新树,Perfect!所有地方都能感受到他的变化。上面提到的第一种状态管理方式 ScopedModel便是基于此而产生的一套第三方库。

其实现在看来 InheritedWidget 已经非常简单了,我们抓住两个点即可完全掌握它:

  1. 状态树中的数据

    class MyInheritedValue extends InheritedWidget {
      const MyInheritedValue({
        Key key,
        @required this.value,
        @required Widget child,
      }) : assert(value != null),
           assert(child != null),
           super(key: key, child: child);
      final int value;
      static MyInheritedValue of(BuildContext context) {
        return context.inheritFromWidgetOfExactType(MyInheritedValue);
      }
      @override
      bool updateShouldNotify(MyInheritedValue old) => 
            value != old.value;
    }
    

    注入到根组件中:

    Widget build(BuildContext context) {
      return MyInheritedValue(
        value: 42,
        child: ...
      );
    }
    
  2. 使用状态树中数据的其他 Widget

    // 拿到状态树中的值
    MyInheritedValue.of(context).value
    

    请注意:这种情况下是不能改 InheritedWidget 中的值的,需要改也很简单就是将 MyInheritedValue 的值封装成一个对象,每次改变这个对象的值,具体法相看我的样例代码

上面所说砍掉整棵树过于粗暴却并不夸张,因为一处改变它将联动整棵树,

ScopedModel 是基于 InheritedWidget 的库,实现起来与 InheritedWidget 大同小异,而且其有一种可以让局部组件不改变的方式:设置 rebuildOnChange 为 false。

return ScopedModelDescendant<CartModel>(
          rebuildOnChange: false,
          builder: (context, child, model) => ProductSquare(
                product: product,
                onTap: () => model.add(product),
              ),
        );

具体代码请看 GitHub,ScopedModel 样例截取一个老外给的实例,就是下方参考链接 Google 开发者大会上演讲的那两位其中之一。

image

这种方式显然有点不足之处就是一旦遇到小规模变动就要引起大规模重新渲染,所以当项目达到一定的规模考虑 Google 爸爸给我们的另一种解决方案。

Streams(流)

在 Android 开发中我们经常会用到 RxJava 这类响应式编程方法的框架,其强大之处无须多言,而 Stream 看上去就是在 Dart 语言中的响应式编程的一种实现。

  • Streams 是什么鬼?

    如果要具体把 Streams 说清楚,一篇文章绝对不够,这里先介绍一下其中的概念,这篇文章目的就是如此。待我后续想好怎么具体描述清楚。

    你可以把它想象成一个管道,有入口(StreamSink)和出口(),我们将想要处理的数据从入口放入经过该管道经过一系列处理(经由 StreamController)从出口中出来,而出口又有一个类似监听器之物,我们不知道它何时到来或者何时处理结束。但是当出口的监听器拿到东西便立即做出相应的反应。

  • 哪些东西可以放入管道?
    任何变量、对象、数组、甚至事件都可以被当作数据源从入口放进去。

  • Streams 种类

    1. Single-subscription Stream,“单订阅”流,这种类型的流只允许在该流的整个生命周期内使用单个侦听器。即使在第一个订阅被取消后,也无法在此类流上收听两次。
    2. Broadcast Streams,第二种类型的 Stream 允许任意数量的侦听器。可以随时向广播流添加侦听器。 新的侦听器将在它开始收听 Stream 时收到事件。

例子:

第一个示例描述了“单订阅”流,只打印输入的数据。 你会发现是哪种数据类型无关紧要。

import 'dart:async';

void main() {
  //
  // Initialize a "Single-Subscription" Stream controller
  //
  final StreamController ctrl = StreamController();
  
  //
  // Initialize a single listener which simply prints the data
  // as soon as it receives it
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // We here add the data that will flow inside the stream
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

第二个示例描述了“广播”流,它传达整数值并仅打印偶数。 我们用 StreamTransformer 来过滤(第14行)值,只让偶数经过。

import 'dart:async';

void main() {
  //
  // Initialize a "Broadcast" Stream controller of integers
  //
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  //
  // Initialize a single listener which filters out the odd numbers and
  // only prints the even numbers
  //
  final StreamSubscription subscription = ctrl.stream
                          .where((value) => (value % 2 == 0))
                          .listen((value) => print('$value'));

  //
  // We here add the data that will flow inside the stream
  //
  for(int i=1; i<11; i++){
    ctrl.sink.add(i);
  }
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

RxDart

RxDart包是 ReactiveX API 的 Dart 实现,它扩展了原始的 Dart Streams API 以符合 ReactiveX 标准。

image

由于它最初并未由 Google 定义,因此它使用不同于 Dart 的变量。 下表给出了 Dart 和 RxDart 之间的关系。

Dart RxDart
Stream Observable
StreamController Subject

RxDart 扩展了原始的 Dart Streams API 并提供了 StreamController 的3个主要变体:

  1. PublishSubject

    PublishSubject 是一个普通的 broadcast StreamController ,有一点不同:stream 返回一个 Observable 而不是一个 Stream 。

    image

    如您所见,PublishSubject 仅向侦听器发送在订阅之后添加到 Stream 的事件。

  2. BehaviorSubject

    BehaviorSubject 也是一个 broadcast StreamController,它返回一个 Observable 而不是一个Stream。

    image

    与 PublishSubject 的主要区别在于 BehaviorSubject 还将最后发送的事件发送给刚刚订阅的侦听器。

  3. ReplaySubject

    ReplaySubject 也是一个广播 StreamController,它返回一个 Observable 而不是一个 Stream。(萝莉啰嗦)

    image

    默认情况下,ReplaySubject 将Stream 已经发出的所有事件作为第一个事件发送到任何新的侦听器。

BloC

BLoC 代表业务逻辑组件 (Business Logic Component)。一般的 Flutter 代码业务逻辑和UI组件糅合在一起,不方便测试,不利于单独的测试业务逻辑部分,不能更好的重用业务逻辑代码,体现在,如果网络请求的逻辑有所变动的话,加入这个业务功能被两个端(web、flutter)使用的话,是需要改动两个地方的。

简而言之,业务逻辑需要:

  • 被移植到一个或几个 BLoC 中,
  • 尽可能从表示层中删除。 也就是说,UI组件应该只关心UI事物而不关心业务,
  • 依赖 Streams 使用输入(Sink)和输出(stream),
  • 保持平台独立,
  • 保持环境独立。

事实上,BLoC 模式最初的设想是实现允许独立于平台重用相同的代码:Web应用程序,移动应用程序,后端。

Bloc 的大概就是 Stream 在 Flutter 中的最佳实践:

image
  • 组件通过 Sinks 向 BLoC 发送事件,
  • BLoC 通过 stream 通知组件,
  • 由 BLoC 实现的业务逻辑。

将 BloC 应用在计数器应用中:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Streams Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream to handle the counter
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream to handle the action on the counter
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc(){
    _counter = 0;
    _actionController.stream
                     .listen(_handleLogic);
  }

  void dispose(){
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data){
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}

你一定在说,卧槽,哇靠~~什么吊玩意,那么就留着悬念吧,今天写不动了!

Bolc 的具体实现我在样例代码里分两步走放在两个文件夹里!如果需要可以先去看看尝尝鲜。

这篇文章的目的就是介绍一些概念给大家关于 Streams、RXDart 及 Bloc 详细明了的解释后续更新!

样例代码

https://github.com/MeandNi/Flutter_StatePro

参考链接

Build reactive mobile apps with Flutter (Google I/O '18)

Reactive Programming - Streams - BLoC

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

推荐阅读更多精彩内容