Flutter状态管理之Provider的使用和架构分析

状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。

本文我们首先了解下什么是状态状态管理呢?然后我们来了解官方的状态管理库Provider的使用,最后分析下Provider背后的秘密。

状态管理

状态

Flutter是声明式编程,Widget定义的UI都是在build()函数中实现的,这个函数的功能就是将状态转换成UI

UI = f(state)

官方对状态的定义如下:

whatever data you need in order to rebuild your UI at any moment in time

翻译过来就是:状态就是任何时间任何场景下重构UI所需要的数据。

这里面至少可以看到两层含义:

  1. 状态就是数据;
  2. 状态的改变驱动了UI的改变。

状态的分类

我们可以把状态分为局部状态全局状态

局部状态就是Widget内部持有的状态,典型代表就是StatefuleWidget和它对应的State局部状态只会影响单个Widget的UI呈现。

当某个状态需要在多个Widget使用,或者在整个APP中使用,那它就是全局状态了。全局状态的典型代表就是InheritedWidget

我们在InheritedWidget的使用和源码分析这篇文章中已经详细介绍过了InheritedWidget的相关内容,当然我们也提到过它的一些不是太完善的地方。

状态管理库

我们这里所说的状态管理库主要是指对全局状态的一些处理库,除了InheritedWidget外,还有一些最近非常流行的库:

它目前是评分最高的库,适合大型的项目。但是它有一个缺点就是理解起来比较困难,编写代码方式也很独特,需要编写一些重复的代码模板。

它是Flutter官方团队共同维护的一个项目,由于有官方背景,所以不用担心后期的维护升级问题。

getx是目前上升趋势最快的一个库,使用非常简单,代码也很简介,功能很多。

当然还有其他一些库,譬如mobx, flutter_redux等,当然你很大可能也不会用到。

我们将会对Providergetx这两个库的使用和源码进行介绍。

Provider的使用

和介绍InheritedWidget时使用的案例类似,本文讲解Provider的时候使用是一个简单的计数器案例:有一个number全局状态,有三个Widget会使用到它,点击FloatingActionButton可以将number的值加1。效果如下:

Demo

 当然复杂的多界面逻辑的实现方法使用的方法是一样的。譬如实现下面的功能:

官方的示例

 基本使用

使用前得先引入库:

dependencies:
  provider: ^5.0.0

接下来我们分三步来了解它的使用:

  1. number封装到ChangeNotifier中,创建需要共享的状态
class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

ChangeNotifierFlutter Framework的基础类,不是Provider库中的类。ChangeNotifier继承自Listenable,也就是ChangeNotifier可以通知观察者值的改变(实现了观察者模式)。

NumberModel有一个_number状态,然后提供了获取的方法get和设置set的方法。

  1. 在应用程序的顶层添加ChangeNotifierProvider
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}
复制代码

将应用的顶层设置为ChangeNotifierProvider, 然后将MyApp()变为它的子Widget

ChangeNotifierProvidercreate函数需要返回ChangeNotifier

  1. 其它Widget使用共享的状态

有四个地方需要使用到共享的状态,三个显示文字的Text WidgetFloatingActionButton

  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取NumberModel的number
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我们将Text Widget封装成了NumberWidget1, 通过int number = Provider.of<NumberModel>(context).number;获取到NumberModelnumber值,然后就可以显示了。

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1 获取NumberModel
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // 2 修改number值
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

FloatingActionButton也需要通过Provider.of<NumberModel>(context)方法先拿到NumberModel,然后调用set方法改变number的值。

全部代码:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (ctx) => NumberModel(),
      child: MyApp(),
    ),
  );
}

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    
    NumberModel model = Provider.of<NumberModel>(context);

    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            model.number++;
          },
          child: Icon(Icons.add),
        ));
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}
  • Consumer

问题:Provider.of有一个问题,就是当状态值发生变化后,Provider.of所在的Widget整个build方法都会重新构建。

上面的例子中,FloatingActionButton会引起Scaffold的重构,所以对性能的影响是最大的。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: Icon(Icons.add),
        );
    },
)

我们将FloatingActionButtonConsumer包裹,builder中的value参数就是我们需要的NumberModel了。

这里我们可以进一步优化一下,对child进行复用。

Consumer<NumberModel>(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
            child: child,
        );
        },
    child: Icon(Icons.add),
));

我们将child传入Consumer的构造函数就能实现复用了。

child复用的逻辑我们在前一篇关于动画源码的文章中有解释,如果需要可以回头参阅。

差异部分的代码如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Consumer<NumberModel>(
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}
  • Consumer

问题:Consumer总归还是需要重构的,其实我们使用FloatingActionButton的时候只是用到了NumberModel的设置方法,根本没有用到它的_number属性,所以即使_number改变了,我们也是可以不需要重构的。

如果不需要重构,我们可以使用Selector

Selector<NumberModel, NumberModel>(
    selector: (ctx, numberModel) => numberModel,
    shouldRebuild: (previous, next) => false,
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: child,
        );
    },
    child: Icon(Icons.add),
)

代码解释:

  1. Selector的泛型中有两个参数类型,第一个是原始类型,第二个是转换后的类型,也就是说Selector多了一个对数据进行转换的功能;
  2. selector是进行数据类型转换的函数;
  3. shouldRebuild是确实是否需要重构,我们明显是不需要的,所以传false;
  4. builderConsumer的功能就是类似的了。

差异部分的代码如下:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider"),
        ),
        body: Center(
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [NumberWidget1(), NumberWidget1(), NumberWidget1()]),
        ),
        floatingActionButton: Selector<NumberModel, NumberModel>(
          selector: (ctx, numberModel) => numberModel,
          shouldRebuild: (previous, next) => false,
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}

多个状态的使用

有时候某个Widget可能需要使用多个状态,我们接下来就介绍这种情况的使用方法。

  1. 创建多个需要共享的状态
class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

我们再创建一个RandomNumberModel,里面有一个随机的数值_randomNumber, 并且设置获取方法get和设置方法resetRandomNumber

  1. 将应用程序的顶层改为MultiProvider
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

MultiProviderproviders放置的是共享的多个Provider

  1. 其它Widget使用共享的状态
  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 读取
    int number = Provider.of<NumberModel>(context).number;
    // 读取
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      // 使用
      child: Text(
        "点击次数: $number 随机数: $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

我们可以通过Provider.of分别取到NumberModelRandomNumberModel,然后读取到相应的值。

  • Consumer2
class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

Consumer2中两个泛型代表使用的哪两个数据,build方法中的value就是NumberModel,value2就是RandomNumberModel,然后读取到相应的值。

  • Selector2
class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}
  1. Selector2有三个泛型参数:NumberModelRandomNumberModel代表使用的两个数据类型,第三个参数表示由前两个数据转换成的新的数据类型,我们需要使用两个int值。

使用Tuple2需要引入三方库 tuple: ^2.0.0。使用它的优点是它内置了==比较操作符,不需要我们去自己比较元素是否相等了。

  1. selector的三个参数为:BuildContextNumberModelRandomNumberModel, 返回值就是转换后的数据。

builder方法中就可以直接使用value.item1value.item2了。

  1. shouldRebuild方法的previousnext的类型是Tuple2<int, int>,可以直接比较。如果相同就不重构了。

多个状态使用的补充

Consumer2还有几个好兄弟:,Consumer3Consumer4Consumer5Consumer6

Selector2也有几个好兄弟:,Selector3Selector4Selector5Selector6

通过名字可以知道,他们分别可以组合对应的多个数据。

全部代码:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<NumberModel>(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider<RandomNumberModel>(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider"),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [NumberWidget1(), NumberWidget2(), NumberWidget3()]),
      ),
      floatingActionButton: Consumer2<NumberModel, RandomNumberModel>(
        child: Icon(Icons.add),
        builder: (context, value, value2, child) {
          return FloatingActionButton(
            onPressed: () {
              value.number++;
              value2.resetRandomNumber();
            },
            child: child,
          );
        },
      ),
    );
  }
}

class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    int number = Provider.of<NumberModel>(context).number;
    int randomNumber = Provider.of<RandomNumberModel>(context).randomNumber;
    return Container(
      child: Text(
        "点击次数: $number 随机数: $randomNumber",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

class NumberWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Consumer2<NumberModel, RandomNumberModel>(
        builder: (context, value, value2, child) {
          return Text("点击次数: ${value.number}  随机数: ${value2.randomNumber}",
              style: TextStyle(fontSize: 30));
        },
      ),
    );
  }
}

class NumberWidget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Selector2<NumberModel, RandomNumberModel, Tuple2<int, int>>(
        selector: (ctx, value1, value2) => Tuple2(value1.number, value2.randomNumber),
        builder: (context, value, child) {
          return Text("点击次数: ${value.item1}  随机数: ${value.item2}",
              style: TextStyle(fontSize: 30));
        },
        shouldRebuild: (previous, next) => previous != next,
      )
    );
  }
}

class NumberModel extends ChangeNotifier {
  int _number = 0;

  int get number => _number;

  set number(int value) {
    _number = value;
    notifyListeners();
  }
}

class RandomNumberModel extends ChangeNotifier {
  int _randomNumber = Random().nextInt(100);

  int get randomNumber => _randomNumber;

  void resetRandomNumber() {
    _randomNumber = Random().nextInt(100);
    notifyListeners();
  }
}

Provider源码解析

  • Provider的基本架构如下:

  1. 所有的Provider都继承自InheritedProvider
  2. InheritedProvider持有一个_CreateInheritedProvider对象_delegate, _delegate持有_ValueInheritedProviderState对象,_ValueInheritedProviderState对象通过createState()方法调用了InheritedProvidercreate()方法生成了_value_value也就是开发者提供的可监测对象ChangeNotifier;

create()只有在需要使用_value时候才会调用,并不是InheritedProvider插入Widget Tree时候就调用,属于懒加载的实现。

  1. InheritedProvider有一个InheritedWidget子Widget _InheritedProviderScope。_InheritedProviderScope持有上面提到的_value的值;

也就是说Provider依赖于InheritedWidget,找到对应的InheritedWidget就能获取对应的_value的值。

  1. Widget重构的时候如果调用Provider.of方法,会找到_value的值并且监听它的变化。
  • Provider的局部刷新逻辑如下:

  1. _value值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents方法,调用依赖WidgetdidChangeDependencies, 这两个方法都会调用markNeedsBuild(),进行重构;
  2. Widget重构的时候会调用Provider.of方法,更新对_value的监听,为下次重构做准备。
  • ConsumerSelector的优化逻辑:

ConsumerSelector只是封装了一层SingleChildStatefulWidget,重构的范围限定在ConsumerSelector内部,内部调用的还是Provider.of方法。

  • MultiProvider的逻辑:

MultiProvider就是嵌套了多个Provider,其他和单个Provider没有什么差别。

总结

其实Provider库还提供了其他的几个ProviderListenableProvider,ValueListenableProvider,StreamProviderFutureProvider,它们都是我们开发中的可选项。

至此,我们将Provider库的使用方式和底层的逻辑解释完了。

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/123850074