- 原文地址:Reactive app state in Flutter
- 原文作者:Maksim Ryzhikov
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Starrier
flutter.io
我使用 Flutter 已經有幾個星期了,所以我能感受到它為開發所帶來的便利,感謝 Flutter 和 Dart 團隊。但起初我嘗試攻擊 Flutter 中演示案例時,遇到了一些問題:
- 如何將應用程式的狀態傳遞給小部件樹
- 如何在更新應用程式狀態之後重建小部件
那麼我們從第一個問題“如何傳遞應用程式狀態”開始。我用 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;
}
複製程式碼
我們重新載入了應用程式,並嘗試按下 “+” 按鈕。沒有改變什麼。但如果我們允許“熱載入”,我們將看到文字已經改變。這是因為我們按下按鈕後改變了應用程式的狀態。但我們在螢幕上看到了舊的狀態,因為我們還沒有重新構建小部件。當我們允許小部件進行“熱載入”時,我們就可以看到螢幕上的實際狀態。
最後的挑戰是在我們更改應用程式的狀態後重建小部件。但在此之前,我們看看已有的東西:
- “Provider” —— 我們應用程式狀態的容器
- “AppState” —— 跟蹤應用程式狀態改變並通知“監聽者”的類
- “_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();
}
...
}
複製程式碼
我們重建這個應用程式,並檢查它是如何工作的。現在,當我們按下 “+” 按鈕時,我們的應用程式狀態就會發生改變,小部件也會被重建。
我們檢查一下我們的問題:
- 我們如何將應用程式狀態傳遞到小部件樹 —— 已解決
- 如何在更改應用程式狀態後重建小部件 —— 已解決
原始碼在這裡 —— gist.github.com/c88f116d7d6…
結論。
本文的總體思想是演示如何在沒有額外包的情況下,在 Flutter 中實現 “redux” 模式。Flutter 已經有了可以實現 “redux” 模式的包,但有時它們不適合你的體系結構,知道如何用你自己的手從頭開始實現是好事 ✋。
感謝,
快樂的編碼,快樂的發展!
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。