Flutter 頁面間資料傳遞(共享)的幾種常用方式

奮鬥的小土豆發表於2019-08-08

前言

    在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(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(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,如有寫的不足之處,望指正~

相關文章