Flutter開發日記-資料傳遞/狀態管理的方式和應用

YYDev發表於2019-12-31

Flutter開發日記-資料傳遞/狀態管理的方式和應用

背景

本文對Flutter常見的資料共享方式進行了總結,方便今後開發中的使用和補充。

部分demo為搬運的例子。

屬性傳值

特點:同一元件樹 逐層傳遞

實現:通過構造器傳遞資料

class DataTransferByConstructorPage extends StatelessWidget {
  final TransferDataEntity data;

  DataTransferByConstructorPage({this.data});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("構造器方式"),
      ),
      body: Text('test')
    );
  }
}
// 父元件通過構造器傳遞data屬性
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    TransferDataEntity data = new TransferDataEntity()
    return DataTransferByConstructorPage(data)
  }
}

複製程式碼

問題:

當我們需要跨層傳遞時 就需要逐層給子元件傳入引數,

資料量增加或者 樹的深度增加時

實現起來就十分麻煩

這種需要跨層傳遞的場景,我們就可以使用 InheritedWidget

// 盜一張官方的圖

Flutter開發日記-資料傳遞/狀態管理的方式和應用

InheritedWidget

特點:同一元件樹傳遞資料 從上到下 跨層傳遞,是flutter官方提供的功能型元件

找了個demo---只有讀功能:

class CountContainer extends InheritedWidget {
  // 方便其子 Widget 在 Widget 樹中找到它
  static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
  
  final int count;

  CountContainer({
    Key key,
    @required this.count,
    @required Widget child,
  }): super(key: key, child: child);

  // 判斷是否需要更新
  @override
  bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}



class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
   // 將 CountContainer 作為根節點,並使用 0 作為初始化 count
    return CountContainer(
      count: 0,
      child: Counter()
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 獲取 InheritedWidget 節點
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${state.count}',
      ),
    );
}
複製程式碼

1 CountContainer繼承自InheritedWidget,CountContainer 宣告瞭一個final屬性和of方法,of方法返回CountContainer物件,方便子widget在widget找到它並且獲取屬性值。

2 重寫了 updateShouldNotify 方法,當count修改時 通知繼承他的widget更新

3 在我們的頁面中 將我們的檢視widget作為child傳遞給CountContainer,並使用靜態方法of獲取到當前上下文的CountContainer,以此讀取他的屬性

找了另外一個demo---寫功能:


class CounterPage extends StatefulWidget {
  CounterPage({Key key}) : super(key: key);


  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {

  int count = 0;

  void _incrementCounter() => setState(() {count++;});


  @override
  Widget build(BuildContext context) {

    return CountContainer(
      //increment: _incrementCounter,
        model: this,
        increment: _incrementCounter,
        child:Counter()
    );
  }
}

class CountContainer extends InheritedWidget {
  static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;

  final _CounterPageState model;

  final Function() increment;


  CountContainer({
    Key key,
    @required this.model,
    @required this.increment,
    @required Widget child,
  }): super(key: key, child: child);

  @override
  bool updateShouldNotify(CountContainer oldWidget) => model != oldWidget.model;

}
複製程式碼

1 將資料本身和操作他的方法宣告放在檢視元件,inheritedwidget只保留對它的引用,利用傳入的_incrementCounter,操作count屬性,當modal上面的屬性發生變化,即觸發繼承它的widget的更新

2 此方法需要將方法和屬性全部定義在widget樹的最上層。然後一起傳入給inheritedwidget,思想有些類似於之前實現相簿plugin使用的控制器。

控制器儲存了所有操作,widget共享的資料(相簿列表,當前所在相簿 等)的操作方法。我們向下傳遞一個controller物件,子widget需要讀或者寫資料時,通過控制器方法獲取。

但這種方法會造成每次更新一個資料,就會造成整棵樹的rebuild.

Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state

如果考慮使用InheritedWidget實現這個功能,可以降低資料更新成本

Flutter開發日記-資料傳遞/狀態管理的方式和應用

基於InheritedWidget實現的第三方庫provider,瞭解一下~

provider

特點: 做資料讀寫共享

又雙叒叕引用了一個demo:

// 定義資料

//定義需要共享的資料模型,通過混入ChangeNotifier管理聽眾
class DataModel with ChangeNotifier {
  int data = 0;
  //讀方法
  int get data => _data; 
  //寫方法
  void increment() {
    _data = _data*2;
    notifyListeners();
  }
}
// 將最上層widget包裹在provider內

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
     //通過Provider元件封裝資料資源
    return ChangeNotifierProvider.value(
        value: DataModel(),//需要共享的資料資源
        child: MaterialApp(
          home: MyPage(),
        )
    );
  }
}

// 下級widget中獲取或運算元據,同一樹的其他widget會重新觸發build

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出資源
    final _counter = Provider.of<DataModel>(context);
    return Scaffold(
      //展示資源中的資料
      body: Text('Counter: ${_data.data}'),
      //用資源更新方法來設定按鈕點選回撥
      floatingActionButton:FloatingActionButton(
          onPressed: _data.increment,
          child: Icon(Icons.add),
     ));
  }
}
複製程式碼

1 可以看出使用方式類似 InheritedWidget,將整棵樹包裹在provider裡,實現資料的跨層傳遞

2 provider可實現更小粒度的更新,當頁面中某部分不依賴於provider資料,放在consumer的child屬性中,而每次資料更新,只重新執行builder

Their optional child argument allows to rebuild only a very specific part of the widget tree

In this example, only Bar will rebuild when A updates. Foo and Baz won't unnecesseraly rebuild.

Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)
複製程式碼

問題:InheritedWidget和peovider提供了父->子的資料流機制,但如果此時我們需要子元件主動的分發事件和資料呢,那就闊以用到Notification了。

Notification

通知(Notification)是Flutter中一個重要的機制,在widget樹中,每一個節點都可以分發通知,通知會沿著當前節點向上傳遞,所有父節點都可以通過NotificationListener來監聽通知。Flutter中將這種由子向父的傳遞通知的機制稱為通知冒泡(Notification Bubbling)。通知冒泡和使用者觸控事件冒泡是相似的,但有一點不同:通知冒泡可以中止,但使用者觸控事件不行。 特點:同一元件樹傳遞資料 從下到上 通知事件 通知接收方可在事件物件中獲取資料 實現:

特點: 同一元件樹,子到父分發通知觸發事件,可跨層。 又找了個demo:


class CustomNotification extends Notification {
  CustomNotification(this.msg);
  final String msg;
}

//抽離出一個子Widget用來發通知
class CustomChild extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //按鈕點選時分發通知
      onPressed: () => CustomNotification("Hi").dispatch(context),
      child: Text("Fire Notification"),
    );
  }
}


class _MyHomePageState extends State<MyHomePage> {
  String _msg = "通知:";
  @override
  Widget build(BuildContext context) {
    //監聽通知
    return NotificationListener<CustomNotification>(
        onNotification: (notification) {
          setState(() {_msg += notification.msg+"  ";});//收到子Widget通知,更新msg
        },
        child:Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[Text(_msg),CustomChild()],//將子Widget加入到檢視樹中
        )
    );
  }
}
複製程式碼

1 宣告一個整合自Notification的子類CustomNotification

2 CustomChild 子元件例項化 CustomNotification並通過dispatch 觸發沿著element樹的向上冒泡通知

3 _MyHomePageState 通過NotificationListener 監聽 指定型別的CustomNotification 型別通知,並在onNotification中獲取到通知的例項物件和資料。

問題:上述提到的三種方法都依賴於widget樹,適用於widget之間有父子關係的場景,當我們需要跨頁面傳遞資料時,可以考慮使用event_bus .(需要安裝第三方依賴)

eventBus

特點:資料傳遞 ,不限制於同一元件樹,使用釋出/訂閱者模式實現跨元件資料通訊

又找了個demo:

---------- pubspec.yaml
dependencies:  
  event_bus: 1.1.0
  
---------- 自定義事件

class CustomEvent {
  String msg;
  CustomEvent(this.msg);
}
---------- 監聽事件
//建立公共的event bus
EventBus eventBus = new EventBus();
//第一個頁面
initState() {
//監聽CustomEvent事件,重新整理UI
subscription = eventBus.on<CustomEvent>().listen((event) {
  setState(() {msg+= event.msg;});//更新msg
});
super.initState();
}
---------- 觸發事件
()=> eventBus.fire(CustomEvent("hello"))

---------- 取消訂閱事件(否則會在元件銷燬後發生記憶體洩露)
subscription.cancel();//State銷燬時,清理註冊


---------- 全部程式碼

class CustomEvent {
  String msg;
  CustomEvent(this.msg);
}

EventBus eventBus = new EventBus();


class FirstPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState()=>_FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  String msg = "通知:";
  StreamSubscription subscription;
  @override
  void initState() {
    //監聽CustomEvent事件,重新整理UI
    subscription = eventBus.on<CustomEvent>().listen((event) {
      print(event);
        setState(() {
          msg += event.msg;
        });
    });
    super.initState();
  }

  dispose() {
    subscription.cancel();//State銷燬時,清理註冊
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("First Page"),),

      body:Text(msg),
        floatingActionButton: FloatingActionButton(onPressed: ()=>Navigator.push(context,MaterialPageRoute(builder: (context) => SecondPage()))),
    );
  }
}

class SecondPage extends StatelessWidget {

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Second Page"),),
      body: RaisedButton(
          child: Text('Fire Event'),
          // 觸發CustomEvent事件
          onPressed: ()=> eventBus.fire(CustomEvent("hello"))
      ),
    );
  }
}
複製程式碼

通過一個資料狀態,可以有多個訂閱者,實現批量的資料同步。

歡迎補充 歡迎指正~

參考文章

time.geekbang.org/column/arti… 極客時間

zhuanlan.zhihu.com/p/36577285 深入瞭解Flutter介面開發(閒魚)

相關文章