热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Flutter系列十:Flutter状态管理之Provider的使用和架构分析

状态管理在Flutter中非常重要,但是它包含的内容又非常的广泛。本文我们首先了解下什么是状态和

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

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

Flutter系列十: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_bloc

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

  • Provider

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

  • getx

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

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

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

Provider的使用

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

Flutter系列十:Flutter状态管理之Provider的使用和架构分析
Demo

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

Flutter系列十:Flutter状态管理之Provider的使用和架构分析
官方的示例

基本使用

使用前得先引入库:

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(context).number;
    return Container(
      child: Text(
        "点击次数: $number",
        style: TextStyle(fontSize: 30),
      ),
    );
  }
}

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1 获取NumberModel
    NumberModel model = Provider.of(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(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(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(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(
    builder: (context, value, child) {
        return FloatingActionButton(
            onPressed: () {
                value.number++;
            },
        child: Icon(Icons.add),
        );
    },
)

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

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

Consumer(
    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(
          builder: (context, value, child) {
            return FloatingActionButton(
              onPressed: () {
                value.number++;
              },
              child: child,
            );
          },
          child: Icon(Icons.add),
        ));
  }
}
  • Consumer

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

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

Selector(
    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(
          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(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider(
          create: (ctx) => RandomNumberModel(),
        ),
      ],
      child: MyApp(),
    ),
  );
}

MultiProviderproviders放置的是共享的多个Provider

  1. 其它Widget使用共享的状态
  • Provider.of
class NumberWidget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 读取
    int number = Provider.of(context).number;
    // 读取
    int randomNumber = Provider.of(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(
        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>(
        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,可以直接比较。如果相同就不重构了。

多个状态使用的补充

Consumer2还有几个好兄弟:,Consumer3Consumer4Consumer5Consumer6

Selector2也有几个好兄弟:,Selector3Selector4Selector5Selector6

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

全部代码:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (ctx) => NumberModel(),
        ),
        ChangeNotifierProvider(
          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(
        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(context).number;
    int randomNumber = Provider.of(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(
        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>(
        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的基本架构如下:
Flutter系列十:Flutter状态管理之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的局部刷新逻辑如下:
Flutter系列十:Flutter状态管理之Provider的使用和架构分析
Provider的局部刷新
  1. _value值发生变化,会通知监听者刷新。其中会调用_InheritedProviderScope的markNeedsNotifyDependents方法,调用依赖WidgetdidChangeDependencies, 这两个方法都会调用markNeedsBuild(),进行重构;
  2. Widget重构的时候会调用Provider.of方法,更新对_value的监听,为下次重构做准备。
  • ConsumerSelector的优化逻辑:
Flutter系列十:Flutter状态管理之Provider的使用和架构分析
Consumer

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

  • MultiProvider的逻辑:
Flutter系列十:Flutter状态管理之Provider的使用和架构分析
多个Provider

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

总结

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

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


推荐阅读
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
  • 如何在PHP中准确获取服务器IP地址?
    如何在PHP中准确获取服务器IP地址? ... [详细]
  • 在C#编程中,设计流畅的用户界面是一项重要的任务。本文分享了实现Fluent界面设计的技巧与方法,特别是通过编写领域特定语言(DSL)来简化字符串操作。我们探讨了如何在不使用`+`符号的情况下,通过方法链式调用来组合字符串,从而提高代码的可读性和维护性。文章还介绍了如何利用静态方法和扩展方法来实现这一目标,并提供了一些实用的示例代码。 ... [详细]
  • Python多线程编程技巧与实战应用详解 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 在多年使用Java 8进行新应用开发和现有应用迁移的过程中,我总结了一些非常实用的技术技巧。虽然我不赞同“最佳实践”这一术语,因为它可能暗示了通用的解决方案,但这些技巧在实际项目中确实能够显著提升开发效率和代码质量。本文将深入解析并探讨这四大高级技巧的具体应用,帮助开发者更好地利用Java 8的强大功能。 ... [详细]
  • NOIP2000的单词接龙问题与常见的成语接龙游戏有异曲同工之妙。题目要求在给定的一组单词中,从指定的起始字母开始,构建最长的“单词链”。每个单词在链中最多可出现两次。本文将详细解析该题目的解法,并分享学习过程中的心得体会。 ... [详细]
  • 开发日志:201521044091 《Java编程基础》第11周学习心得与总结
    开发日志:201521044091 《Java编程基础》第11周学习心得与总结 ... [详细]
  • React项目基础教程第五课:深入解析组件间通信机制 ... [详细]
  • PHP编程中的命名规则与最佳实践 ... [详细]
  • 探索聚类分析中的K-Means与DBSCAN算法及其应用
    聚类分析是一种用于解决样本或特征分类问题的统计分析方法,也是数据挖掘领域的重要算法之一。本文主要探讨了K-Means和DBSCAN两种聚类算法的原理及其应用场景。K-Means算法通过迭代优化簇中心来实现数据点的划分,适用于球形分布的数据集;而DBSCAN算法则基于密度进行聚类,能够有效识别任意形状的簇,并且对噪声数据具有较好的鲁棒性。通过对这两种算法的对比分析,本文旨在为实际应用中选择合适的聚类方法提供参考。 ... [详细]
  • 开发笔记:深入解析Android自定义控件——Button的72种变形技巧
    开发笔记:深入解析Android自定义控件——Button的72种变形技巧 ... [详细]
  • 刷题笔记:探索乘积小于K的子数组问题 ... [详细]
  • 本文详细探讨了OpenCV中人脸检测算法的实现原理与代码结构。通过分析核心函数和关键步骤,揭示了OpenCV如何高效地进行人脸检测。文章不仅提供了代码示例,还深入解释了算法背后的数学模型和优化技巧,为开发者提供了全面的理解和实用的参考。 ... [详细]
  • Go语言中的高效排序与搜索算法解析
    在探讨Go语言中高效的排序与搜索算法时,本文深入分析了Go语言提供的内置排序功能及其优化策略。通过实例代码,详细讲解了如何利用Go语言的标准库实现快速、高效的排序和搜索操作,为开发者提供了实用的编程指导。 ... [详细]
author-avatar
艹尛鱈_695
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有