state
Flutter采用的是声明式的编程方式,和iOS命令式的不同,声明式编程需要大量的state来进行管理,通过改变state来驱动UI进行更新。因此我们需要对state有一个清晰的认识。
局部状态:是可以完全包含在一个独立的widget中的状态,widget树中其他部分不需要访问这种状态。
全局状态:在应用的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态。
局部状态我们自己在widget中管理即可,而全局状态就需要我们科学的进行统一管理和维护,避免代码出现高耦合和低维护的情况。
下面这几种都是Flutter中用来管理状态的方式,没有好坏之分,只有使用场景不同。
InheritedWidget
InheritedWidget提供了在widget树中从上到下的数据共享方式。
我们先来简单使用一下InheritedWidget。
先创建一个继承自InheritedWidget的子Widget
import 'package:flutter/material.dart';
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({required this.data, super.key, required super.child});
final int data; // 需要在子树中共享的数据
// 定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
// data发生变化时是否通知子树中使用data的widget重新build
@override
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
然后创建一个使用该Widget组件
import 'package:flutter/material.dart';
import 'package:myapp/share_data_widget.dart';
class TestWidget extends StatefulWidget {
const TestWidget({super.key});
@override
State<TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
}
@override
Widget build(BuildContext context) {
return Text('第' + ShareDataWidget.of(context)!.data.toString() + '次');
}
}
最后在示例程序中使用
Widget 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:',
),
ShareDataWidget(
data: _counter,
child: const TestWidget(),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
可见在InheritedWidget中保存的数据在它的所有子孙Widget中都能获取到,并且,当数据发生改变的时候会更新依赖于它的子孙组件(通过didChangeDependencies()和build())。
我们可以使用getElementForInheritedWidgetOfExactType()来替换dependOnInheritedWidgetOfExactType()。区别在于dependOnInheritedWidgetOfExactType()会调用dependOnInheritedElement来注册依赖于它的子孙组件,之后,当数据发生变化的时候就会更新注册的子孙组件。
provider
provider是基于InheritedWidget的,所以,当不同页面不同的widget想要使用相同的数据的时候,就应该把这个共享数据置于更上层的widget中。
这里我们用一个CountModel来保存一个数据count:
class CountModel extends ChangeNotifier {
int _count = 0;
int currentCount() {
return _count;
}
void incrementCount() {
_count += 1;
notifyListeners();
}
}
然后将该数据模型保存在上层widget中(实例保存在了顶层widget中):
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CountModel(),
child: const MyApp(),
),
);
}
这样当数据发生变化后会通知ChangeNotifierProvider,ChangeNotifierProvider内部会重新构建InheritedWidget,从而更新依赖于InheritedWidget的子孙组件。
最后通过Consumer来使用数据:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
const Text('当前的数字'),
Consumer<CountModel>(
builder: ((context, countM, child) {
return Text('${countM.currentCount()}');
}),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
var counter = context.read<CountModel>();
counter.incrementCount();
},
child: const Icon(
Icons.add,
color: Colors.white,
),
),
);
}
}
尽量在实际使用数据的widget外层来包裹Consumer,这样可以尽可能少的重新构建widget。
如果不需要数据模型改变的时候刷新ui,只是访问该数据,那么可以使用Provider.of,并且将 listen 设置为 false。
Provider.of<CountModel>(context, listen: false).currentCount();
优势:
- 只要数据发生了改变,UI会自动更新,而不用手动调用
setState()。 - 数据改变所带来的通知传递被封装在provider中了,开发者不需要去处理状态改变事件的发布和订阅了。
GetX
Provider是基于InheritedWidget,全局状态的管理基于自上而下的context,所以这种方案就限制了状态管理必须在父子Widget中,会使得业务逻辑与视图之间有较强的耦合。而GetX不需要上下文,有自己的生命周期,,所以可以不依赖界面中的任何东西。
先来简单使用一下,还是以计数器为例。
首先我们还是先创建一个数据模型类,这个在GetX中被称为controller:
class CountController extends GetxController {
int _count = 0;
int get counter => _count;
void incrementCount() {
_count++;
update();
}
}
在使用中不需要像provider那样在父widget中保存数据模型,只需要在用到数据模型的widget外部包裹GetBuilder即可:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return GetBuilder<CountController>(
init: CountController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
Text('当前的数字: ${controller.counter}'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.incrementCount();
},
child: const Icon(
Icons.add,
color: Colors.white,
),
),
);
});
}
}
我们可以让builder的范围更小一点:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
GetBuilder<CountController>(
init: CountController(),
builder: (controller) => Text('当前的数字: ${controller.counter}'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
//controller.incrementCount();
Get.find<CountController>().incrementCount();
},
child: const Icon(
Icons.add,
color: Colors.white,
),
),
);
}
}
调整了两个位置,GetBuilder只包裹了Text,这个时候FloatingActionButton并不在GetBuilder的作用域里面,所以使用Get.find来找到数据模型进行修改,这里我们也可以看出,GetX是不需要像Provider那样依赖context的。
这里只是简单的使用了一下GetX,主要是用于和Flutter中其他的数据共享方式做一个简单的区别,GetX还有很多其他好用的功能,比如路由功能、obs响应式开发等等就不在此一一赘述了。