状态管理
刚接触Flutter开发时,不管是为了性能还是项目代码简洁等原因,总归还是会选择一个合适的状态管理工具。但是看着茫茫多的选择还是感觉无从下手,当然我也一样。下方记录一下各种状态管理(Riverpod、Provider、Getx)的使用,以便后续遇到其他项目使用不同状态管理时能快速上手。也希望为看到这篇文章的同学提供一些参考。
所以此篇不适合老司机们, 可以 退🤺! 退🤺! 退🤺!。
Riverpod
使用的前提需要注意,需要使用ProviderScope包裹父级节点
Provider
Provider可以简单的用来共享数据,也可以用来拆分逻辑用来监听一些值的变化。
///简单共享数据
final _counterProvider = Provider<int>((ref) => 1);
/// 使用的地方
Consumer(builder: (context, ref, child) {
return Text(ref.read(_counterProvider).toString());
},
)
用来拆分逻辑
class _UserInfo {
String phone;
String pwd;
_UserInfo({
this.phone = '',
this.pwd = '',
});
}
final _userInfoProvider = StateNotifierProvider<_UserInfoNotifier, _UserInfo>(
(ref) => _UserInfoNotifier());
final _isComplete = Provider<bool>((ref) {
final info = ref.watch(_userInfoProvider);
return info.phone.length == 11 && info.pwd.length >= 8;
});
Consumer(builder: (context, ref, _) {
final isComplete = ref.watch(_isComplete);
return TextButton(
onPressed: isComplete ? () {} : null,
child: const Text('登录'));
})
StateProvider
对于简单数据的状态管理,可以直接使用StateProvider,相比于Provider提供了直接修改数据的能力,相比于StateNotifierProvider,避免了为实现简单逻辑还要编写一个StateNotifier类。
final _counter = StateProvider((ref) => 0);
/// 改变状态值
ref.read(_counter.state).state += 1;
/// 重新设置为默认值
ref.refresh(_counter);
/// 监听值的改变
ref.listen(_counter, (_, value) {
print(value);
});
Consumer(builder: (context, ref, _) {
return Text(ref.watch(_counter).toString());
})
StateNotifierProvider
一般页面都会有比较复杂的逻辑,这个时候就推荐创建 CustomState
、 CustomStateNotifier\<CustomState>
, 以及提供StateNotifierProvider<CustomStateNotifier, CustomState>
下方代码可能太多,具体例子可以在此处查看
mixin CodeLoginProviders {
final manager =
StateNotifierProvider.autoDispose<CodeLoginStateNotifier, CodeLoginState>(
(ref) {
return CodeLoginStateNotifier();
});
late final canSendCode = Provider.autoDispose<bool>((ref) {
final state = ref.watch(manager);
return state.phone.length == 11;
});
...
}
class CodeLoginStateNotifier extends StateNotifier<CodeLoginState> {
CodeLoginStateNotifier()
: super(
CodeLoginState(phone: DeerStorage.phone),
);
Timer? _timer;
...
void sendCode() {
_startTimer();
}
void _startTimer() {
_stopTimer();
_hadSendCode = true;
_timeCount = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (state.timeCount == 1) {
_stopTimer();
} else {
_timeCount = state.timeCount - 1;
}
});
}
...
}
class CodeLoginState extends Equatable {
final String phone;
final String code;
final int timeCount;
final bool hadSendCode;
...
}
FutureProvider
FutureProvider可以用来进行异步操作,比如网络请求、应用启动加载配置等
/// 网络请求
final request = FutureProvider.autoDispose<int>((ref) async {
ref.onDispose(() {
/// 可以在此处取消网络请求
print('dispose');
});
await Future.delayed(const Duration(seconds: 2));
return Random().nextInt(1000);
});
Consumer(builder: (context, ref, _) {
return ref.watch(request).map(data: (value) {
if (value.isLoading) return const CircularProgressIndicator();
return Text('数据 ${value.value}');
}, error: (error) {
return Text(error.error.toString());
}, loading: (_) {
return const CircularProgressIndicator();
});
})
/// 模拟App启动时加载必要的配置
final sharePreference = FutureProvider<void>((ref) async {
/// 可以保存起来,封装存储 ,或者直接返回,使用此provider
await SharedPreferences.getInstance();
});
final loadConfig = FutureProvider<void>((ref) async {
/// 获取一些本地存储的数据等
await Future.delayed(const Duration(seconds: 2));
});
/// 其他配置
final otherFuture = FutureProvider<void>((ref) async {
await Future.delayed(const Duration(seconds: 4));
});
/// 统一配置状态完成provider
final appConfig = FutureProvider((ref) async {
await Future.wait([
ref.watch(sharePreference.future),
ref.watch(loadConfig.future),
ref.watch(otherFuture.future),
]);
});
// 可以在应用启动时包裹,提供 Splash页面。
Consumer(builder: (context, ref, _) {
return ref.watch(appConfig).when(data: (_) {
print(DateTime.now());
return Text('程序初始化完成');
}, error: (_, __) {
return Text('错误');
}, loading: () {
print(DateTime.now());
return Text('loading');
});
})
family & dependencies
像商品详情页,如果你的provider定义的是全局变量,可以使用family使用商品的id作为入参,提供不同的StateNotifier。我一般是使用mixin提供页面相关的provider,State混入即可,感觉这样更适合单页面的状态管理。
/// pageView切换
static final pageIndex = StateProvider<int>((ref) => 0);
static final isSelected = Provider.family<bool, int>((ref, arg) {
final index = ref.watch(pageIndex);
return index == arg;
}, dependencies: [pageIndex]);
/// 对应下标的item是否处于点击状态
final isSelected = ref.watch(HeaderProviders.isSelected(index));
overrideWithValue
可以设置provider的值 如例子中应用初始化配置完成后,为全局Provider设置当前用户的信息
/// 等待本地存储等加载完成后设置用户信息
final userInfo =
StateNotifierProvider<DeerUserInfoState, DeerUserInfo?>(
(ref) => throw UnimplementedError())
ProviderScope(
overrides: [
UserProviders.userInfo
.overrideWithValue(DeerUserInfoState(DeerStorage.userInfo)),
],
...
)
Riverpod的具体使用可以在此处找到
Provider
Provider
简单的提供数据共享,不会刷新UI,使用和InheritedWidget差不多,树
class ProviderPage extends StatelessWidget {
const ProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(ProviderRouters.counter),
),
body: Provider(
create: (context) => 2,// 提供数据
child: const _ProviderContainer(),
),
);
}
}
class _ProviderContainer extends StatelessWidget {
const _ProviderContainer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
// 获取数据
child: Consumer<int>(builder: (context, value, _) {
return Text(value.toString());
}),
);
}
}
ChangeNotifierProvider
提供数据共享,数据模型需要继承或者混入ChangeNotifier
,并且在数据改变时调用notifyListeners
,不过在实际开发中很少会只有一个属性变化,需要注意合理使用Consumer
和Selector
,避免不必要的刷新。
class ChangeNotifierProviderPage extends StatelessWidget {
const ChangeNotifierProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(ProviderRouters.changeNotifierProvider),
),
body: ChangeNotifierProvider(
create: (context) => _State(), // 提供状态
child: const Center(child: _CounterContainer()),
),
);
}
}
class _CounterContainer extends StatelessWidget {
const _CounterContainer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final _state = context.read<_State>();
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
// 如果有多个属性值更新,只想在一个值改变时刷新
Selector<_State, int>(
builder: (context, value, _) {
print('text - rebuild');
return Text(value.toString());
},
selector: (_, value) => value.count,
),
TextButton(
onPressed: _state.increase,
child: const Text('+'),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
/// 对比Selector
Consumer<_State>(builder: (context, value, _) {
print('switch - rebuild');
return Switch(
value: value.isOpen,
onChanged: (_) {},
);
}),
TextButton(
onPressed: _state.change,
child: const Text('打开/关闭'),
),
],
)
],
);
}
}
class _State extends ChangeNotifier {
int count = 0;
bool isOpen = false;
void increase() {
count += 1;
notifyListeners();
}
void change() {
isOpen = !isOpen;
notifyListeners();
}
}
FutureProvider
设置初始值
,接收一个Future
,在状态更新时刷新child
class FutureProviderPage extends StatelessWidget {
const FutureProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(ProviderRouters.futuerProvider),
),
body: FutureProvider.value(
value: Future.delayed(
const Duration(seconds: 2), () => _State()..loadCompleted()),
initialData: _State(),
child: Consumer<_State>(builder: (_, value, __) {
Widget container;
if (value.isLoading) {
container = const CircularProgressIndicator();
} else {
container = const Text('加载完成');
}
return Center(child: container);
}),
),
);
}
}
class _State {
bool isLoading = true;
void loadCompleted() {
isLoading = false;
}
}
StreamProvider
和FutureProvider一致,只是接收变成了一个Stream
class StreamProviderPage extends StatelessWidget {
const StreamProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(ProviderRouters.streamProvider),
),
body: StreamProvider<int>(
create: (_) {
return Stream.periodic(const Duration(seconds: 1), (value) {
return value;
});
},
initialData: 0,
builder: (context, __) {
return Center(
/// 此处使用context.watch,原理和Consumer是一样的
child: Text(context.watch<int>().toString()),
);
},
),
);
}
}
ProxyProvider
如果一个模型需要依赖另外一个或者多个模型值的改变,则可以使用ProxyProvider
class ProxyProviderPage extends StatelessWidget {
const ProxyProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => _Counter()),
ProxyProvider<_Counter, _Logic>(
create: (_) => _Logic(number: 0),
update: (context, value, _) {
return _Logic(number: value.count);
},
),
],
child: Scaffold(
appBar: AppBar(title: const Text('data')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Consumer<_Counter>(
builder: (_, value, __) => Text(value.count.toString())),
Consumer<_Logic>(builder: (_, value, __) => Text(value.desc)),
Builder(builder: (context) {
return TextButton(
onPressed: () {
context.read<_Counter>().increase();
},
child: const Text('+'),
);
})
],
),
),
),
);
}
}
class _Counter extends ChangeNotifier {
int count = 0;
void increase() {
count += 1;
notifyListeners();
}
}
class _Logic {
int number;
_Logic({
required this.number,
});
String get desc => number.isEven ? "偶数" : "奇数";
}
ChangeNotifierProxyProvider
如果一个数据模型自己是一个ChangeNotifier,并且它需要依赖其他数据模型的数据,则可以使用ChangeNotifierProxyProvider,更适合使用于依赖的数据模型的数据是不变的, 当然依赖的其他数据是ChangeNotifier也是可以的。
可以查看官方demo
class ChangeNotifierProxyProviderPage extends StatelessWidget {
const ChangeNotifierProxyProviderPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider(create: (context) => _Letters()),
ChangeNotifierProxyProvider<_Letters, _LikeLetters>(
create: (_) => _LikeLetters(),
update: (context, value, like) {
return like!..updateLetters(value.letters);
},
),
],
child: Scaffold(
appBar: AppBar(title: const Text('data')),
body: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Expanded(child: AllListView()),
Divider(),
Expanded(child: LikeListView()),
],
),
),
);
}
}
class AllListView extends StatelessWidget {
const AllListView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final letters =
context.select<_Letters, List<String>>((value) => value.letters);
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 10),
itemBuilder: (context, index) {
final letter = letters[index];
return Row(children: [
Expanded(
child: Text(letter),
),
LikeButton(
letter: letter,
),
]);
},
itemExtent: 50,
itemCount: letters.length,
);
}
}
class LikeListView extends StatelessWidget {
const LikeListView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final letters = context.watch<_LikeLetters>().like;
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 10),
itemBuilder: (context, index) {
final letter = letters[index];
return Text(letter);
},
itemExtent: 50,
itemCount: letters.length,
);
}
}
class LikeButton extends StatelessWidget {
final String letter;
const LikeButton({
Key? key,
required this.letter,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final _like = context.watch<_LikeLetters>();
final isLike = _like.isLike(letter);
return TextButton(
onPressed: () {
isLike ? _like.removeLetter(letter) : _like.addLetter(letter);
},
child: Text(isLike ? "不喜欢" : "喜欢"));
}
}
class _Letters {
List<String> letters = [
'A',
'B',
'C',
'D',
'E',
];
}
class _LikeLetters extends ChangeNotifier {
late List<String> _letters;
List<String> get letters => _letters;
List<String> like = [];
void updateLetters(List<String> value) {
_letters = value;
notifyListeners();
}
bool isLike(String letter) => like.contains(letter);
void addLetter(String letter) {
like.add(letter);
notifyListeners();
}
void removeLetter(String letter) {
like.remove(letter);
notifyListeners();
}
}
GetX
项目中按照GetxController+GetBuilder基本可以大部分状态管理问题,这里只做简单使用记录,Getx有一篇文章,感兴趣的可以去看一下
Obx
一般 obs 配合 obx 使用
class ObxPage extends StatelessWidget {
ObxPage({Key? key}) : super(key: key);
final _count = 0.obs;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(GetXRouter.obx),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Text(_count.value.toString())),
TextButton(
onPressed: () => _count.value += 1,
child: const Text('+'),
)
],
)),
);
}
}
Getbuilder
一般 GetxController 和 Getbuilder配合使用
class GetBuilderPage extends StatelessWidget {
GetBuilderPage({Key? key}) : super(key: key);
final controller = Get.put(_Controller());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(GetXRouter.getBuilder),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
GetBuilder<_Controller>(
builder: (controller) {
print('rebuild A');
return Text('A: ${controller.a}');
},
filter: (controller) => controller.a,
),
TextButton(
onPressed: () {
controller.increaseA();
},
child: const Text('A +')),
GetBuilder<_Controller>(
builder: (controller) {
print('rebuild B');
return Text('B: ${controller.b}');
},
filter: (controller) => controller.b,
),
TextButton(
onPressed: () {
controller.increaseB();
},
child: const Text('B +')),
],
)),
);
}
}
class _Controller extends GetxController {
var a = 0;
var b = 0;
void increaseA() {
a += 1;
update();
}
void increaseB() {
b += 1;
update();
}
}
最后
还有一些其它就不在此处做记录了,总体来说我是喜欢Riverpod的,配合hooks_riverpod你会感到什么叫做幸福的。Riverpod作为Provider的改进版本(同一作者),Provider的选择优先级可以降一降。当然Getx也很好,毕竟star量在那里。这里有一篇GetX的详细教程,作者还写有flutter_bloc、fish_redux等框架的使用文章,有需要可以去查看
本篇文章demo在这里