Flutter数据共享

105 阅读3分钟

state

Flutter采用的是声明式的编程方式,和iOS命令式的不同,声明式编程需要大量的state来进行管理,通过改变state来驱动UI进行更新。因此我们需要对state有一个清晰的认识。

局部状态:是可以完全包含在一个独立的widget中的状态,widget树中其他部分不需要访问这种状态。

全局状态:在应用的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态。

局部状态我们自己在widget中管理即可,而全局状态就需要我们科学的进行统一管理和维护,避免代码出现高耦合和低维护的情况。

下面这几种都是Flutter中用来管理状态的方式,没有好坏之分,只有使用场景不同。

InheritedWidget

InheritedWidget提供了在widget树中从上到下的数据共享方式。

我们先来简单使用一下InheritedWidget。

先创建一个继承自InheritedWidget的子Widget

import 'package:flutter/material.dart';

class ShareDataWidget extends InheritedWidget {
  ShareDataWidget({required this.data, super.key, required super.child});

  final int data; // 需要在子树中共享的数据

  // 定义一个便捷方法,方便子树中的widget获取共享数据
  static ShareDataWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  }

  // data发生变化时是否通知子树中使用data的widget重新build
  @override
  bool updateShouldNotify(ShareDataWidget old) {
    return old.data != data;
  }
}

然后创建一个使用该Widget组件

import 'package:flutter/material.dart';
import 'package:myapp/share_data_widget.dart';

class TestWidget extends StatefulWidget {
  const TestWidget({super.key});

  @override
  State<TestWidget> createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('didChangeDependencies');
  }

  @override
  Widget build(BuildContext context) {
    return Text('第' + ShareDataWidget.of(context)!.data.toString() + '次');
  }
}

最后在示例程序中使用

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            ShareDataWidget(
              data: _counter,
              child: const TestWidget(),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );

可见在InheritedWidget中保存的数据在它的所有子孙Widget中都能获取到,并且,当数据发生改变的时候会更新依赖于它的子孙组件(通过didChangeDependencies()build())。

我们可以使用getElementForInheritedWidgetOfExactType()来替换dependOnInheritedWidgetOfExactType()。区别在于dependOnInheritedWidgetOfExactType()会调用dependOnInheritedElement来注册依赖于它的子孙组件,之后,当数据发生变化的时候就会更新注册的子孙组件。

provider

image.png

provider是基于InheritedWidget的,所以,当不同页面不同的widget想要使用相同的数据的时候,就应该把这个共享数据置于更上层的widget中。

这里我们用一个CountModel来保存一个数据count

class CountModel extends ChangeNotifier {
  int _count = 0;

  int currentCount() {
    return _count;
  }

  void incrementCount() {
    _count += 1;
    notifyListeners();
  }
}

然后将该数据模型保存在上层widget中(实例保存在了顶层widget中):

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CountModel(),
      child: const MyApp(),
    ),
  );
}

这样当数据发生变化后会通知ChangeNotifierProvider,ChangeNotifierProvider内部会重新构建InheritedWidget,从而更新依赖于InheritedWidget的子孙组件。

最后通过Consumer来使用数据:

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            const Text('当前的数字'),
            Consumer<CountModel>(
              builder: ((context, countM, child) {
                return Text('${countM.currentCount()}');
              }),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          var counter = context.read<CountModel>();
          counter.incrementCount();
        },
        child: const Icon(
          Icons.add,
          color: Colors.white,
        ),
      ),
    );
  }
}

尽量在实际使用数据的widget外层来包裹Consumer,这样可以尽可能少的重新构建widget。

如果不需要数据模型改变的时候刷新ui,只是访问该数据,那么可以使用Provider.of,并且将 listen 设置为 false

Provider.of<CountModel>(context, listen: false).currentCount();

优势:

  1. 只要数据发生了改变,UI会自动更新,而不用手动调用setState()
  2. 数据改变所带来的通知传递被封装在provider中了,开发者不需要去处理状态改变事件的发布和订阅了。

GetX

Provider是基于InheritedWidget,全局状态的管理基于自上而下的context,所以这种方案就限制了状态管理必须在父子Widget中,会使得业务逻辑与视图之间有较强的耦合。而GetX不需要上下文,有自己的生命周期,,所以可以不依赖界面中的任何东西。

先来简单使用一下,还是以计数器为例。

首先我们还是先创建一个数据模型类,这个在GetX中被称为controller:

class CountController extends GetxController {
  int _count = 0;
  int get counter => _count;
  void incrementCount() {
    _count++;
    update();
  }
}

在使用中不需要像provider那样在父widget中保存数据模型,只需要在用到数据模型的widget外部包裹GetBuilder即可:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return GetBuilder<CountController>(
        init: CountController(),
        builder: (controller) {
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: Center(
              child: Column(
                children: [
                  Text('当前的数字: ${controller.counter}'),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                controller.incrementCount();
              },
              child: const Icon(
                Icons.add,
                color: Colors.white,
              ),
            ),
          );
        });
  }
}

我们可以让builder的范围更小一点:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            GetBuilder<CountController>(
              init: CountController(),
              builder: (controller) => Text('当前的数字: ${controller.counter}'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          //controller.incrementCount();
          Get.find<CountController>().incrementCount();
        },
        child: const Icon(
          Icons.add,
          color: Colors.white,
        ),
      ),
    );
  }
}

调整了两个位置,GetBuilder只包裹了Text,这个时候FloatingActionButton并不在GetBuilder的作用域里面,所以使用Get.find来找到数据模型进行修改,这里我们也可以看出,GetX是不需要像Provider那样依赖context的。

这里只是简单的使用了一下GetX,主要是用于和Flutter中其他的数据共享方式做一个简单的区别,GetX还有很多其他好用的功能,比如路由功能、obs响应式开发等等就不在此一一赘述了。