状态管理中的声明式编程思维
Flutter应用是 声明式 的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态,在 Flutter 应用中,当状态变化时,会重新构建部分界面,而不是原生Android或iOS的命令式。当Flutter应用的状态发生改变时(例如:点击了一个按钮,触发了某个动画或者某个值的更新),改变状态就会导致UI界面重绘。去改变用户界面本身是没有必要的(例如 widget.setText ),因为这样的代码不会在UI界面上更新,只要改变了状态,那么用户界面将重新构建。
通过setState更新
class MyHomePage extends StatefulWidget {const MyHomePage({super.key, required this.title});final String title;@overrideState<MyHomePage> createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[const Text('You have pushed the button this many times:',),Text('$_counter',style: Theme.of(context).textTheme.headlineMedium,),],),),floatingActionButton: FloatingActionButton(onPressed: _incrementCounter,tooltip: 'Increment',child: const Icon(Icons.add),), // This trailing comma makes auto-formatting nicer for build methods.);}
}
从上述代码可以看出,如果我们需要改变某个值,只需要使用 setState((){ }) 在此函数中更新就会导致界面重绘,会重新执行build构建UI,前提是此Widget是StatefulWidget的子类。
State方式的状态属于短暂状态,widget 树中其他部分不需要访问这种状态。不需要去序列化这种状态,这种状态也不会以复杂的方式改变,需要用的只是一个 StatefulWidget
。
了解Provider
如果我们想在应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态),为了管理应用状态,就需要研究使用Provider。
示例
假设我们有2个页面,A页面编辑的信息,在B页面需要使用,那此时我们可能无法在B中使用State拿到A页面的信息,此时Provider就帮上忙了,需要访问一些全局的状态。比如,A页面的会被添加到B页面中。但是它可能需要检查和自己相同的元素是否已经被添加到B页面中。
这里我们出现了第一个问题:我们把当前页面的状态放在哪合适呢?
提高状态的层级
在 Flutter 中,有必要将存储状态的对象置于 widget 树中对应 widget 的上层。
为什么呢?在类似 Flutter 的声明式框架中,如果你想要修改 UI,那么你需要重构它。并没有类似 B.updateWith(newData)
的简单调用方法。很难通过外部调用方法修改一个 widget。即便自己实现了这样的模式,那也是和整个框架不相兼容。
比如在B页面的入口在A页面,那需要在A页面创建Provider,在B中可以共享到A页面创建的 数据,更新数据后A也能同步到最新的数据。
void onTap(BuildContext context) {var model = ProviderModel(context);model.add(item);
}
这里 B页面
可以在各种版本的 UI 中调用同一个代码路径,获取数据
Widget build(BuildContext context) {var model = ProviderModel(context);return Continer(// ···);
}
在上面的例子中,model会存在于A-B 的生命周期中。当它发生改变的时候,它会从上层重构 B页面
。因为这个机制,所以 B页面
无需考虑生命周期的问题—它只需要针对 providerModel声明所需显示内容即可。当内容发生改变的时候,旧的 B的
widget 就会消失,完全被新的 widget 替代。
如何使用
在使用 provider 之前,请不要忘记在 pubspec.yaml 文件里加入依赖。
运行 flutter pub add 将 provider 添加为依赖:
flutter pub add provider
现在可以在代码里加入 import 'package:provider/provider.dart';
进而开始构建你的应用了
provider
你无须关心回调或者 InheritedWidgets
。但是你需要理解三个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
ChangeNotifier
是 Flutter SDK 中的一个简单的类。它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier
,你可以订阅它的状态变化。(这和大家所熟悉的观察者模式相类似)。
在 provider
中,ChangeNotifier
是一种能够封装应用程序状态的方法。对于特别简单的程序,你可以通过一个 ChangeNotifier
来满足全部需求。在相对复杂的应用中,由于会有多个模型,所以可能会有多个 ChangeNotifier
。 (不是必须得把 ChangeNotifier
和 provider
结合起来用,不过它确实是一个特别简单的类)。
在示例中用 ChangeNotifier
来管理状态。我们创建一个新类,继承它(可以把他理解成MVVM中的viewModel),像下面这样:
class ProviderModel extends ChangeNotifier {/// Internal, private state of the cart.final List<Item> _items = [];/// An unmodifiable view of the items in the cart.UnmodifiableListView<Item> get items => UnmodifiableListView(_items);/// The current total price of all items (assuming all items cost $42).int get totalPrice => _items.length * 42;/// Adds [item] to cart. This and [removeAll] are the only ways to modify the/// cart from the outside.void add(Item item) {_items.add(item);// This call tells the widgets that are listening to this model to rebuild.notifyListeners();}/// Removes all items from the cart.void removeAll() {_items.clear();// This call tells the widgets that are listening to this model to rebuild.notifyListeners();}
}
唯一一行和 ChangeNotifier
相关的代码就是调用 notifyListeners()
。当模型发生改变并且需要更新 UI 的时候可以调用该方法。而剩下的代码就是 ProviderModel
和它本身的业务逻辑。可以这么理解,调用了 notifyListeners()
之后,会执行创建Provider的Build方法重新构建UI
如果使用创建的Provider:
class MyApp extends StatelessWidget {const MyApp({super.key});// This widget is the root of your application.@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: ChangeNotifierProvider(create: (context) => ProviderModel(), child: MyHomePage(title: 'Flutter Demo Home Page')),);}
}
或者
class MyApp extends StatelessWidget {const MyApp({super.key});// This widget is the root of your application.@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,),home: ChangeNotifierProvider(create: (context) => ProviderModel(),child: Consumer<ProviderModel>(builder: (context, viewModel, child) {return MyHomePage(title: 'Flutter Demo Home Page');})));}
}
现在 ProviderModel
已经通过 ChangeNotifierProvider
在应用中与 widget 相关联。我们可以开始调用它了。
child: Consumer<ProviderModel>(builder: (context, viewModel, child) {}
我们必须指定要访问的模型类型。在这个示例中,我们要访问 ProviderModel
那么就写上 Consumer<ProviderModel>
。
Consumer
widget 唯一必须的参数就是 builder。当 ChangeNotifier
发生变化的时候会调用 builder 这个函数。(换言之,当你在模型中调用 notifyListeners()
时,所有相关的 Consumer
widget 的 builder 方法都会被调用。)
builder 在被调用的时候会用到三个参数。第一个是 context
,在每个 build 方法中都能找到这个参数。
builder 函数的第二个参数是 ChangeNotifier
的实例。它是我们最开始就能得到的实例。你可以通过该实例定义 UI 的内容
第三个参数是 child
,用于优化目的。如果 Consumer
下面有一个庞大的子树,当模型发生改变的时候,该子树 并不会 改变,那么你就可以仅仅创建它一次,然后通过 builder 获得该实例。
return Consumer<ProviderModel>(builder: (context, cart, child) => Stack(children: [// Use SomeExpensiveWidget here, without rebuilding every time.if (child != null) child,Text('Total price: ${cart.totalPrice}'),],),// Build the expensive widget here.child: const SomeExpensiveWidget(),
);
如果我们Widget树比较庞大,那么就需要考虑到,更换Consumer的位置,在需要更新的位置使用,这样当数据发生改变时就不会全盘重新构建 widget 树了。例如:
return Consumer<ProviderModel>(builder: (context, cart, child) {return AWidget(// ...child: BWidget(// ...child: Text('Total price: ${cart.totalPrice}'),),);},
);
换成:
return AWidget(// ...child: Consumer<ProviderModel>(builder: (context, cart, child) {return BWidget(// ...child: Text('Total price: ${cart.totalPrice}'),),);},
);
有的时候你不需要模型中的 数据 来改变 UI,但是你可能还是需要访问该数据。比如A页面的一个按钮能够B页面的数据。它不需要显示B页面里的内容,只需要调用 clear()
方法。
我们可以使用 Consumer<ProviderModel>
来实现这个效果,不过这么实现有点浪费。因为我们让整体框架重构了一个无需重构的 widget,所以这里我们可以使用 Provider.of
,并且将 listen
设置为 false
可以使用:
Provider.of<ServicePieceViewModel>(context, listen: false).clear();
或许通过context:
context.read<ServicePieceViewModel>().clear();
在 build 方法中使用上面的代码,当 notifyListeners
被调用的时候,并不会使 widget 被重构。
context.read和context.watch的区别
extension ReadContext on BuildContext {T read<T>() {return Provider.of<T>(this, listen: false);}
}
extension WatchContext on BuildContext {T watch<T>() {return Provider.of<T>(this);}
}
context.read是ReadContext类中的函数,继承了BuildContext,其实就是封装了一下获取Provider的方法,用read方法获取的Provider,那么当value改变的时候,不会使页面重建,而且这个方法不能再StatelessWidget.build和State.build方法中调用,也就是说可在这些方法外面随意调用。
context.watch是WatchContext类中的函数,刚好和read相反,WatchContext中的watch方法和ReadContext中的read方法是相似的,但是watch方法会导致widget重构。
总结
Flutter的状态管理机制涵盖了短时状态和共享状态,足够满足我们在日常开发中所遇到的数据更新去刷新UI的需求,个人感觉比命令式编程轻松了很多,后面我也会在此基础上看看是否能够封装一套自己的状态管理框架,欢迎同学们一起交流讨论。