Flutter学习总结(五)状态管理器

Flutter是声明式编程模式,开发者就像堆积木一样来堆模型,只用关心每块积木展示的条件,不需要关心积木内部的功能实现细节。因此每一块积木都是一种状态,慢慢随着积木越来越多开发者必须要把这些状态(形态、关系)管理好,否则就会是一团麻了。

编程语言主要有三种类型

  • 声明式编程:专注于”做什么”而不是”如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程。
    如:css, 正则表达式,sql 语句,html, xml…
  • 命令式编程(过程式编程) : 专注于”如何去做”,这样不管”做什么”,都会按照你的命令去做。解决某一问题的具体算法实现。
  • 函数式编程:把运算过程尽量写成一系列嵌套的函数调用。

iOS 和 Android 的原生开发模式是命令式编程模式。命令式编程要求开发者一步步描述整个构建过程,从而引导程序去构建用户界面。

常见状态管理器对比

状态管理器一般会关注两点:

  • 状态的更新
  • 状态的共享

以下是一些常见的状态管理对比

状态管理框架 状态更新 状态共享 说明
setState yes no 自带的状态管理器,适用于较小规模 widget 的暂时性状态的基础管理方法
StreamController yes no 基于流/观察者模式的基础管理方法
RxDart yes no 基于流/观察者模式的框架,对StreamController进行了高级封装
flutter_bloc yes yes 底层基于 InheritedWidget,核心思想也是基于流来管理数据
Provider yes yes 基于 InheritedWidget 和 ChangeNotifier 进行了封装,使用缓存提升性能,避免不必要的重绘
Fish-Redux yes yes 闲鱼出品,基于前端Redux思想,似乎不再维护
GetX yes yes 轻量级响应式状态管理解决方案,目前很受欢迎

InheritedWidget

系统自带的数据共享组件,例如我们在应用根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子 widget 中来获取该共享的数据。

创建方式:

class MyInheritedWidget extends InheritedWidget {
  final int data;

  /// 在构造方法中,我们需要传入两个参数:
  /// 一个是我们希望共享的数据(在本例中数据是int型,实际业务中共享的通常是一个相对复杂的数据),
  /// 另一个就是我们带界面的Widget。
  MyInheritedWidget(@required this.data, Widget child) : super(child: child);

  /// 获取数据方法
  static MyInheritedWidget getData(BuildContext context) {
    return context.inheritFromWidgetOfExactType(MyInheritedWidget);
  }

  /// 决定通知子节点中StatefulWidget的didChangeDependencies方法是否调用。
  /// StatefulWidget的didChangeDependencies方法就是与InheritedWidget配合使用的。只有当InheritedWidget发生更新并且决定通知时,didChangeDependencies才会调用。
  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.data != data;
  }
}

使用场景:

@override
Widget build(BuildContext context) {
    return Text(
        'count : ${MyInheritedWidget.getData(context).data}'
    );
}

StreamController

RxDart、BloC、flutter_bloc 都是基于 Stream 开发,Stream 的思想是基于管道(pipe)和 生产者消费者模式。

Stream并是不Flutter的产物,而是由Dart提供的,Stream是一个抽象的接口,Dart提供了StreamController接口类可以让我们方便的使用Stream。步骤大致如下:

  • 创建StreamController
  • 获取StreamSink,用作发射事件
  • 获取Stream流,可用作事件的监听
  • 获取StreamSubscription,用作管理监听、关闭、暂停等
  //创建StreamController
  StreamController streamController = StreamController();
  // 获取StreamSink用于发射事件
  StreamSink get streamSink  => streamController.sink;
  // 获取Stream用于监听
  Stream get streamData => streamController.stream;

  //发射一个事件.
  streamSink.add(index.toString());

  //监听事件,返回StreamSubscription,可用于监听的取消
  StreamSubscription subscription = streamData.listen((value) {
        // do something
  });

StreamBuilder

StreamBuilder对Stream相关逻辑的包装组件(Widget),StreamBuilder 内部已经帮我们完成了stream的订阅与取消订阅,因此在Widget中监听数据变化及时刷新界面更方便。

构造方法:

StreamBuilder({Key key, T initialData, Stream stream, @required AsyncWidgetBuilder builder })
  • initialData : 默认初始化数据
  • stream : stream事件流对象
  • builder : 接收两个参数 BuildContext contextAsyncSnapshot snapshot , 返回值是 Widget 组件 ; AsyncSnapshot snapshot 参数中包含有异步计算的信息;

其他方法:

  • afterConnected:返回一个AsyncSnapshot,当订阅了stream时会回调此AsyncSnapshot
  • afterData:返回一个AsyncSnapshot,当stream有事件触发时会回调此AsyncSnapshot
  • afterDisconnected:返回一个AsyncSnapshot,当取消订阅stream时会回调此AsyncSnapshot
  • afterDone:返回一个AsyncSnapshot,当stream被关闭时会回调此AsyncSnapshot
  • afterError:返回一个AsyncSnapshot,stream发生错误时会回调此AsyncSnapshot

接收数据:

@override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('streamBuilder')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            StreamBuilder(
              stream: streamData,
              builder: (BuildContext context, AsyncSnapshot snapshot) {
                return Text('Result: ${snapshot.data}');
              }
            )
          ],
        )
      )
    );
  }

Provider

8种内容提供者,常用的有:

  1. MultiProvider,多种复合嵌套,不建议入口处同时初始化多个provider,容易引起内存瞬间加大,除了一些特殊场景,例如日夜间切换,和语言切换。
  2. ChangeNotifyProvider, 使用时继承 ChangeNotifier即可。

provider 实例化及取值也有三种:

  1. XXXProvider provider = Provider.of(context);
  2. 采用Consume
  3. 调用Provider的方法:context.read().xxx();
    观察Provider的数据,用于展示:context.watch().xxx;

实例:

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

void main() {
  runApp(MyApp());
}

/// 1 创建ChangeNotifier,实际上就是我们的状态,它不仅存储了我们的数据模型,还包含了更改数据的方法,并暴露出它想要暴露出的数据
class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    /// 2 ChangeNotifierProvider. 在Widget Tree中插入ChangeNotifierProvider,以便Consumer可以获取到数据
    ///
    /// 创建顶层共享数据。这里使用MultiProvider可以创建多个共享数据,因为实际的应用不可能只有一个数据模型
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: MaterialApp(
        title: 'Flutter Demo',
        home: FirstPage(),
      ),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Provider练习")),
      body: Center(
        /// 3 获取Provider实例
        /// ctx: context,上下文,目的是知道当前树的对象
        /// counterPro: ChangeNotifier对应的实例,也是我们在builder函数中主要使用的对象
        /// child: 目的是进行优化,如果builder下面有一颗庞大的子树,当模型发生改变的时候,我们并不希望重新build这颗子树,那么就可以将这颗子树放到Consumer的child中,在这里直接引入即可.
        child: Consumer(builder: (ctx, counterPro, child) {
          return Text(
            "当前计数:${counterPro.count}",
            style: TextStyle(fontSize: 20, color: Colors.red),
          );
        }),
      ),

      floatingActionButton: Consumer(
        builder: (ctx, counterPro, child) {
          return FloatingActionButton(
            child: child,
            onPressed: () {
              counterPro.increment();
            },
          );
        },
        child: Icon(Icons.add), //! Icon放在builder外面,防止每次跟着一起刷新
      ),
    );
  }
}

GetX

GetX相比Provider更灵活,轻量,简单,api也更丰富,除了包含状态管理,还包含路由配置,另外不在依赖Context,全局随便调用。

快速上手

  1. 入口,将MaterialApp改成GetMaterialApp
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// GetMaterialApp重写了MaterialApp,可以配置很多,如,主题、国际化、静态路由等
    return GetMaterialApp(
      home: SimplePage(),
    );
  }
}
  1. 创建GetxController,用于管理数据和刷新操作
class SimpleController extends GetxController {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    // 刷新(GetxController通过update()更新GetBuilder)
    update();
  }
}
  1. 采用GetBuild构建视图,监听数据变化
class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('SimplePage--build');
    return GetBuilder(
        init: SimpleController(),
        builder: (controller) {
          
          return Scaffold(
            appBar: AppBar(title: Text('Simple')),
            body: Center(
              child: Text(controller.counter.toString()),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                controller.increment();
              },
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Simple')),
      body: Center(
        child: GetBuilder(
            init: SimpleController(),
            // 每次执行update,builder内部都会重新构建
            builder: (controller) {
              return Text(controller.counter.toString());
            }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 找到Controller执行操作
          Get.find().increment();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

注:GetBuilder用于小部件的刷新,不建议放到根节点上全局刷。

局部刷新

update()使所有绑定了该Controller的GetBuilder都执行了刷新,可通过追加id,进行局部刷新:

Controller:

  int _counter = 0;
  int get counter => _counter;

  String _name = "Lili";
  String get firstName => _name;
  
    void increment() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update(['counter']);
  }

  void changeName() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update(['name']);
  }

GetBuilder:

GetBuilder(
            id: 'counter',
            builder: (ctl) => Text(ctl.counter.toString()),
          ),
          SizedBox(
            height: 50,
          ),
          GetBuilder(
            id: 'name',
            builder: (ctl) => Text(ctl.firstName),
          ),

GetxController生命周期

class SimpleController extends GetxController {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    update();
  }

  @override
  void onInit() {
    super.onInit();
    print('SimpleController--onInit');
  }

  @override
  void onReady() {
    super.onReady();
    print('SimpleController--onReady');
  }

  @override
  void onClose() {
    super.onClose();
    print('SimpleController--onClose');
  }
}

响应式刷新

在Controller的变量后追加.obs创建响应式数据,使得该变量变成了可观察者:

var name = '新垣结衣'.obs;

观察变量的改变,不在需要update,不在是GetBuilder了:

Obx (() => Text (controller.name));

因为是响应式,只要数据改变,界面布局自动刷新。

细心的同学会发现.obs的底层是Rx包装,所以也可以用Rx来定义响应式变量,这里不在说了。

跨路由使用

不同页面跨路由使用,只需要把controller存储起来即可:

class CrossOnePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// Get.put存储controller
    CrossOneController controller = Get.put(CrossOneController());
...
}}

另外一个页面使用时,获取controller:

  CheetahButton('打印CrossOneController的age', () {
    /// Get.find获取controller
            print(Get.find().age);
          }),

此时需要注意一个问题:

  • Get.put在build里put时,controller的生命周期和该widget保持一致的,widget销毁了这个controller也自动销毁了
  • Get.put作为成员变量在build外部执行时,controller的生命周期是和所属widget的引用保持一致的。

我们开发过程中总喜欢一个页面对应一个controller,如果作为成员变量存放,这样的话需要额外注意内存的回收问题,可以建一个Widget,来管理controller的回收。

其他

路由、弹框需自行进一步挖掘。

你可能感兴趣的:(Flutter,flutter,android,状态管理器)