Flutter学习 功能型Widget
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习 功能型Widget相关的知识,希望对你有一定的参考价值。
文章目录
1. WillPopScope
是导航返回拦截的组件, 类似于 android 中封装的 onBackPress
方法,来看看它的构造函数:
class WillPopScope extends StatefulWidget
const WillPopScope(
Key? key,
required this.child,
required this.onWillPop,
)
onWillPop
是回调函数, 当用户点击该按钮时被回调,该函数需要返回一个 Future
对象,如果返回 Future 最终值为 false 时,则当前路由不出栈, 如果最终值为 true 时, 则当前路由出栈
1.1 示例
下面代码是为了防止误触而关闭当前页面的返回键拦截示例, 如果1s内两次点击返回按钮,则退出,如果超过1s,则重新计时:
class _WillPopScopeRouteState extends State<WillPopScopeRoute>
// 上次的点击时间
DateTime? _lastPressedAt;
@override
Widget build(BuildContext context)
return Scaffold(
body: WillPopScope(
onWillPop: () async
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 1))
_lastPressedAt = DateTime.now();
return false;
return true;
,
child: Container(
alignment: Alignment.center,
child: Text("1s 内连续按两次返回键退出"),
),
));
2. InheritedWidget
用于数据共享的组件,提供了一种在 Widget 树中从上到下共享数据的方式,比如我们在应用的根 Widget 中通过 InheritedWidget
共享了一个数据,那我们可以在任意子 Widget 树中去获取该共享数据。
这个特性在一些需要整个 Widget 中共享数据的场景中非常方便。比如 Flutter 正是通过该组件来实现 共享应用主题 和 Locale 信息。
2.1 didChangeDependencies
在学习 StatefulWidget
时,我们提到了 State
对象有一个 didChangeDependencies
的回调,它会在 “依赖” 发生变化的时候被Flutter框架调用,而这个 “依赖” 就是 子Widget 是否使用了 父Widget 中 InheritedWidget
的数据,如果使用了,则代表 子Widget 有依赖, 如果没有则表示没有这种依赖。
这种机制可以使子组件所依赖的 InheritedWidget
变化时来更新自身, 比如主题、locale 等发生变化时,依赖其 子Widget 的 didChangeDEpendencies
方法就会被调用
下面来看下官方示例中, 计算器应用的 InheritedWidget
版本:
class ShareDataWidget extends InheritedWidget
ShareDataWidget(Key? key, required this.data, required Widget child)
: super(key: key, child: child);
// 共享数据,代表被点击的次数
final int data;
// 提供一个便捷方法,用于给树中 子Widget 获取共享数据
static ShareDataWidget? of(BuildContext context)
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
// 该回调决定了当 data 发生变化的时,是否通知子树中依赖data的widget
@override
bool updateShouldNotify(ShareDataWidget oldWidget)
return oldWidget.data != data;
class _TestWidget extends StatefulWidget
@override
State<_TestWidget> createState() => _TestWidgetState();
class _TestWidgetState extends State<_TestWidget>
@override
Widget build(BuildContext context)
return Text(ShareDataWidget.of(context)!.data.toString());
@override
void didChangeDependencies()
super.didChangeDependencies();
// 父或祖先Widget 中的 InheritedWidget 发生了改变时被调用
// 如果build中没有依赖,则不会调用该回调
print("依赖发生了改变");
class InheritedWidgetTestRoute extends StatefulWidget
@override
_InheritedWidgetTestRouteState createState() =>
_InheritedWidgetTestRouteState();
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute>
int cnt = 0;
@override
Widget build(BuildContext context)
return Scaffold(
body: Center(
child: ShareDataWidget(
data: cnt,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: _TestWidget()),
ElevatedButton(
onPressed: () => setState(()
++cnt;
),
child: const Text("自增"))
],
),
),
),
);
运行后,每当点击自增按钮,打印台就会打印:
如果 _TestWidget 中没有使用 ShareDataWidget 中的数据,那么它的 didChangeDependencies() 将不会调用,因为没有依赖其数据。
didChangeDependencies 中可以做什么?
一般来说, 子 Widget 很少会重写该方法,因为在依赖改变后, Flutter 框架也会调用 build
方法重新构建组件树,但是如果需要在依赖改变后执行一些昂贵的操作,比如数据库存储或者网络库请求,这时最好的方式就是在此方法中执行,这样可以避免每次 build 都去执行这些昂贵的操作。
2.2 深入了解 InheritedWidget
如果我们只想在 _TestWidgetState
中引用 ShareDataWidget 的数据,却不希望 ShareDataWidget 发生变化时调用了 _TestWidgetState
的方法应该怎么办呢?
我们只需要改一下 ShareDataWidget.of()
的实现方式:
static ShareDataWidget? of(BuildContext context)
// return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget;
唯一的改动是把 dependOnInheritedWidgetOfExactType
方法换成了 getElementForInheritedWidgetOfExactType
,他们有什么区别呢?我们来看下这两个方法的源码:
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>()
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(Object? aspect)
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null)
// 比前者多调用了 dependOnInheritedElement 方法
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
_hadUnsatisfiedDependencies = true;
return null;
来看下 dependOnInheritedElement
:
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, Object? aspect )
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
dependOnInheritedElement
方法主要是注册了依赖关系,加进到一个 HashSet 中。而 getElementForInheritedWidgetOfExactType()
不会。
需要注意的是:上面的示例中如果改成了 getElementForInheritedWidgetOfExactType
的实现方式, 运行示例后,会发现 _TestWidgetState
的 didChangeDependencies
不会再被调用,但是build方法会调用,这是因为点击了自增按钮后,会调用 setState,重构整个页面, 而 _TestWidget
并没有做缓存,所以它也会被重建,所以会调用 build
方法
那么就引入了一个新的问题: 实际上,我们只想更新子树中依赖了 ShareDataWidget
的子节点,而在调用了父组件(这里是 _InheritedWidgetTestRouteState
)setState
方法必然会导致所有子节点build。 这会赵成不必要的浪费,而且可能会出现问题。
而 缓存数据 可以解决这个问题, 就是通过封装一个 StatefulWidget
将 子Widget 树缓存起来,下面就来实现一个 Provider
来演示。
3. Provider
Provider 包的思想是: 将需要跨组件共享的状态保存在 InheritedWidget
中,然后子组件引用 InheritedWidget
, InheritedWidget
会绑定子组件产生依赖关系,然后当数据发生改变时,自动更新子孙组件。
为了加强理解,这里不直接看 Provider 实现,而是实习哪一个最小功能的 Provider
3.1 实现简易 Provider
这里引入泛型 ,便于外界能够保存更通用的数据
class InheritedProvider<T> extends InheritedWidget
InheritedProvider(required this.data, required Widget child)
: super(child: child);
final T data;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget)
// 这里先返回true
return true;
第二步,我们来实现 “数据发生改变时该如何改变?”, 这里的做法是通过使用加监听器, Flutter 中有 ChangeNotifier
,继承自 Listenable
,是一个发布-订阅者模式,通过 addListener
、 removeListener
来添加监听者, 用 notifyListener
来触发监听器的回调。
所以我们将共享的状态放到一个 Model 类中,然后让它继承自 ChangeNotifier
, 这样当共享状态改变时,只需要调用 notify 就可以通知订阅者,订阅者来重新构建 InheritedProvider 了:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget
ChangeNotifierProvider(Key? key, required this.data, required this.child);
final Widget child;
final T data;
static T of<T>(BuildContext context)
final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
return provider!.data;
@override
State<StatefulWidget> createState() => _ChangeNotifierProviderState<T>();
该类继承自 StatefulWidget
,然后提供 of 方法供子类方便获取 Widget 树中的 InheritedProvider
中保存的共享状态, 下面来实现该类对应的 State 类:
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider>
void update()
setState(()
// 如果数据发生了变化,则重新构建 InheritedProvider
);
@override
void didUpdateWidget(
covariant ChangeNotifierProvider<ChangeNotifier> oldWidget)
if (widget.data != oldWidget.data)
// 当 Provider 更新时,如果新旧数据不相同,则解绑截数据监听,同时添加新数据监听
oldWidget.data.removeListener(update);
widget.data.addListener(update);
super.didUpdateWidget(oldWidget);
@override
void initState()
// 给 model 添加监听器
widget.data.addListener(update);
super.initState();
@override
void dispose()
widget.data.removeListener(update);
super.dispose();
@override
Widget build(BuildContext context)
return InheritedProvider(
data: widget.data,
child: widget.child,
);
可以看到, _ChangeNotifierProviderState
类的主要作用是监听到共享状态改变时,重新构建 Widget 树。在 _ChangeNotiferProviderState
中调用 setState 方法, widget.child
始终是同一个,所以执行 build 时, InheritedProvider
的child引用的始终是同一个 子widget, 所以 widget.child
并不会重新 build , 这也就相当于对 child 进行了缓存,当然如果 ChangeNotifierProvider 的 父Widget 重新build 时, 则其传入的 child 可能会发生变化。
接下来我们用该组件实现一个 购物车示例。
3.1.1购物车示例
我们需要实现一个显示购物车中所有商品总价的功能,而这个价格显然就是我们想要共享的状态, 因为购物车的价格会随着商品的添加和移除而改变。
我们来定义一个 Item
类,用于表示商品信息:
class Item
Item(this.price, this.count);
// 商品单价
double price;
// 商品数量
int count;
接着定义一个保存购物车内商品数据的 CartModel
类:
class CartModel extends ChangeNotifier
final List<Item> _items = [];
// 禁止改变购物车里面的信息
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
// 总价
double get totalPrice => _items.fold(
0,
(previousValue, element) =>
previousValue + element.count * element.price);
// 将 [item] 添加到购物车, 该方法的作用是外部改变购物车
void add(Item item)
_items.add(item);
// 通知订阅者重新构建 InheritedProvider 来更新状态
notifyListeners();
这个 CartModel 就是我们需要跨组件共享的数据类型,最后我们写一个示例页面:
class _ProviderRouteState extends State<ProviderRoute>
@override
Widget build(BuildContext context)
return Scaffold(
body: Center(
child: ChangeNotifierProvider<CartModel>(
data: CartModel(),
child: Builder(builder: (context)
return Column(
children: [
Builder(builder: (context)
var cart = ChangeNotifierProvider.of<CartModel>(context);
return Text("总价为:$cart?.totalPrice");
),
Builder(builder: (context)
print("ElevatedButton build");
return ElevatedButton(
onPressed: ()
ChangeNotifierProvider.of<CartModel>(context)
?.add(Item(15, 1));
,
child: Text("添加商品"));
)
],
);
),
),
),
);
接下来每次点击添加商品的按钮都会增加15块钱。一般来说, ChangeNotifierProvider
作为整个 App 的路由优势会非常明显,可以共享数据到整个App中去。Provider 的模型如下图所示:
使用 Provider 后带来的好处有:
- 业务代码值关心数据更新,只需要更新 Model, UI就会自动更新,而不用在状态改变后去手动调用 setState 来显示刷新页面
- 数据改变的消息传递被屏蔽了
- 大型复杂场景下,使用全局共享变量会简化代码逻辑
4. 主题 Theme
ThemeData
用于保存 Material 组件库中的主题数据, 它包含了可以自定义的部分,我们可以通过 ThemeData 来自定义应用主题,在子组件中,我们可以通过 Theme.of
方法来获取当前的 ThemeData。
ThemeData 的可定义属性非常之多,下面截取一些常用的构造属性:
ThemeData(
Brightness? brightness, //深色还是浅色
MaterialColor? primarySwatch, //主题颜色样本,见下面介绍
Color? primaryColor, //主色,决定导航栏颜色
Color? cardColor, //卡片颜色
Color? dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
ColorScheme? colorScheme,
...
)
下面我们来实现一个路由换肤的功能:
class _ThemeRouteState extends State<ThemeRoute>
// 当前主题颜色
MaterialColor _themeColor = Colors.teal;
@override
Widget build(BuildContext context)
ThemeData themeData = Theme.of(context);
return Theme(
data: ThemeData(
// 用于导航栏、 FloatingActionButton 的颜色
primarySwatch: _themeColor,
// 用于 Icon 的颜色
iconTheme: IconThemeData(color: _themeColor)),
child: Scaffold(
appBar: AppBar(title: Text("主题测试")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 第一行 Icon 使用主题中的 iconTheme
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite),
Icon(Icons.airport_shuttle),
Text("颜色跟随主题")
],
),
// 第二行 Icon 自定义颜色
Theme(
data: themeData.copyWith(
iconTheme: themeData.iconTheme.copyWith(color: Colors.blue)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite),
Icon(Icons.airport_shuttle),
Text("颜色固定蓝色")
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _themeColor =
_themeColor == Colors.teal ? Colors.green : Colors.teal),
child: Icon(Icons.palette),
),
),
);
效果如下:
我们可以通过局部主题覆盖全局主题,如果需要对整个应用换肤,可以修改 MaterialApp
的 theme
5. ValueListenableBuilder
InheritedWidget
提供了一种从上到下的数据共享方式,而有些场景并非从上到下传递,比如横向传递或者从下到上,为了解决这个问题, Flutter 提供了 ValueListenableBuilder
组件,它的功能是监听一个数据源,如果数据源发生了变化,则会重新执行其 builder。
定义为:
const ValueListenableBuilder(
Key? key,
required this.valueListenable,
required this.builder,
this.child,
)
valueListenable
表示一个可监听的数据源, 类型为ValueListenable<T>
builder
数据源发生变化通知时, 会重新调用 builder 重新 build 子组件树child
builder 中每次都会重新构建子组件树,如果子组件树中有一些不变的部分,可以将其传递给 child, child 会作为 builder 的第三个参数传递给 builder, 通过这种方式就可以实现组件缓存
5.1 示例
class _ValueListenableState extends State<ValueListenableRoute>
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
static const double textScaleFactor = 1.5;
@override
Widget build(BuildContext context)
print("build");
return Scaffold(
bodyFlutter-Widget-学习笔记