Flutter 状态管理:概述
状态管理是 UI 框架必须实现的关键特性之一并且实现得很好。正是出于这个原因,许多开发人员已经开始构建专用的状态管理库;内置的解决方案对他们来说还不够,或者他们想根据自己的口味进行调整。
UI 框架已经加强了他们的游戏以平衡竞争环境。他们的内置状态管理解决方案现在可以与现有外部解决方案的性能相匹配。例如,React 引入了 Hooks 和 Context 来与 React-Redux 竞争。
Flutter 也发生了同样的情况:它提供了许多内置的方法来管理应用程序状态。在本文中,我们将介绍一些基本但功能强大的方法来管理 Flutter 应用程序中的状态。
setState
在 Flutter 中使用
如果你来自 React,你会发现 Flutter 中这种管理状态的方法类似于使用useState
Hook。
setState 只管理声明它的小部件中的状态——就像在 React 中一样,其中 useState 钩子只在创建它的组件中管理本地状态。这种类型的状态管理被称为 ephemeral 状态。在这里,这种状态是使用 StatefulWidget 和 setState()方法控制的。
使用小部件本身来管理状态
让我们setState
通过创建一个简单的计数器应用程序来查看一些有关如何工作的示例。该应用程序将有一个计数器编号,我们可以通过单击按钮来增加和减少。
首先,通过运行以下命令来搭建 Flutter 项目:
flutter create myapp
这将创建一个名为 的 Flutter 项目文件夹myapp
。现在让我们在服务器上运行项目:
flutter run myapp
在我们的项目文件夹中,我们应该看到一个main.dart
文件。这是主要的 Flutter 应用程序文件。清除文件内容并添加以下代码:
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( appBar: AppBar( title: Text("State Mgmt Demo"), ), body: CounterPage(title: 'Flutter Demo')), ); } }
Flutter 中的一切都是一个小部件。MyApp
是我们应用程序的入口/根小部件。在body
道具中,请注意我们正在渲染一个CounterPage
小部件。这是一个扩展StatefulWidget
类的有状态小部件。
StatefulWidgets
用于管理小部件中的本地状态。它们创建一个关联的State
对象,并且它们还持有不可变的变量。
下面是一个例子:
class NotificationCounter extends StatefulWidget { final String name; NotificationCounter({this.name}); @override _NotificationCounterState createState() => _NotificationCounterState(); }
name
上面的变量是一个不可变的变量。StatefulWidget
只保存不可变的变量和State
对象。
让我们看看我们的CounterPage
代码:
class CounterPage extends StatefulWidget { CounterPage({Key key, this.title}) : super(key: key); final String title; @override CounterPageState createState() => CounterPageState(); }
该createState
方法从中创建一个对象CounterPageState
并返回它。该createState
方法在构建小部件时被调用。
让我们看看代码CounterPageState
:
class CounterPageState extends State<CounterPage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: _incrementCounter, ) ], ), ) ); } }
CounterPageState
有一个可变变量_counter
,它存储计数器的编号并且可以在小部件的生命周期内更改。
build
当必须构建小部件时调用该方法。它返回小部件的 UI,并且appBar
->title
设置将在页面的应用栏中显示的内容。该body
设置窗口小部件的身体的 UI。
通常,此小部件将显示文本 Counter:、_counter
一行中的变量和下一行中的按钮。该按钮onPressed
设置了一个事件,类似于onclick
HTML 中的事件。
该_incrementCounter
函数setState
在按下按钮时调用。此方法调用告诉 Flutter 小部件内部的状态已更改,必须重新绘制小部件。setState
增加_counter
变量的函数参数。
void _incrementCounter() { setState(() { _counter++; }); }
因此,每当我们单击 Increment Counter 按钮时,_counter
都会增加并setState
调用它,这会告诉 Flutter 重建小部件树。的build
方法CounterPageState
被调用,然后 widget 中的 widget 树被重建并在 UI 上重新渲染(注意,只有发生变化的部分才会被重新渲染)。
如果我们在模拟器中启动我们的应用程序,它应该是这样的:
按下按钮时,数字会增加:
现在让我们添加一个递减按钮。此按钮将减少计数器并将更新反映到屏幕上。我们如何做到这一点?
简单:我们将添加一个新FlatButton
的文本Decrement Counter
并onPressed
在其上设置一个事件。我们将创建一个方法_decrementCounter
并将其设置为onPressed
事件的处理程序。
此_decrementCounter
方法将_counter
在调用时减少 1 并调用setState
更新 UI:
class CounterPageState extends State<CounterPage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } void _decrementCounter() { setState(() { _counter--; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: _incrementCounter, ), FlatButton( color: Colors.red, child: Text('Decrement Counter', style: TextStyle(color: Colors.white)), onPressed: _decrementCounter, ) ], ), )); } }
我们给 Decrement Button 一个红色背景,将它放在 Increment Button 下方。该_decrementCounter
方法设置为其onPressed
事件。该_decrementCounter
方法在_counter
每次调用时递减,并调用setState
来触发 UI 更新。
请观看下面的演示:
现在我们已经看到了如何使用小部件本身来管理状态,让我们看看另外两个选项:使用父小部件来管理状态,以及使用混合匹配方法。
使用父小部件管理状态
在这种方法中,小部件的父级保存状态变量并管理状态。父级通过将状态变量向下传递给子级小部件来告诉小部件何时更新。用于更改状态的方法也传递给子小部件,小部件可以调用它来更改状态并更新自身。
我们可以重写使用上面这种方法来重写counter
例子。我们将有一个无状态小部件,其工作是呈现 UI。创建一个类Counter
并按如下方式填充它:
class Counter extends StatelessWidget { final counter; final decrementCounter; final incrementCounter; Counter( {Key key, this.counter: 0, @required this.decrementCounter, @required this.incrementCounter}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( '$counter', style: Theme.of(context).textTheme.headline4, ), ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: () { incrementCounter(); }, ), FlatButton( color: Colors.red, child: Text('Decrement Counter', style: TextStyle(color: Colors.white)), onPressed: () { decrementCounter(); }, ) ], )); } }
同样,这是一个无状态小部件,因此它没有状态;它只是呈现传递给它的内容。
请注意,我们将渲染计数器的工作移到了这个小部件上。计数器通过 传递给它,递减和递增函数分别通过和传递给它。所有这些都是从父小部件传递的。this.counter``this.decrementCounter``this.incrementCounter``CounterPageState
现在,CounterPageState
小部件将如下所示:
class CounterPageState extends State<CounterPage> { // ... @override Widget build(BuildContext context) { return Scaffold( // ... body: Center( child: Counter( counter: _counter, decrementCounter: _decrementCounter, incrementCounter: _incrementCounter ) ) ); } }
复制代码
该Counter
现在由CounterPageState
渲染,它以前呈现的 UI 现在正由新的方式处理Counter
部件。
在这里,_counter
状态被传递给 prop 中的Counter
小部件counter
。该Counter
控件将通过访问计数器counter
在它的身上。
此外,_decrementCounter
和_incrementCounter
方法被传递给Counter
小部件。这些是从所谓的Counter
小部件更新状态_counter
的CounterPageState
小工具,这将导致CounterPageState
重建和重新渲染Counter
,以显示新更改的状态。
混搭状态管理
在这种方法中,父小部件管理一些状态,而子小部件管理状态的另一方面。为了演示这一点,我们将让我们的Counter
小部件保持一个状态,使其成为StatefulWidget
.
我们将跟踪 Increment Button 和 Decrement Button 被点击的次数,并在两种状态下保持数字。
现在,让我们使Counter
小部件成为有状态的小部件:
class Counter extends StatefulWidget { final counter; final decrementCounter; final incrementCounter; Counter( {Key key, this.counter: 0, @required this.decrementCounter, @required this.incrementCounter}) : super(key: key); @override CounterState createState() => CounterState(); }
我们可以看到该createState
方法返回一个CounterState
对象。我们来看一下这个CounterState
类:
class CounterState extends State<Counter> { var incrButtonClicked = 0; var decreButtonClicked = 0; @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( widget.counter.toString(), style: Theme.of(context).textTheme.headline4, ), ], ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("'Increment Button' clicked $incrButtonClicked times"), Text("'Decrement Button' clicked $decreButtonClicked times") ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: () { widget.incrementCounter(); setState(() { incrButtonClicked++; }); }, ), FlatButton( color: Colors.red, child: Text('Decrement Counter', style: TextStyle(color: Colors.white)), onPressed: () { widget.decrementCounter(); setState(() { decreButtonClicked++; }); }, ) ], )); } }
请注意,Counter
小部件先前的 UI 在这里。我们添加了incrButtonClicked
和decreButtonClicked
状态来保存按钮被按下的次数。我们还添加了一个Column
小Text
部件,以在以主轴为中心的列中显示小部件。这些Text
小部件将显示每个按钮被点击的次数。
现在,在onPressed
每个按钮的事件处理程序中,我们通过对象调用incrementCounter
ordecrementCounter
方法widget
。我们使用widget
对象来访问有状态小部件中的父变量。然后,我们调用了setState
增加或减少状态变量incrButtonClicked
和 的方法decreButtonClicked
。
所以我们在这里可以看到我们有一个混合匹配的状态管理方法:父小部件处理counter
状态,而子小部件处理点击状态。
请参阅下面的演示:
InheritedModel
和 InheritedWidget
该技术使用父小部件和子小部件之间的通信方法。数据设置在父小部件上,子小部件可以从父小部件访问数据,这样小部件状态就可以无缝传递。
这种状态管理类似于Service
在 Angular 中使用 s 类,也与 React 的 Context API 有相似之处。
InheritedWidget
InheritedWidget
是 Flutter 中的一个基类,用于在小部件树中传播信息。
这是它的工作原理:一个InheritedWidget
包含一个小部件树。现在,树中的小部件可以引用 up 来InheritedWidget
访问其中的公共变量,从而在树中传递数据。由 持有的数据InheritedWidget
通过其构造函数传递给它。
InheritedWidget
当我们必须通过一长串小部件传递数据只是为了在小部件中使用它时,这是非常有用的。例如,我们有这样的小部件树:
MyApp | v CounterPage | v 虚拟容器1 | v 虚拟容器2 | v Counter
该CounterPage
有一个counter
与国家incrementCounter
和incrementCounter
方法。我们想counter
在 UI 中显示Counter
小部件。为此,我们必须将counter
状态和两个方法传递给Counter
小部件。
首先,从小CounterPage
部件中,我们将呈现DummyContainer
小部件,将counter
和 两个方法作为参数传递给其构造函数。接下来,DummyContainer1
将呈现DummyContainer2
并将counter
状态和两个方法DummyContainer2
作为参数传递给构造函数。最后,DummyContainer2
将呈现Counter
并将计数器和方法传递给它。
有了InheritedWidget
,我们就可以随便驾驶了。使用InheritedWidget
,我们将在其中设置counter
和两个方法。在InheritedWidget
将呈现DummyContainer1
和CounterPage
将呈现InheritedWidget
。CounterPage
将counter
和 方法设置为InheritedWidget
.
MyApp | v CounterPage | v 我的继承小部件 | v 虚拟容器1 | v 虚拟容器2 | v Counter
这就是包含InheritedWidget
.
让我们编码吧!我们将从CounterPage
:
class CounterPage extends StatefulWidget { CounterPage({Key key, this.title}) : super(key: key); final String title; @override CounterPageState createState() => CounterPageState(); static CounterPageState of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>().data; } }
我们添加了一个static
方法of
。此方法使用context
来返回InheritedWidget
使用方法调用。此方法返回精确类型的小部件树中最近的;在这种情况下,我们需要一个类型。dependOnInheritedWidgetOfExactType<MyInheritedWidget>()``Inherited``W``idget``MyInheritedWidget
现在,在我们的 CounterPageState
中,我们将渲染MyInheritedWidget
,在其中,我们将渲染DummyContainer1
小部件。
class CounterPageState extends State&lt;CounterPage&gt; { // ... @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: MyInheritedWidget( child: DummyContainer1(), data: this ) ) ); } }
的data
参数保存this
,这意味着的公共属性CounterPageState
是可访问MyInheritedWidget
经由data
支柱。我们这样做是因为我们想要的_counter
,而这两种方法_incrementCounter
和_decrementCounter
,由一个被引用InheritedWidget
。有了这个,我们可以使用InheritedWidget
来访问counter
小部件树中任何位置的状态和方法。
让我们创建的MyInheritedWidget
,DummyContainer1
和DummyContainer2
小部件。
class MyInheritedWidget extends InheritedWidget { final CounterPageState data; MyInheritedWidget({ Key key, @required Widget child, @required this.data, }) : super(key: key, child: child); @override bool updateShouldNotify(InheritedWidget oldWidget) { return true; } }
我们有一个data
属性和一个对象CounterPageState
。这是我们在CounterPageState
. 该updateShouldNotify
方法确定是否InheritedWidget
将重建其下方的小部件树。如果返回 true,将重建 widget 树;如果返回 false,则状态更改时将不会重新构建小部件树。
class DummyContainer1 extends StatelessWidget { const DummyContainer1({Key key}) : super(key: key); @override Widget build(BuildContext context) { return DummyContainer2(); } }
此DummyContainer1
小部件呈现DummyContainer2
小部件。
class DummyContainer2 extends StatelessWidget { const DummyContainer2({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Counter(); } }
该DummyContainer2
窗口小部件,反过来,呈现Counter
小部件。
现在,让我们看看我们的Counter
小部件:
class Counter extends StatefulWidget { @override CounterState createState() =&gt; CounterState(); }
它只实现了以下createState
方法:
class CounterState extends State&lt;Counter&gt; { var incrButtonClicked = 0; var decreButtonClicked = 0; var counter; CounterPageState data; @override void didChangeDependencies() { super.didChangeDependencies(); data = CounterPage.of(context); counter = data._counter; } @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: &lt;Widget&gt;[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( counter.toString(), style: Theme.of(context).textTheme.headline4, ), ], ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("'Increment Button' clicked $incrButtonClicked times"), Text("'Decrement Button' clicked $decreButtonClicked times") ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: () { data._incrementCounter(); setState(() { incrButtonClicked++; }); }, ), FlatButton( color: Colors.red, child: Text('Decrement Counter', style: TextStyle(color: Colors.white)), onPressed: () { data._decrementCounter(); setState(() { decreButtonClicked++; }); }, ) ], )); } }
请注意,我们从构造函数中删除了 props 。我们用得到CounterPageState data = CounterPage.of(context);
变量。从MyInheritedWidget
那里,我们可以访问data
. 注意我们是如何访问,_counter
_incrementCounter
,_decrementCounter
数据变量的属性。
这些是存储在MyInheritedWidget
from 中的属性CounterPageState
,因此一旦我们引用了MyInheritedWidget
,我们就可以从小部件树的任何位置获取这些属性。这就是通过InheritedWidget
小部件树中的任何位置传递和访问数据的方式。
这是演示:
InheritedModel
InheritedModel
工作方式与 相同InheritedWidget
:它管理状态并在其小部件树中传播状态。但InheritedModel
略有不同,它允许更好地控制更改检测触发器和更新通知,可以设置为在特定数据更改时做出响应。
InheritedModel
易于实施。让我们重写上面的Counter
示例以使用InheritedModel
. 令人惊讶的是,代码几乎相同。
首先,更改MyInheritedWidget
为MyInheritedModel
:
class MyInheritedModel extends InheritedModel&lt;String&gt; { final CounterPageState data; MyInheritedModel({ Key key, @required Widget child, @required this.data, }) : super(key: key, child: child); @override bool updateShouldNotify(MyInheritedModel old) { return true; } @override bool updateShouldNotifyDependent(MyInheritedModel old, Set&lt;String&gt; aspects) { return true; } static MyInheritedModel of(BuildContext context, String aspect) { return InheritedModel.inheritFrom&lt;MyInheritedModel&gt;(context, aspect: aspect); } }
还是一样; 这里的关键是static
方法of
。它返回自身的一个实例,因此我们可以使用它来访问其公共属性。是我们想要公开的属性——它是将通过它的小部件树向下传播的状态。请注意,它的值由构造函数中的参数设置。final CounterPageState data;``InheritedModel``this.data
接下来,我们相应地更新我们的CounterState
:
class CounterState extends State&lt;Counter&gt; { var incrButtonClicked = 0; var decreButtonClicked = 0; var counter; MyInheritedModel inheritedModel; @override Widget build(BuildContext context) { inheritedModel = MyInheritedModel.of(context, ""); counter = inheritedModel.data._counter; return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: &lt;Widget&gt;[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter:', style: Theme.of(context).textTheme.headline4, ), Text( counter.toString(), style: Theme.of(context).textTheme.headline4, ), ], ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("'Increment Button' clicked $incrButtonClicked times"), Text("'Decrement Button' clicked $decreButtonClicked times") ], ), FlatButton( color: Colors.orange, child: Text('Increment Counter', style: TextStyle(color: Colors.white)), onPressed: () { inheritedModel.data._incrementCounter(); setState(() { incrButtonClicked++; }); }, ), FlatButton( color: Colors.red, child: Text('Decrement Counter', style: TextStyle(color: Colors.white)), onPressed: () { inheritedModel.data._decrementCounter(); setState(() { decreButtonClicked++; }); }, ) ], )); } }
在这里,我们有MyInheritedModel inheritedModel;
,我们称之为inheritedModel = MyInheritedModel.of(context, "");``build()
的方法得到的实例。MyInheritedModel
现在,从inheritedModel
我们可以访问属性来获取,以及在属性窗口小部件。final CounterPageState data;``counter``_incrementCounter``_decrementCounter``CounterPageState
从计数器状态接收,然后在显示之前转换为字符串。counter = inheritedModel.data._counter;
_incrementCounter
,_decrementCounter
方法是通过所谓的inheritedModel.data._incrementCounter();
和增加inheritedModel.data._decrementCounter();
,分别减少按钮点击次数。``
这将是Counter
代码:
class Counter extends StatefulWidget { @override CounterState createState() =&gt; CounterState(); }
复制代码
这里没有什么需要注意的;只需实现该createState
方法并返回CounterState
小部件的实例。
现在,这是我们的CounterPageState
:
class CounterPageState extends State&lt;CounterPage&gt; { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } void _decrementCounter() { setState(() { _counter--; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: MyInheritedModel( child: DummyContainer1(), data: this ) ) ); } }
CounterPageState
坐骑MyInheritedModel
。的实例CounterPageState
通过data
参数传递给它的构造函数。这就是我们能够访问CounterPageState
from 的公共属性的方式MyInheritedModel
。
这是演示:
结论
我们已经使用 Flutter 的内置机制介绍了状态管理的基础知识。我们首先分析了什么是状态管理,以及它对任何 UI 框架来说是多么重要。接下来,我们查看setState
与 React 的useState
Hook 的比较。我们通过示例说明了如何setState
工作以及如何使用它来构建现实世界的应用程序。
然后我们讨论InheritedWidget
并看到了我们如何声明一个状态并将它向下传播到小部件树。树下的小部件可以订阅状态以在状态更改时获取更新。
与 类似InheritedWidget
,我们查看了InheritedModel
,它沿小部件树向下传播状态。这里的区别在于我们可以选择我们希望在更改时收到通知的状态。
进一步阅读
好的,状态管理的相关基础我也介绍完了,我们继续加油