探讨一下flutter不同界面之间的通信

4,569 阅读6分钟

最常见的场景是我们在一个界面执行了某种操作,需要在另一个界面的某个地方(例如一个变量)产生更新等效果,就需要进行界面之间的通信


本文将介绍四种方法来完成这种事

  • 使用scoped_model
  • 使用InheritedWidget
  • 使用key进行访问
  • 父widget创建state后保存供子widget访问


一 使用scoped_model

什么是scoped_model?

Scoped_model是一个dart第三方库,提供了让您能够轻松地将数据模型从父Widget传递到它的后代的功能。此外,它还会在模型更新时重新渲染使用该模型的所有子项。,而且无论该widget是不是有状态的都可以进行更新,再一次build

实现原理

Scoped model使用了观察者模式,将数据模型放在父代,后代通过找到父代的model进行数据渲染,最后数据改变时将数据传回,父代再通知所有用到了该model的子代去更新状态。

用途

可以进行全局公共数据共享,除了使用scoped_model也可以使用shared_preferences、或者新建一个类将共享数据设置成static类的成员。

  • 使用sp的缺点是效率低,需要读写数据库,同时不可以在状态改变后主动发起更新
  • 使用static类成员的缺点也是不可以主动发起类更新
  • 使用scoped_model的速度上更快,并且可以监听变换后自动发起setState变化

具体使用

1 添加依赖

scoped_model: ^0.3.0

2 、创建Model

import 'package:scoped_model/scoped_model.dart';

class CountModel extends Model{
  int _count = 0;
  get count => _count;
  
  void increment(){
    _count++;
    notifyListeners();
  }
}

3、将model放在顶层

//创建顶层状态
  CountModel countModel = CountModel();

  @override
  Widget build(BuildContext context) {
    return ScopedModel<CountModel>(
      model: countModel,
      child: new MaterialApp(
        home: TopScreen(),
      ),
    );
  }

4、在子界面中获取Model

@override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<CountModel>(
      builder: (context,child,model){
        return Scaffold(
          body: Center(
            child: Text(
              model.count.toString(),
              style: TextStyle(fontSize: 48.0),
            ),
          ),
        );
      },
    );
  }

5 一个简单的demo可以直接运行

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


class CountModel extends Model{
  int _count = 0;
  get count => _count;

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

void main()
{
  CountModel countModel = CountModel();
  runApp(new ScopedModel(model: countModel, child: MaterialApp(
    home: PageA(),
  )));
}

class PageA extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return new _PageA();
  }
}

class _PageA extends State<PageA>
{
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return ScopedModelDescendant<CountModel>(
      builder: (context,child,model)
      {
        return Scaffold(
          appBar: AppBar(
            title: Text(model.count.toString()),
          ),
          body: RaisedButton(
            onPressed: (){model.increment();},
          ),
        );
      },
    );
  }
}


二 使用InhertedWidget

什么是InheritedWidget?

InheritedWidget 是一个特殊的 Widget,它将作为另一个子树的父节点放置在 Widget 树中。该子树的所有 widget 都必须能够与该 InheritedWidget 暴露的数据进行交互


为了更好理解概念,我们看一个例子

class MyInheritedWidget extends InheritedWidget {
   MyInheritedWidget({
      Key key,
      @required Widget child,
      this.data,
   }): super(key: key, child: child);

   final data;

   static MyInheritedWidget of(BuildContext context) {
      return context.inheritFromWidgetOfExactType(MyInheritedWidget);
   }

   @override
   bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}

以上代码定义了一个名为 “MyInheritedWidget” 的 Widget,目的在于为子树中的所有 widget 提供某些『共享』数据。

如前所述,为了能够传播/共享某些数据,需要将 InheritedWidget 放置在 widget 树的顶部,这解释了传递给 InheritedWidget 基础构造函数的 @required Widget child 参数。

static MyInheritedWidget of(BuildContext context) 方法允许所有子 widget 通过包含的 context 获得最近的 MyInheritedWidget 实例(参见后面的内容)。

最后重写 updateShouldNotify 方法用来告诉 InheritedWidget 如果对数据进行了修改,是否必须将通知传递给所有子 widget(已注册/已订阅)。

因此,使用时我们需要把它放在父节点

class MyParentWidget... {
   ...
   @override
   Widget build(BuildContext context){
      return new MyInheritedWidget(
         data: counter,
         child: new Row(
            children: <Widget>[
               ...
            ],
         ),
      );
   }
}

子结点如何访问InhertedWidget中的数据呢?

class MyChildWidget... {
   ...

   @override
   Widget build(BuildContext context){
      final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);

      ///
      /// 此刻,该 widget 可使用 MyInheritedWidget 暴露的数据
      /// 通过调用:inheritedWidget.data
      ///
      return new Container(
         color: inheritedWidget.data.color,
      );
   }
}

InhertedWidget的高级用法

上文讲到的用法有一个明显的缺点是无法实现类似于Scoped_model的自动更新,为此,我们将inhertedWidget依托于一个statefulWidget实现更新

场景如下:



   为了说明交互方式,我们做以下假设:

  • ‘Widget A’ 是一个将项目添加到购物车里的按钮;
  • ‘Widget B’ 是一个显示购物车中商品数量的文本;
  • ‘Widget C’ 位于 Widget B 旁边,是一个内置任意文本的文本;
  • 我们希望 ‘Widget A’ 在按下时 ‘Widget B’ 能够自动在购物车中显示正确数量的项目,但我们不希望重建 ‘Widget C’

示例代码如下

class Item {
   String reference;

   Item(this.reference);
}

class _MyInherited extends InheritedWidget {
  _MyInherited({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final MyInheritedWidgetState data;

  @override
  bool updateShouldNotify(_MyInherited oldWidget) {
    return true;
  }
}

class MyInheritedWidget extends StatefulWidget {
  MyInheritedWidget({
    Key key,
    this.child,
  }): super(key: key);

  final Widget child;

  @override
  MyInheritedWidgetState createState() => new MyInheritedWidgetState();

  static MyInheritedWidgetState of(BuildContext context){
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedWidgetState extends State<MyInheritedWidget>{
  /// List of Items
  List<Item> _items = <Item>[];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  /// Helper method to add an Item
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State<MyTree> {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: <Widget>[
            new WidgetA(),
            new Container(
              child: new Row(
                children: <Widget>[
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

说明

在这个非常基本的例子中:

  • _MyInherited 是一个 InheritedWidget,每次我们通过 ‘Widget A’ 按钮添加一个项目时它都会重新创建
  • MyInheritedWidget 是一个 State 包含了项目列表的 Widget。可以通过 static MyInheritedWidgetState of(BuildContext context) 访问该 State
  • MyInheritedWidgetState 暴露了一个获取 itemsCount 的 getter 方法 和一个 addItem 方法,以便它们可以被 widget 使用,这是 widget 树的一部分
  • 每次我们将项目添加到 State 时,MyInheritedWidgetState 都会重建
  • MyTree 类仅构建了一个 widget 树,并将 MyInheritedWidget 作为树的根节点
  • WidgetA 是一个简单的 RaisedButton,当按下它时,它将从最近MyInheritedWidget 实例中调用 addItem 方法
  • WidgetB 是一个简单的 Text,用来显示最近 级别 MyInheritedWidget 的项目数


三 使用key进行访问

什么是key?

在 Fultter 中,每一个 Widget 都是被唯一标识的。这个唯一标识在 build/rendering 阶段由框架定义。

该唯一标识对应于可选的 Key 参数。如果省略该参数,Flutter 将会为你生成一个。

在某些情况下,你可能需要强制使用此 key,以便可以通过其 key 访问 widget。

为此,你可以使用以下方法中的任何一个:GlobalKeyLocalKeyUniqueKeyObjectKey

GlobalKey 确保生成的 key 在整个应用中是唯一的。


强制使用GlobalKey的方法

GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
    return new MyWidget(
        key: myKey
    );
}


使用GlobalKey访问State

class _MyScreenState extends State<MyScreen> {
    /// the unique identity of the Scaffold
    final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

    @override
    Widget build(BuildContext context){
        return new Scaffold(
            key: _scaffoldKey,
            appBar: new AppBar(
                title: new Text('My Screen'),
            ),
            body: new Center(
                new RaiseButton(
                    child: new Text('Hit me'),
                    onPressed: (){
                        _scaffoldKey.currentState.showSnackBar(
                            new SnackBar(
                                content: new Text('This is the Snackbar...'),
                            )
                        );
                    }
                ),
            ),
        );
    }
}


四 父widget创建state后保存供子widget访问

假设你有一个属于另一个 Widget 的子树的 Widget,如下图所示。


使用步骤如下

1. 『带有 State 的 Widget』(红色)需要暴露其 State

为了暴露State,Widget 需要在创建时记录它,如下所示:

class MyExposingWidget extends StatefulWidget {

   MyExposingWidgetState myState;

   @override
   MyExposingWidgetState createState(){
      myState = new MyExposingWidgetState();
      return myState;
   }
}
复制代码

2. “Widget State” 需要暴露一些 getter/setter 方法

为了让“其他类” 设置/获取 State 中的属性,Widget State 需要通过以下方式授权访问:

  • 公共属性(不推荐)
  • getter / setter

例子:

class MyExposingWidgetState extends State<MyExposingWidget>{
   Color _color;

   Color get color => _color;
   ...
}
复制代码

3. “对获取 State 感兴趣的 Widget”(蓝色)需要得到 State 的引用

class MyChildWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context){
      final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
      final MyExposingWidgetState state = widget?.myState;

      return new Container(
         color: state == null ? Colors.blue : state.color,
      );
   }
}