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

flutter.iohtml

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

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

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

而後咱們打開 「main.dart」 文件,並使 「MyHomePage」 小部件爲 「stateless」:android

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。如今咱們能夠從新加載咱們的應用程序,而後發現屏幕上沒有任何變化,除了 「+」 按鈕 —— 如今它尚未任何功能。這不要緊,由於咱們的小部件是無狀態的。ios

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

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」。此外,咱們應該有可能從樹下的任何小部件中獲取這些數據,但稍後咱們會考慮的。如今,咱們爲咱們的新小部件寫下簡單的實現。github

首先,咱們經過 「main.dart」 來新建一個 「Provider.dart」 dart 文件,而後用來實現咱們的 「Provider」 小部件。redux

如今咱們建立 「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」bash

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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索