Flutter:从ValueListenableBuilder到Provider(三)|技术点评
上一节简单回顾
上一节里面,我们主要了解了单个页面,由多个变量控制Widget,其中核心的组件就是使用MultiProvider进行变量注册,然后通过Selector与Consumer组件对Widget进行创建与控制。并简单分析了Selector与Consumer组件的使用方法和区别。
那么这一节,我们就来聊的就是全局状态管理。这一节的代码可能不会太多,主要聊应用场景。
往期文章:
全局管理
我们先讲几个App开发的时候的应用场景,由于我是iOS端开发,Android端技术积累不了解,所以以iOS端的实现为主。再来就Flutter中,这些场景应该如何处理。
场景1:A页面,跳转到B页面,在B页面输入了一个姓名,点击确定后,回传A页面展示。
-
iOS端: 这是典型的相邻页面逆传值,B→A,推荐使用callback或delegate进行实现。当然使用Notification也是可以的。
-
Flutter端: 这当然是可以用callback。 不过系统给的pop函数配合协程不香么?下面是官方的代码注释:
void _accept() {
Navigator.pop(context, true); // dialog returns true
}
根本轮不到Provider出场,Flutter系统自带方法的解决战斗
场景2:A页面,由B1,B2两个子组件构成,B1可以操作,使得A页面状态变化,B2也可以操作,使得A页面状态也变化。
-
iOS端: 这是一个页面,由几个封装好的独立组件构成,需要在B1,B2组件中暴露callback或者delegate,然后在A页面进行统一的实现与逻辑处理。
-
Flutter端: 这当然也可以用callback。 不过考虑到组件B1,B2都在A页面之前,在A构建的时候,在A外层包裹一层Provider组件并注册相应的变量,然后在B1,B2页面使用,下面是代码实现:
class Counter extends ChangeNotifier {
int _count = 1;
int get count => _count;
set count(int value) {
_count = value;
notifyListeners();
}
}
class ExampleWithCompose extends StatelessWidget {
const ExampleWithCompose({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: PartManager(),
);
}
}
class PartManager extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Counter(),
child: _contentView(context),
);
}
Widget _contentView(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Part Manager"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'count值变化监听',
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Consumer<Counter>(
builder: (context, value, child) {
return Text(
'count:${value.count}',
);
},
),
),
AddButton(),
SubtractButton(),
FlatButton(
color: Colors.green,
child: Text("Next Page"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NextPage();
}),
);
},
),
],
),
),
);
}
}
class NextPage extends StatelessWidget {
const NextPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Next Page"),
),
body: Container(),
);
}
}
class AddButton extends StatelessWidget {
const AddButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
color: Colors.blue,
child: Text('count++'),
onPressed: () {
Provider.of<Counter>(context, listen: false).count++;
},
);
}
}
class SubtractButton extends StatelessWidget {
const SubtractButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
color: Colors.blue,
child: Text('count--'),
onPressed: () {
Provider.of<Counter>(context, listen: false).count--;
},
);
}
}
这个例子,看似有点多此一举,不过所以的页面都是由StatelessWidget构成,唯一精细化控制的是Consumer<Counter>
。
而且试想一下,随着业务量的增加,比如B1组件可以改变B2页面的状态,B2组件可以改变B1页面的状态等等,Provider针对的单一页面下的组件与页面的通信优势就越明显,回想一下,在上一节MultiProvider的例子,也算是这种场景的特例化,只是没有把组件单独拿抽出来封装而已。
另外,需要注意的是,本例子中有一个NextPage按钮,点击后会push到NextPage页面,通过DevTools可以看到,虽然是通过PartManager的操作push过来,但是在tree上面,两者没有任何关系,也不能在此页面调用Provider.of<Counter>(context, listen: false)
这类方法,切记切记!
场景3:TabBarController持有A,B,C, D四个页面,D页面做了一个操作,希望A页面的状态进行更改。
-
iOS端: 这是典型的跨页面数据传递,由于A页和D页面没有关联,无法使用callback或者delegate,使用Notification进行通知的发与收,来进行实现。
-
Flutter端: 这个可以使用EventBus来实现,实现原理与iOS的Notification方式类似,通过总线进行数据的发与收,达到改变数据,进而改变页面。 我们也可以考虑在顶层的MaterialApp之前包裹Provider组件并注册相应的变量组件并注册相应的变量,以达到任何页面都可以获取并改变该变量的机会,达到全局管理的目的。
class ExampleWithGlobalManager extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Counter(),
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: Text("Global Manager"),
bottom: TabBar(
tabs: [Text("A"), Text("B"), Text("C"), Text("D")],
),
),
body: TabBarView(
children: [
PageA(),
PageB(),
PageC(),
PageD(),
],
),
),
);
}
}
class PageA extends StatelessWidget {
const PageA({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Consumer<Counter>(
builder: (context, value, child) => Text(
'count:${value.count}',
),
),
),
);
}
}
class PageB extends StatelessWidget {
const PageB({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text("B"),
),
);
}
}
class PageC extends StatelessWidget {
const PageC({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text("C"),
),
);
}
}
class PageD extends StatelessWidget {
const PageD({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AddButton(),
SubtractButton(),
],
),
);
}
}
/// 下面是模型与组件代码
class Counter extends ChangeNotifier {
int _count = 1;
int get count => _count;
set count(int value) {
_count = value;
notifyListeners();
}
}
class AddButton extends StatelessWidget {
const AddButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
color: Colors.blue,
child: Text('count++'),
onPressed: () {
Provider.of<Counter>(context, listen: false).count++;
},
);
}
}
class SubtractButton extends StatelessWidget {
const SubtractButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlatButton(
color: Colors.blue,
child: Text('count--'),
onPressed: () {
Provider.of<Counter>(context, listen: false).count--;
},
);
}
}
由于gif帧率问题,实际操作中,count++与count--按钮都是按了很多下的。
全局管理,说明ChangeNotifierProvider立于所有Widget之上,基本上在整个tree的顶部,我们从DevTools就可以看出:
试想一下,如何全局更换主题颜色,App主语言?很多Flutter的Demo也有方案。典型的就是在MaterialApp组件上包裹MultiProvider,注册需要全局控制的变量即可,很多Flutter的Demo中也使用了这类方案。
总结
到此,从ValueListenableBuilder的单页面管理,到使用Provider进行单页面管理,最后展开到Provider的全局管理,在我理解范围内基本上已经完成。
在我刚入门Provider的时候,我觉得非常神秘,甚至是非常可怕,因为这货并不是App端开发的思维模式,更多的是前端类似Flux、Redux、Vuex这种思维的传承。
传统的App端开发,我们更多是通过callback或者发消息的机制进行跨页面、多页面的数据更新与操作,而在这个App端向大前端进化过程中(iOS中的SwiftUI、Android的ComposeUI、跨平台方案Flutter),这些框架不约而同的使用了声明式、响应式编程,可谓是盼星星盼月亮,也是大势所趋吧。
总之,如果可以减轻开发复杂度,在写代码的时候少掉些头发,何乐而不为呢?
转载自:https://juejin.cn/post/6938267386000867364