前端开发那些事儿

Flutter轻量级状态管理

2020-12-15  本文已影响0人  半心_忬

响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在React/Vue(两者都是支持响应式编程的Web开发框架)还是Flutter中,他们讨论的问题和解决的思想都是一致的。言归正传,我们想一个问题,StatefulWidget的状态应该被谁管理?Widget本身?父Widget?都会?还是另一个对象?答案是取决于实际情况!以下是管理状态的最常见的方法:

如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

在Widget内部管理状态封装性会好一些,而在父Widget中管理会比较灵活。有些时候,如果不确定到底该怎么管理状态,那么推荐的首选是在父widget中管理(灵活会显得更重要一些)。

一、状态管理的现状

由于flutter发展的时间不长,状态管理方案各家也都在探索,目前主流的状态管理,scope_model、redux、fish_redux、BloC、rxDart、provider等等,还有一些探索中的模式,融合多个模式的优点,比如reBloc,它们都各具优势,也都不完美。

目前工程中使用的是fish_redux,使用redux的理念对业务层再做了一层封装,对于我们现在的项目来说,太重,学习成本也很高,不利于项目开发的介入,再者,现在flutter版本更新频繁,三方的更新速度过慢,跟不上业务的发展。

flutter的状态管理分类

按使用的范围来分,flutter的状态管理分为两种:局部状态和全局状态

个人推荐状态管理

要应对如上的状态管理,由于主流方案都各具优势,也都不完美,必然是组合使用,个人觉得目前最好的方案是RxBloc和provider的组合使用:

Tips:

具体每个方案的优劣就不在本文中详述,自行google即可,这里着重介绍RxBloc和Provider的流程和使用。

在这之前,你需要了解如下概念:

二、局部状态管理 —— RxBLoC

局部状态管理,其实flutter自身已经为我们提供了状态管理,而且你经常都在用到,它就是 Stateful widget。当我们接触到flutter的时候,首先需要了解的就是有些小部件是有状态的,有些则是无状态的。StatelessWidget 与StatefulWidget。

在stateful widget中,我们widget的描述信息被放进了State,而stateful widget只是持有一些immutable的数据以及创建它的状态而已。它的所有成员变量都应该是final的,当状态发生变化的时候,我们需要通知视图重新绘制,这个过程就是setState。

这看上去很不错,我们改变状态的时候setState一下就可以了。

在我们一开始构建应用的时候,也许很简单,我们这时候可能并不需要状态管理。如下图,setState就足够了。

simple.png

但是随着功能的增加,应用程序将会有几十个甚至上百个状态。这个时候应用应该会是这样。

nan.png

一旦当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能、代码的可读性和维护性带来一定的影响。

那我们就会希望有一种更加强大的方式,来管理我们的状态:

于是BLoC呼之欲出,来帮我们处理这些问题。

BLoC是什么

BLoC代表业务逻辑组件(Business Logic Component),由来自Google的两位工程师 Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次展示。有兴趣的话可以点击观看Youtube视频

BLoC是一种利用reactive programming方式构建应用的方法,这是一个由流构成的完全异步的世界。

bloc流程图.png

BLoC工作流程如下:

BLoC能够允许我们完美的分离业务逻辑!再也不用考虑什么时候需要刷新了(setState不需要我们显示调用),一切交给StreamBuilder和BLoC!

Tips:

通过上面的分析,也许我们会说那我们就可以跟StatefulWidget说88了,但通过测试后,准确地描述,应该是可以和大部分StatefulWidget说88,至少保持一个StatefulWidget,使用其state来保存BLoC实例,Stream在不需要使用的时候,需要显示的调用close方法,不然会造成内存泄露或循环引用

使用RxDart

ReactiveX是一个强大的库,用于通过使用可观察序列来编写异步和基于事件的程序。它突破了语言和平台的限制,为我们编写异步程序提供了极大的便利。

如果之前接触过Rx系列,相信已收获Rx带来的便利。

仅使用flutter提供的Stream足够我们实现BLoC,但RxDart丰富和扩展了Stream,使BLoC更简单更强大。

RxDart对Stream做了哪些封装,不是本文的重点,需要了解的话自行Google,RxDart具体的API到github自行查看RxDart

举个栗子

我们使用BLoC来实现如下这个功能,简单的一个登陆(忽略丑巨的UI,测试而已哈...),需求如下:

登录demo.png
定义BLoC 抽象类

BLoC中无论是直接使用Stream还是RxDart,本质都是Stream,在Stream不需要使用的时候,我们需要显示地调用close方法,所以写一个简单的抽象类,所有的BLoC对象都继承该抽象类,Stream的close都在dispose方法中实现。

// 所有Bloc的基类
abstract class BlocProviderBase {
  // 销毁stream
  void dispose();
}
创建 LoginBLoC 登录BLoC
/// 登录 bloc
class LoginBlocProvider extends BlocProviderBase {
  String _account = '';
  String _password = '';

  final PublishSubject<String> _accountSub = PublishSubject<String>();
  PublishSubject<String> get accountSub => _accountSub;

  final PublishSubject<String> _passwordSub = PublishSubject<String>();
  PublishSubject<String> get passwordSub => _passwordSub;

  final PublishSubject<bool> _validSub = PublishSubject<bool>();
  PublishSubject<bool> get validSub => _validSub;

  final PublishSubject<String> _loginSub = PublishSubject<String>();
  PublishSubject<String> get loginSub => _loginSub;

  // 构造方法
  LoginBlocProvider() {
    _handleSubscript();
  }

  // 登录操作
  void doLogin() async {
    await Future.delayed(Duration(seconds: 3));

    print('登录成功 => 用户名:$_account, 密码:$_password');

    _loginSub.add('登录成功~');
  }

  // 处理订阅
  void _handleSubscript() {
    CombineLatestStream<String, bool>([_accountSub, _passwordSub], (values) {
      return values.first.length >= 6 &&
          values.first.length <= 20 &&
          values.last.length >= 6 &&
          values.last.length <= 12;
    }).listen((value) {
      _validSub.sink.add(value);
    });

    _accountSub.listen((value) {
      _account = value;
    });

    _passwordSub.listen((value) {
      _password = value;
    });
  }

  // 销毁
  void dispose() {
    _accountSub.close();
    _passwordSub.close();
    _validSub.close();
    _loginSub.close();
  }
}

为什么要使用私有变量“_”,提供get方法

一个应用需要大量开发人员参与,你写的代码也许在几个月之后被另外一个开发看到了,这时候假如你的变量没有被保护的话,那么是可以随意改变其中的属性的,比如_account,如果直接进行赋值,那么就破坏了整个BLoC的流程。

虽然两种方式的效果完全一样,但是第二种方式将会让我们的business logic零散的混入其他代码中,提高了代码耦合程度,非常不利于代码的维护以及阅读,所以为了让BLoC完全分离我们的业务逻辑,请务必使用私有变量。

创建 LoginBLoC 实例

flutter常被人诟病的一点是嵌套过深,我们可以通过抽取子widget来一定程度上规避嵌套地狱,本例中抽取了多个子Widget,一会详细看代码,但同时也就会带来一个BLoC实例从父widget传递到子widget的问题,这里我们使用Provider来实现局部共享,不使用InheritWidget的原因,上文中已说明,就不赘述了,Provider的具体使用,后面会详解,这里先主要说明RxBLoC。

上文中也提到,我们通过Stream和StreamBuilder实现局部刷新,完全不需要使用setState了,那也就不需要使用StatefulWidget,但是我们需要在页面销毁的时候,调用BLoC实例的dispose方法,我们就至少需要一个顶层的StatefulWidget来保存BLoC实例。

于是我们在state中创建并保存BLoC实例,并在build的顶层,使用Provider来共享该实例,且在state的dispose中调用BLoC实例中的dispose方法,关闭Stream:

class _ProviderSharePageHomeState extends State<ProviderSharePageHome> {
  LoginBlocProvider _bloc;

  @override
  void initState() {
    super.initState();

    _bloc = LoginBlocProvider();
  }

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (ctx) => _bloc,
      child: Column(
        children: <Widget>[
          SizedBox(
            height: 50,
          ),
          LoginAccountWidget(),
          SizedBox(
            height: 10,
          ),
          AccountWidget(),
          SizedBox(
            height: 10,
          ),
          PasswordWidget(),
          SizedBox(
            height: 10,
          ),
          LoginButtonWidget(),
          SizedBox(
            height: 10,
          ),
          LoginStateWidget(),
        ],
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();

    _bloc.dispose();
  }
}
LoginBLoC 的使用

以账号输入为例,与BLoC的连接如下:

class AccountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return TextField(
      onChanged: (value) {
        _bloc.accountSub.add('$value');
      },
      decoration: InputDecoration(
        labelText: '用户名',
        filled: true,
      ),
    );
  }
}

通过对TextField的onChanged方法监听,将新的输入数据通过bloc中的对应的stream,发送给bloc,由bloc做对应的逻辑处理。

以输入的用户名text为例,使用StreamBuilder构建如下:

class LoginAccountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return Container(
        width: double.infinity,
        height: 40,
        color: Colors.black12,
        child: Center(
          child: StreamBuilder(
            stream: _bloc.accountSub.where((origin) {
              // 丢弃
              return origin.length >= 6 && origin.length <= 20;
            }).debounceTime(Duration(milliseconds: 500)),
            initialData: '',
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
              return Text(
                "输入的用户名:${snapshot.data.isEmpty ? '' : snapshot.data}",
                style: TextStyle(color: Colors.red),
              );
            },
          ),
        ));
  }
}

当输入的账号和密码符合规则,登录按钮按钮才会变得可用,同样是是使用StreamBuilder:

class LoginButtonWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of<LoginBlocProvider>(context);

    return Container(
        width: 128,
        height: 48,
        child: StreamBuilder(
          stream: _bloc.validSub,
          initialData: false,
          builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
            return FlatButton(
              color: Colors.blueAccent,
              disabledColor: Colors.blueAccent.withAlpha(50),
              child: Text(
                '登录',
                style: TextStyle(color: Colors.white),
              ),
              onPressed: snapshot.data
                  ? () {
                      print('点击了登录');

                      _bloc.doLogin();
                    }
                  : null,
            );
          },
        ));
  }
}

如此,整个登录功能就实现了,BLoC的流程就是这样,其他功能的代码请详见DEMO。

大型应用中应该如何组织 BLoC

大型应用程序需要多个BLoC。一个好的模式是为每个屏幕使用一个顶级组件,并为每个复杂足够的小部件使用一个。但是,太多的BLoC会变得很麻烦。此外,如果您的应用中有数百个可观察量(流),则会对性能产生负面影响。换句话说:不要过度设计你的应用程序。

——Filip Hracek

三、全局状态管理 —— Provider

Provider是目前官方推荐的全局状态管理工具,由社区作者Remi Rousselet 和 Flutter Team共同编写。

3.1 Provider的基本使用

在使用Provider的时候,我们主要关心三个概念:

3.1.1 创建自己的ChangeNotifier

我们需要一个ChangeNotifier来保存我们的状态,所以创建它

class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  intget counter {
    return _counter;
  }
  set counter(int value) {
    _counter = value;
    notifyListeners();
  }
}
3.1.2 在Widget Tree中插入ChangeNotifierProvider

我们需要在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以获取到数据:

void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: MyApp(),
  ));
}
3.1.3 使用Consumer引入和修改状态
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("provider测试"),
      ),
      body: Center(
        child: Consumer<CounterProvider>(
          builder: (ctx, counterPro, child) {
            return Text("当前计数:${counterPro.counter}", style: TextStyle(fontSize: 20, color: Colors.red),);
          }
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
3.1.4 创建一个新的页面,在新的页面中修改数据
class BasicProviderSecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二个页面"),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
3.2 Provider详解
3.2.1 Consumer的builder方法解析
3.2.2 Provider.of解析

事实上,因为Provider是基于InheritedWidget,所以我们在使用ChangeNotifier中的数据时,我们可以通过Provider.of的方式来使用,比如下面的代码:

Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
  style: TextStyle(fontSize: 30, color: Colors.purple),
),

我们会发现很明显上面的代码会更加简洁,那么开发中是否要选择上面这种方式呢?

为什么呢?因为Consumer在刷新整个Widget树时,会尽可能少的rebuild Widget。

方式一:Provider.of的方式完整的代码:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("调用了HomePage的build方法");
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("当前计数:${Provider.of<CounterProvider>(context).counter}",
              style: TextStyle(fontSize: 30, color: Colors.purple),
            )
          ],
        ),
      ),
      floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.counter += 1;
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

方式二:将Text中的内容采用Consumer的方式修改如下:

3.2.3 Selector的选择

Consumer是否是最好的选择呢?并不是,它也会存在弊端

直接上代码:

floatingActionButton: Selector<CounterProvider, CounterProvider>(
  selector: (ctx, provider) => provider,
  shouldRebuild: (pre, next) => false,
  builder: (ctx, counterPro, child) {
    print("floatingActionButton展示的位置builder被调用");
    return FloatingActionButton(
      child: child,
      onPressed: () {
        counterPro.counter += 1;
      },
    );
  },
  child: Icon(Icons.add),
),

Selector和Consumer对比,不同之处主要是三个关键点:

这个时候,我们重新测试点击floatingActionButton,floatingActionButton中的代码并不会进行rebuild操作。

所以在某些情况下,我们可以使用Selector来代替Consumer,性能会更高。

3.2.4 MultiProvider

在开发中,我们需要共享的数据肯定不止一个,并且数据之间我们需要组织到一起,所以一个Provider必然是不够的。

我们再增加一个新的ChangeNotifier

import'package:flutter/material.dart';

class UserInfo {
  String nickname;
  int level;

  UserInfo(this.nickname, this.level);
}

class UserProvider extends ChangeNotifier {
  UserInfo _userInfo = UserInfo("test", 18);

  set userInfo(UserInfo info) {
    _userInfo = info;
    notifyListeners();
  }

  get userInfo {
    return _userInfo;
  }
}

如果在开发中我们有多个Provider需要提供应该怎么做呢?

方式一:多个Provider之间嵌套

runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),
    child: ChangeNotifierProvider(
      create: (context) => UserProvider(),
      child: MyApp()
    ),
  ));

方式二:使用MultiProvider

runApp(MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (ctx) => CounterProvider()),
    ChangeNotifierProvider(create: (ctx) => UserProvider()),
  ],
  child: MyApp(),
));
3.3 RxBLoC+Provider 栗子

由于RxBLoC是使用StreamBuilder来连接BLoC中的Stream,当有新数据时,会自动刷新子Widget,在全局共享时,我们并不需要使用Provider的notify功能,所以共享的数据直接使用我们定义好的BLoC就可以了。

由于我们不需要notify功能,所以在APP顶层共享数据是,也不需要使用ChangeNotifierProvider,直接使用Provider即可,当共享多个BLoC时,使用MultiProvider,这个例子即是演示共享多个状态。

3.3.1 小需求

需要在全局共享一个count和一个name,count的初始值是10,name的初始值是name,在count页面,点击右下角的+,count累加,在name页面点击右下角的+,name在后面拼接一个1字符串,count页面和name页面,都显示count+name的格式化字符串。

counter.png name.png
3.3.2 创建共享的count和name的BLoC
/// 数值 bloc
class CounterBlocProvider extends BlocProviderBase {
  int _counter = 10;

  BehaviorSubject<int> _counterSub = BehaviorSubject.seeded(10);
  BehaviorSubject<int> get counterSub => _counterSub;

  // 构造方法
  CounterBlocProvider() {
    _handleSubscript();
  }

  // 增加操作
  void doAdd() {
    print('执行了 counter 增加操作');

    _counterSub.add(++_counter);
  }

  // 处理订阅
  void _handleSubscript() {
    _counterSub.listen((value) {
      _counter = value;
    });
  }

  // 销毁
  void dispose() {
    _counterSub.close();
  }
}

/// name bloc
class NameBlocProvider extends BlocProviderBase {
  String _name = 'name';

  BehaviorSubject<String> _nameSub = BehaviorSubject.seeded('name');
  BehaviorSubject<String> get nameSub => _nameSub;

  // 构造方法
  NameBlocProvider() {
    _handleSubscript();
  }

  // 增加操作
  void doAdd() {
    print('执行了 name 增加操作');

    _nameSub.add(_name + '1');
  }

  // 处理订阅
  void _handleSubscript() {
    // _nameSub.add(_name);
    _nameSub.listen((value) {
      _name = value;
    });
  }

  // 销毁
  void dispose() {
    _nameSub.close();
  }
}
3.3.3 在APP顶层共享全局状态
void main() {
  runApp(MultiProvider(
    providers: [
      Provider(create: (ctx) => CounterBlocProvider()),
      Provider(create: (ctx) => NameBlocProvider()),
    ],
    child: MyApp(),
  ));
}
3.3.4 创建count page 和 name page,使用全局共享状态

由于我们需要显示的内容是共享的两个BLoC状态,所以对两个Stream进行了合并操作,使用了RxDart中的CombineLatestStream,无论是count还是name发生了变化,在显示的地方都会实时刷新。

如果是单纯使用Stream,这个功能实现会比较麻烦,这也是Rx带来的便利的体现。

class ProviderPage extends StatefulWidget {
  static const String routeName = "/providerPage";

  const ProviderPage({Key key}) : super(key: key);

  @override
  _ProviderPageState createState() => _ProviderPageState();
}

class _ProviderPageState extends State<ProviderPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('counter provider page'),
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage2.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream<dynamic, dynamic>(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
          builder: (context, cntProvider, nameProvider, child) {
            return FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  cntProvider.doAdd();
                });
          },
        ));
  }
}
class ProviderPage2 extends StatefulWidget {
  static const String routeName = "/providerPage2";

  const ProviderPage2({Key key}) : super(key: key);

  @override
  _ProviderPage2State createState() => _ProviderPage2State();
}

class _ProviderPage2State extends State<ProviderPage2> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('name provider page'),
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.people),
                onPressed: () {
                  Navigator.pushNamed(context, ProviderPage.routeName);
                })
          ],
        ),
        body: Center(
          child: Consumer2<CounterBlocProvider, NameBlocProvider>(
              builder: (context, cntProvider, nameProvider, child) {
            return StreamBuilder(
                initialData: '初始化',
                stream: CombineLatestStream<dynamic, dynamic>(
                    [cntProvider.counterSub, nameProvider.nameSub], (values) {
                  print('合并的值是啥:${values.join(' + ')}');
                  return values.join(' + ');
                }),
                builder: (context, snapshot) {
                  return Chip(label: Text(snapshot.data));
                });
          }),
        ),
        floatingActionButton: Consumer2<CounterBlocProvider, NameBlocProvider>(
          builder: (context, cntProvider, nameProvider, child) {
            return FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  nameProvider.doAdd();
                });
          },
        ));
  }
}

注意点:

问题点:

四、后记

没有最完美的代码,也没有最完美的框架,只有适合自己的框架,以上内容仅供参考~

上述代码的DEMO,传送门

参考文档:

https://mp.weixin.qq.com/s/ywGQnaYpioPxlYvYTSpR4w
https://www.jianshu.com/p/7573dee97dbb
https://www.jianshu.com/p/a5d7758938ef
https://www.jianshu.com/p/e0b0169a742e

上一篇 下一篇

猜你喜欢

热点阅读