[譯] Flutter 中的原生應用程式狀態

Starrier發表於2018-08-04

[譯] Flutter 中的原生應用程式狀態

flutter.io

我使用 Flutter 已經有幾個星期了,所以我能感受到它為開發所帶來的便利,感謝 Flutter 和 Dart 團隊。但起初我嘗試攻擊 Flutter 中演示案例時,遇到了一些問題:

  1. 如何將應用程式的狀態傳遞給小部件樹
  2. 如何在更新應用程式狀態之後重建小部件

那麼我們從第一個問題“如何傳遞應用程式狀態”開始。我用 Flutter 的標準 “Counter” 示例應用程式來演示我的解決方案。建立一個這樣的應用程式非常簡單:我們只需要在終端輸入 “flutter create myapp”(“myapp” —— 是我的示例應用程式的名字)。

然後我們開啟 “main.dart” 檔案,並使 “MyHomePage” 小部件為 “stateless”:

import 'package:flutter/material.dart';

var _counter = 0;
...

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }

  _incrementCounter() {}
}
複製程式碼

我們只需將 “build” 方法 “MyHomePageState” 移至 “MyHomePage” 小部件,然後在檔案的頂部新建空方法 _incrementCounter 和變數 _counter。現在我們可以重新載入我們的應用程式,然後發現螢幕上沒有任何變化,除了 “+” 按鈕 —— 現在它還沒有任何功能。這沒關係,因為我們的小部件是無狀態的。

我們考慮一下提供應用程式狀態的小部件應該是什麼樣子的:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Provider(      
      data: _counter,
      child: new MaterialApp(
        title: 'Flutter Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: new MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}
複製程式碼

這裡我們可以看到包裝了我們整個應用程式的新的小部件 “Provider”。它有兩個屬性:包含應用程式狀態的 “data” 和子代小部件的 “child”。此外,我們應該有可能從樹下的任何小部件中獲取這些資料,但稍後我們會考慮的。現在,我們為我們的新小部件寫下簡單的實現。

首先,我們通過 “main.dart” 來新建一個 “Provider.dart” dart 檔案,然後用來實現我們的 “Provider” 小部件。

現在我們建立 “Provider” 作為 “Stateless” 小部件:

import 'package:flutter/widgets.dart';

class Provider extends StatelessWidget {
  const Provider({this.data, this.child});

  final data;
  final child;

  @override
  Widget build(BuildContext context) => child;
}
複製程式碼

是的,直截了當。現在我們將 "Provider" 匯入 “main.dart”

import 'package:flutter/material.dart';

import 'package:myapp/Provider.dart';
複製程式碼

重建我們應用程式,以檢查所有的工作是否有錯。如果全部“執行”,那我們就可以進行下一步了。我們現在有了一個用於應用程式狀態的容器,我們可以返回如何從這個容器中檢索資料的問題。幸運的是,Flutter 已經有了解決方案,而且它是 “InheritedWidget”。這些文件已經說得很清楚了:

用於在樹中有效傳播資訊的小部件的基類。

這正是我們所需要的。我們建立“繼承”小部件。

開啟 “Provider.dart”,然後建立私有 “_InheritedProvider” 小部件:

class _InheritedProvider extends InheritedWidget {
  _InheritedProvider({this.data, this.child})
      : super(child: child);

  final data;
  final child;

  @override
  bool updateShouldNotify(_InheritedProvider oldWidget) {
    return data != oldWidget.data;
  }
}
複製程式碼

“InheritedWidget” 的所有子類都應該實現 “updateShouldNotify” 方法。此時,我們只需檢查傳遞的 “data” 是否已更改。例如,當我們將計數器從 “0” 改為 “1” 時,該方法應該返回 “true”。

我們現在講“繼承”小部件,然後新增到小部件樹中:

class Provider extends StatelessWidget {
  ...

  @override
  Widget build(BuildContext context) {
    return new _InheritedProvider(data: data, child: child);
  }
  ...
}
複製程式碼

好的,我們現在有了小部件,它在小部件樹中傳播資料,但我們應該建立一個公有方法,它允許 get 這個資料:

class Provider extends StatelessWidget {
  ...

  static of(BuildContext context) {
    _InheritedProvider p =
        context.inheritFromWidgetOfExactType(_InheritedProvider);
    return p.data;
  }
  ...
}
複製程式碼

inheritFromWidgetOfExactType” 方法獲取 “_InheritedProvider” 型別例項的最近父部件。

我們現在有了解決第一個問題的能力:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Provider(
      data: 0, 
      ...
    }
  ...
}

...

class MyHomePage extends StatelessWidget {
  ...
  Widget build(BuildContext context) {
    var _counter = Provider.of(context);
  ...
}
複製程式碼

我們刪除了全域性變數 “_counter” 並使用 “Provider.of” 在 “MyHomePage” 小部件中取得了 “counter”。如你所見,我們沒有將它作為引數傳遞給 “MyHomePage”,而是使用 “Provider.of” 來獲取應用程式的狀態,他可以應用於樹下的任何小部件。 此外,“Provider.of” 還包括當前小部件的上下文和重建,並在更改 “_InheritedProvider” 小部件時對其進行註冊

現在是時候檢測我們的應用程式是否起作用了:我們重新載入它。為了確保我們的 "Provider" 正常工作,我們可以將 “data” 從 “0” 更改為 “MyApp” 小部件中的 “1”,然後我們必須重新載入應用程式。然而,我們的 “+” 按鈕仍然無法工作。

在這裡,我們面臨的第二個問題是“如何在改變應用程式的狀態後重建小部件”,現在我們應該重新開始思考。

我們的應用程式狀態只是一個數字,但當這個數字被更改時,檢測起來就沒有那麼容易了。如果我們將“計數器”編號包裝成一個“可觀察的”物件,該物件將跟蹤更改並通知“監聽器”這些更改。

慶幸的是,Flutter 已經有了解決方案,這就是 “ValueNotifier”。像通常一樣,這裡有一個很好的文件解釋:

value 被替代時,這個類會通知它的監聽器。

好的,讓我們在 “mian.dart” 中建立應用程式的狀態類:

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

class AppState extends ValueNotifier {
  AppState(value) : super(value);
}

var appState = new AppState(0);
複製程式碼

然後將其傳遞給 "Provider"

Widget build(BuildContext context) {
    return new Provider(
      data: appState,
複製程式碼

由於 “data” 包含一個物件,所以我們更改 “Provider.of(context)” 用法,那就這樣做:

Widget build(BuildContext context) {
    var _counter = Provider.of(context).value;
複製程式碼

重建我們的應用程式,並確保沒有錯誤。

我們現在已經實現了 “_incrementCounter”:

  floatingActionButton: new FloatingActionButton(
        onPressed: () => _incrementCounter(context),
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }

  _incrementCounter(context) {
    var appState = Provider.of(context);
    appState.value += 1;
  }
複製程式碼

我們重新載入了應用程式,並嘗試按下 “+” 按鈕。沒有改變什麼。但如果我們允許“熱載入”,我們將看到文字已經改變。這是因為我們按下按鈕後改變了應用程式的狀態。但我們在螢幕上看到了舊的狀態,因為我們還沒有重新構建小部件。當我們允許小部件進行“熱載入”時,我們就可以看到螢幕上的實際狀態。

最後的挑戰是在我們更改應用程式的狀態後重建小部件。但在此之前,我們看看已有的東西:

  1. “Provider” —— 我們應用程式狀態的容器
  2. “AppState” —— 跟蹤應用程式狀態改變並通知“監聽者”的類
  3. “_InheritedProvider” —— 小部件將有效地將應用程式狀態傳播到網上,並在改變了自己的狀態之後重建使用者。

首先,我們回顧一下 “_InheritedProvider” 的 “updateShouldNotify” 方法:

  @override
  bool updateShouldNotify(_InheritedProvider oldWidget) {
    return data != oldWidget.data;
  }
複製程式碼

現在 “data” 等於 “AppState” 的例項,這意味著我們在 “_incrementCounter” 方法中更改此例項的 “value” 時,它實際上並不會改變例項本身。因此,這個比較總是返回 “false”。我們通過比較 “value”-s 來解決這個問題。但為此,我們應該將“值”曝出在小部件中,這允許我們可以不丟失重構之間的 “value”:

class _InheritedProvider extends InheritedWidget {
  _InheritedProvider({this.data, this.child})
      : _dataValue = data.value,
        super(child: child);

  final data;
  final child;
  final _dataValue;

  @override
  bool updateShouldNotify(_InheritedProvider oldWidget) {
    return _dataValue != oldWidget._dataValue;
  }
}
複製程式碼

現在它可以正確地工作了:當我們改變狀態值時,小部件會重新構建消費者。但在重新構建消費者之前,我們應該在改變應用程式的狀態後重建小部件本身。

我們的程式碼中只有一個可以瞭解 “_InheritedProvider” 的小部件,就是 “Provider” 小部件。如果我們想要跟蹤小部件中的某種狀態,我們應該建立 “statefull” 小部件。好的,讓我們將 “Provider” 小部件從 “stateless” 轉換為 “statefull”:

class Provider extends StatefulWidget {
  const Provider({this.child, this.data});

  static of(BuildContext context) {
    _InheritedProvider p =
        context.inheritFromWidgetOfExactType(_InheritedProvider);
    return p.data;
  }

  final ValueNotifier data;
  final Widget child;

  @override
  State<StatefulWidget> createState() => new _ProviderState();
}

class _ProviderState extends State<Provider> {
  @override
  Widget build(BuildContext context) {
    return new _InheritedProvider(
      data: widget.data,
      child: widget.child,
    );
  }
}

class _InheritedProvider extends InheritedWidget {
  _InheritedProvider({this.data, this.child})
複製程式碼

我們現在可以 “subscribe” 應用程式的狀態改變,並在它被更改後呼叫 “setState”:

class _ProviderState extends State<Provider> {
  @override
  initState() {
    super.initState();
    widget.data.addListener(didValueChange);
  }

  didValueChange() => setState(() {});
複製程式碼

不要忘記在小部件被銷燬後刪除垃圾:

class _ProviderState extends State<Provider> {
  ...

  @override
  dispose() {
    widget.data.removeListener(didValueChange);
    super.dispose();
  }
  ...
}
複製程式碼

我們重建這個應用程式,並檢查它是如何工作的。現在,當我們按下 “+” 按鈕時,我們的應用程式狀態就會發生改變,小部件也會被重建。

我們檢查一下我們的問題:

  1. 我們如何將應用程式狀態傳遞到小部件樹 —— 已解決
  2. 如何在更改應用程式狀態後重建小部件 —— 已解決

原始碼在這裡 —— gist.github.com/c88f116d7d6…

結論。

本文的總體思想是演示如何在沒有額外包的情況下,在 Flutter 中實現 “redux” 模式。Flutter 已經有了可以實現 “redux” 模式的包,但有時它們不適合你的體系結構,知道如何用你自己的手從頭開始實現是好事 ✋。

感謝,

快樂的編碼,快樂的發展!

感謝 Elizaveta Kulikova

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章