Flutter中的全局状态管理(InheritedWidget与Provider)

1,773 阅读2分钟

InheritedWidget

InheritedWidget是flutter中一个非常重要的组件,其功能是数据共享。我们只要在widget树的根或者某个widget中,使用InheritedWidget进行了数据共享,那么在其后面的子widget中都可以使用其中的共享数据。如下图:

图中,InheritedWidget包含了需要共享的数据,在InheritedWidget下面的所有子孙widget都可以使用其中的数据。

InheritedWidget的使用步骤

InheritedWidget的使用过程并不复杂,具体过程如下:

  1. 创建InheritedWidget的子类,并实现如下父类方法:
    bool updateShouldNotify(YQCounterWidget oldWidget)
    
  2. 定义需要共享的数据,比如:
    final int counter;
    
  3. 定义构造方法
  4. 定义一个类方法(of方法),用以根据context查找距离当前widget最近的InheritedWidget。

完整代码如下:

class YQSharedDataWidget extends InheritedWidget{
  final int counter;//需要共享的数据

  YQSharedDataWidget({this.counter,Widget child}) : super(child: child);
  //用于子树中widget获取共享数据
  static YQSharedDataWidget of(BuildContext context){
    //沿着Element树,去找到最近的匹配的element,从element中取出Widget对象.
    return context.dependOnInheritedWidgetOfExactType<YQSharedDataWidget>();
  }

  @override
  /// 该方法决定当共享数据发生变化的时候,是否通知子树中对此有依赖的widget
  /// 如果返回true,则子树中`state.didchangedependencies`会被调用
  bool updateShouldNotify(YQSharedDataWidget oldWidget) {//
    return oldWidget.counter != counter;
  }
}

在子树widget中使用共享数据:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget"),),
      body: Container(
        child: YQSharedDataWidget(
          counter: _counter,
          child: Center(child: YQUseSharedDataWidget(),),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: (){
          setState(() {
            _counter++;
          });
        },
      ),
    );
  }
}

class YQUseSharedDataWidget extends StatefulWidget {
  @override
  _YQUseSharedDataWidgetState createState() => _YQUseSharedDataWidgetState();
}

class _YQUseSharedDataWidgetState extends State<YQUseSharedDataWidget> {
  @override
  Widget build(BuildContext context) {
  	//使用InheritedWidget中的共享数据
    int counter = YQSharedDataWidget.of(context).counter;
    return Card(
      color: Colors.red,
      child: Text("点击次数: $counter"),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

注意,上面的代码中,如果共享数据发生变化,didChangeDependencies是会被调用的。为什么呢?两个条件:

  • YQSharedDataWidget中,updateShouldNotify在数据发生变化时候返回的是true
  • _YQUseSharedDataWidgetState的build方法中,依赖了YQSharedDataWidget(int counter = YQSharedDataWidget.of(context).counter;)

上述两个条件任何一个不成立,_YQUseSharedDataWidgetState的didChangeDependencies方法都不会被调用。

Provider(官方推荐)

Provider虽然是官方推荐的,但是并没有在flutter的sdk中继承。所以,使用前需要先添加依赖。

相关链接:

  1. 库地址:pub.flutter-io.cn/packages/pr…
  2. 文档地址:github.com/rrousselGit…

Provider的使用步骤

  1. 创建共享的数据:实现ChangeNotifier的子类,在其中设置需要共享的数据,并设置对应的setter和getter,在setter中调用notifyListeners(),这样当数据改变的时候,可以通知其对应的监听者。

  2. 在应用程序的顶层(传入runApp作为参数的widget)使用ChangeNotifierProvider或者MultiProvider对根widget进行包裹。

  3. 在其他地方使用共享的数据。使用共享数据的三种方式:

  • Provider.of:当共享数据发生变化的时候,会导致依赖数据的build方法重新执行。
  • Consumer:当共享数据发生变化时候,只会重新执行Consumer中的builder,而不会重新执行其所在的build方法。注意,只要数据发生变化,Consumer中的builder就会重新执行!
    • Consumer的的builder的签名如下:

      Widget Function(BuildContext context, T value, Widget child) builder
      

      如果需要使用多个共享的数据,可以使用Consumer2...Consumer6,Consumer后面紧跟的数字标识bulder中接收的value的个数,如Consumer3的签名如下:

      Widget Function(
      BuildContext context, A value, B value2, C value3, Widget child) builder
      

      其中的value,value2,value3则是共享的数据。

  • Selector,可以控制是否执行builder()方法,优化性能。参数:
    • selector:对原有数据进行转换

    • shouldRebuild:要不要重新构建(builder要不要重新执行)

    • builder:返回需要构建的widget。

      在builder的定义中,value就是共享的数据,第三个参数child则是用于性能优化。如下代码:

      floatingActionButton: Consumer<YQCounterViewModel>(
       builder: (context, value, child){
         return FloatingActionButton(
           child: Icon(Icons.add),//共享数据发生变化,会重新渲染
           onPressed: (){
             setState(() {
               value.counter += 1;
             });
           },
         );
       },
      ),
      

      在上面的代码中,如果共享的数据发生了变化,FloatingActionButton的child会因为builder执行而重新渲染,但是此处是跟数据没有关系的,根本不需重新渲染。修改如下:

      floatingActionButton: Consumer<YQCounterViewModel>(
        builder: (context, value, child){
          return FloatingActionButton(
            child: child,//①
            onPressed: (){
              setState(() {
                value.counter += 1;
              });
            },
          );
        },
        child: Icon(Icons.add),//②
      ),
      
      

      上面的代码中, 在Consumer的child中生成了Icon(标识②的位置),然后在FloatingActionButton的child属性中使用了该值(标识①的位置),这样在buidler方法重新执行的时候,FloatingActionButton的Icon就不会重新渲染了。这就是Consumer的builder中的child的作用。

Provider完整的使用代码如下:

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

import 'view_model/counter_view_model.dart';
import 'view_model/user_view_model.dart';
import 'view_model/providers.dart';//专门管理共享数据

void main() {
  runApp(MultiProvider(//在app的顶层使用MultiProvider包裹来共享数据
    child: MyApp(),
    providers: providers,
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.green,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Provider"),),
      body: Container(
        child: YQUseProviderDataWidget(),
      ),
      floatingActionButton: Selector<YQCounterViewModel,YQCounterViewModel>(
        selector: (context, value)=>value,//可以对value进行处理
        shouldRebuild: (prev,next) => false ,//是否需要调用builder方法
        builder: (context, value, child){
          return FloatingActionButton(
            child: child,
            onPressed: (){
              setState(() {
                value.counter += 1;
              });
            },
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class YQUseProviderDataWidget extends StatefulWidget {
  @override
  _YQUseProviderDataWidgetState createState() => _YQUseProviderDataWidgetState();
}

class _YQUseProviderDataWidgetState extends State<YQUseProviderDataWidget> {
  @override
  Widget build(BuildContext context) {
    //通过Provider.of来使用共享的数据
    int counter = Provider.of<YQCounterViewModel>(context).counter;

    return Column(
      children: [
        Container(
          height: 100,
          color: Colors.red,
          child: Center(
            child: Text("$counter"),
          ),
        ),
        Container(
          height: 100,
          color: Colors.orangeAccent,
          child: Center(
            child: Consumer<YQUserViewModel>(//通过Consumer来使用共享的数据
              builder: (ctx,userVM,child){
                return Text("${userVM.user.nick}");
              },
            ),
          ),
        )
      ],
    );
  }
}

上面代码中,main方法里面,如果需要共享的数据只有一个,可以将之修改为如下代码:

 runApp(ChangeNotifierProvider(
   child: MyApp(),
   create: (BuildContext context){
     return YQCounterViewModel();
   },
 ));

相关文件:

counter_view_model.dart

import 'package:flutter/material.dart';

class YQCounterViewModel extends ChangeNotifier{
  int _counter = 1;
  int get counter => _counter;
  set counter(int value) {
    _counter = value;
    notifyListeners();//通知所有的监听者
  }
}

user_view_model.dart

import 'package:flutter/material.dart';
import 'package:demos/models/user.dart';

class YQUserViewModel extends ChangeNotifier{
  User _user;

  User get user => _user;

  set user(User value) {
    _user = value;
    notifyListeners();
  }

  YQUserViewModel(this._user);
}

user.dart

class User{
  String nick;
  int level;
  String avatar;

  User(this.nick, this.level, this.avatar);
}

providers.dart

import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';

import 'counter_view_model.dart';
import 'user_view_model.dart';
import '../models/user.dart';

List<SingleChildWidget> providers = [
  ChangeNotifierProvider(create: (ctx)=>YQCounterViewModel()),
  ChangeNotifierProvider(create: (ctx)=>YQUserViewModel(User("zhangsan",99,"avatar")))
];