Flutter 页面间数据传递(共享)的几种常用方式

前言

    在Android中,我们常遇到的场景就是在页面跳转(Frament,Activity)时候,要将当前的部分数据携带到另外一个页面中,供另外页面使用。这时候我们常用的就是使用Intent, Bundle等携带数据。

    那么在Flutter的开发过程中,页面之间的数据传递也是必不可少的,又是怎么把一个页面的数据传递(共享)给另外一个页面,或者关闭当前页面并把当前页面的数据带给前一个页面。

    本篇文章将会介绍Flutter中,页面面之间的数据传递(共享)的几种常见方式及场景。

在开始数据传递之前我们先创建一个传递数据的类

在Android中传递对象我们需要序列化实现Serializable或者Parcelable接口才能被传递,在Flutter中数据传递没有序列化的方法,直接就可以传递对象。定义一个简单的类如下:
///用来传递数据的实体
class TransferDataEntity {
  String name;
  String id;
  int age;

  TransferDataEntity(this.name, this.id, this.age);
}

我们具体看看数据传递的方式

通过构造器(constructor)传递数据

    通过构造器传递数据是一种最简单的方式,也是最常用的方式,在第一个页面,我们模拟创建一个我们需要传递数据的对象。当点击跳转的时候,我们把数据传递给DataTransferByConstructorPage页面,并把携带过来的数据展示到页面上。

  • 创建一个传递数据对象

    final data = TransferDataEntity("001", "张三丰", 18);
  • 定义一个跳转到DataTransferByConstructorPage页面的方法

    _transferDataByConstructor(BuildContext context, TransferDataEntity data) {
        Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => DataTransferByConstructorPage(data: data)));
      }
  • 在DataTransferByConstructorPage页面接收到数据并展示出来,代码如下

    我们只需要做两件事:

    1.提供一个final变量 final TransferDataEntity data

    2.提供一个构造器接收参数 DataTransferByConstructorPage({this.data});

    ///通过构造器的方式传递参数
    class DataTransferByConstructorPage extends StatelessWidget {
      final TransferDataEntity data;
    
      DataTransferByConstructorPage({this.data});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("构造器方式"),
          ),
          body: Column(
            children: <Widget>[
              Container(
                width: double.infinity,
                height: 40.0,
                alignment: Alignment.center,
                child: Text(data.id),
              ),
              Container(
                width: double.infinity,
                height: 40.0,
                alignment: Alignment.center,
                child: Text(data.name),
              ),
              Container(
                width: double.infinity,
                height: 40.0,
                alignment: Alignment.center,
                child: Text("${data.age}"),
              )
            ],
          ),
        );
      }
    }

当一个页面关闭时携带数据到上一个页面(Navigator.pop)

在Android开发中我们需要将数据传递给上一个页面通常使用的传统方式是startActivityForResult()方法。但是在flutter就不用这么麻烦了。只需要使用Navigator.pop方法即可将数据结果带回去。但是我们跳转的时候需要注意两点:

1.我们需要定义一个异步方法用于接收返回来的结果

///跳转的时候我们需要使用异步等待回调结果 dataFromOtherPage 就是返回的结果
_toTransferForResult(BuildContext context, TransferDataEntity data) async {
    final dataFromOtherPage = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => TransferRouterPage(data: data)),
    ) as TransferDataEntity;
  }

2.在我们要关闭的页面使用Navigator.pop 返回第一个页面

//返回并携带数据
  _backToData(BuildContext context){
    var transferData = TransferDataEntity("嘻嘻哈哈","007",20);
    Navigator.pop(context,transferData);
  }

InheritedWidget方式

官网给出的解释:InheritedWidget是Flutter中非常重要的一个功能型Widget,它可以高效的将数据在Widget树中向下传递、共享,这在一些需要在Widget树中共享数据的场景中非常方便,如Flutter中,正是通过InheritedWidget来共享应用主题(Theme)和Locale(当前语言环境)信息的。

InheritedWidget和React中的context功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget的在Widget树中数据传递方向是从上到下的,这和Notification的传递方向正好相反。

优点:可以控制每个Widget单独去取数据并使用

如果一个页面只存在同一层级的Weiget这时候使用构造器的方式当然是最简单的,也是最方便的,但是如果一个页面存在多个层级的Weiget这时候构造器的方法就有了局限性,这时候我们使用InheritedWidget 是一个比较好的选择。

使用InheritedWidget方式如下几步:

  • 继承InheritedWidget提供一个数据源

    class IDataProvider extends InheritedWidget{
    
      final TransferDataEntity data;
    
      IDataProvider({Widget child,this.data}):super(child:child);
    
    
      @override
      bool updateShouldNotify(IDataProvider oldWidget) {
        return data!=oldWidget.data;
      }
    
      static IDataProvider of(BuildContext context){
        return context.inheritFromWidgetOfExactType(IDataProvider);
      }
    }
  • 定义页面跳转时候携带数据的方法

    ///跳转到IDataWidget页面并携带数据
    _inheritedToPage(BuildContext context, TransferDataEntity data) {
        Navigator.push(
            context,
            MaterialPageRoute(
                builder: (context) => IDataProvider(
                      child: IDataWidget(),
                      data: data,
                    )));
      }
  • 跳转的到的页面并展示数据代码如下

    class IDataWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final data = IDataProvider.of(context).data;
    
        return Scaffold(
          appBar: AppBar(
            title: Text("Inherited方式传递数据"),
          ),
          body: Column(
            children: <Widget>[
              Container(
                alignment: Alignment.center,
                height: 40.0,
                child: Text(data.name),
              ),
              Container(
                alignment: Alignment.center,
                height: 40.0,
                child: Text(data.id),
              ),
              Container(
                alignment: Alignment.center,
                height: 40.0,
                child: Text("${data.age}"),
              ),
              IDataChildWidget()
            ],
          ),
        );
      }
    }
    
    class IDataChildWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final data = IDataProvider.of(context).data;
        return Container(
          child: Text(data.name),
        );
      }
    }

我们将上面的IDataProvier进行改造加入泛型就可以通用了

1.修改后的Provider类如下

class IGenericDataProvider<T> extends InheritedWidget {
  final T data;

  IGenericDataProvider({Key key, Widget child, this.data})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(IGenericDataProvider oldWidget) {
    return data != oldWidget.data;
  }

  static T of<T>(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(
            IGenericDataProvider<T>().runtimeType) as IGenericDataProvider<T>).data;
  }
}

2.使用跳转的时候修改代码如下(主要是添加泛型支持)

_inheritedGenericToPage(BuildContext context, TransferDataEntity data) {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => IGenericDataProvider<TransferDataEntity>(
              child: IDataWidget(),
              data: data,
            )));
  }

接收传递的值的方式如下

IGenericDataProvider.of<TransferDataEntity>(context) 可以直接取值

class IGenericDataWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final data = IGenericDataProvider.of<TransferDataEntity>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text("Inherited泛型方式传递数据"),
      ),
      body: Column(
        children: <Widget>[
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.name),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.id),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text("${data.age}"),
          ),
        ],
      ),
    );
  }
}

全局的提供数据的方式

这种方式我们还是使用InheritedWidget,区别就是我们不是跳转的时候去创建IGenericDataProvider。而是把他放在最顶层

注意:这种方式一定要把数据放在顶层

定义顶部数据

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  //传递值的数据
  var params  = InheritedParams();

  @override
  Widget build(BuildContext context) {
    return IGenericDataProvider(
      data: params,
      child: MaterialApp(
        title: 'Data Transfer Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: 'Data Transfer Demo'),
      ),
    );
  }
}

接收数据的方式基本和InheritedWidget相同

final data = IGenericDataProvider.of<TransferDataEntity>(context),获取数据

class InheritedParamsPage extends StatefulWidget {
  @override
  _InheritedParamsPageState createState() => _InheritedParamsPageState();
}

class _InheritedParamsPageState extends State<InheritedParamsPage> {
  @override
  Widget build(BuildContext context) {
    final data = IGenericDataProvider.of<TransferDataEntity>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("通过全局数据方式"),
      ),
      body:Column(
        children: <Widget>[
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.name),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.id),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text("${data.age}"),
          ),
        ],
      ),
    );
  }
}

通过全局单例模式来使用

这种方式就是创建一个全局单例对象,任何地方都可以操控这个对象,存储和取值都可以通过这个对象

  • 创建单例对象
class TransferDataSingleton {
  static final TransferDataSingleton _instanceTransfer =
      TransferDataSingleton.__internal();

  TransferDataEntity transData;

  factory TransferDataSingleton() {
    return _instanceTransfer;
  }

  TransferDataSingleton.__internal();
}

final transSingletonData = TransferDataSingleton();

  • 给单例对象存放数据
_singletonDataTransfer(BuildContext context) {
    var transferData = TransferDataEntity("二汪", "002", 25);
    transSingletonData.transData = transferData;
    Navigator.push(context,
        MaterialPageRoute(builder: (context) => TransferSingletonPage()));
  }

  • 接收并使用传递的值
class TransferSingletonPage extends StatefulWidget {
  @override
  _TransferSingletonPageState createState() => _TransferSingletonPageState();
}

class _TransferSingletonPageState extends State<TransferSingletonPage> {
  @override
  Widget build(BuildContext context) {
    //直接引入单例对象使用
    var data = transSingletonData.transData;
    return Scaffold(
      appBar: AppBar(
        title: Text("全局单例传递数据"),
      ),
      body: Column(
        children: <Widget>[
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.name),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text(data.id),
          ),
          Container(
            alignment: Alignment.center,
            height: 40.0,
            child: Text("${data.age}"),
          ),
        ],
      ),
    );
  }
}

全局单例结合Stream的方式传递数据

  • 创建一个接受全局的单例对象,并把传递值转成Stream方式
  • 在接收数据可以使用StreamBuilder直接接收并处理
class TransferStreamSingleton {
  static final TransferStreamSingleton _instanceTransfer =
      TransferStreamSingleton.__internal();
  StreamController streamController;

  void setTransferData(TransferDataEntity transData) {
    streamController = StreamController<TransferDataEntity>();
    streamController.sink.add(transData);
  }

  factory TransferStreamSingleton() {
    return _instanceTransfer;
  }

  TransferStreamSingleton.__internal();
}

final streamSingletonData = TransferStreamSingleton();

  • 传递要携带的数据
_streamDataTransfer(BuildContext context) {
    var transferData = TransferDataEntity("三喵", "005", 20);
    streamSingletonData.setTransferData(transferData);
    Navigator.push(context,
        MaterialPageRoute(builder: (context) => TransferStreamPage()));
  }

  • 接收要传递的值
class TransferStreamPage extends StatefulWidget {
  @override
  _TransferStreamPageState createState() => _TransferStreamPageState();
}

class _TransferStreamPageState extends State<TransferStreamPage> {

  StreamController _streamController = streamSingletonData.streamController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("全局单例结合Stream"),
        ),
        body: StreamBuilder(
                stream: _streamController.stream,
                initialData: TransferDataEntity("", "", 0),
                builder: (context, snapshot) {
                  return Column(
                    children: <Widget>[
                      Container(
                        alignment: Alignment.center,
                        height: 40.0,
                        child: Text(snapshot.data.name),
                      ),
                      Container(
                        alignment: Alignment.center,
                        height: 40.0,
                        child: Text(snapshot.data.id),
                      ),
                      Container(
                        alignment: Alignment.center,
                        height: 40.0,
                        child: Text("${snapshot.data.age}"),
                      ),
                    ],
                  );
                }));
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }
}

总结

    以上是我们在在Flutter中常用的几种页面之间传递数据的方式,其中最后一种方式提到了StreamStreamBuilder我有一篇文章专门介绍了FlutterStreamFlutter Stream简介及使用 详细的介绍了Stream及部分操作的使用。

    现在官方推荐的provider实际上就是使用了InheritedWidget有时间的话建议详细了下InheritedWidget及使用方法。

    以上是对页面之间值传递的一个总结,本文Demo,如有写的不足之处,望指正~