Flutter中我们经常使用setState来进行页面的rebuild。但为了更新某一个widget而在父widget中直接setState,会导致很多不必要的多次build问题。这里我们就以bottomNavigationBar为例。 Flutter中要实现底部导航栏效果,系统已经提供了现成的方法供我们使用,比如使用Scaffold,他的bottomNavigationBar属性即是配置底部导航栏的地方,在切换时显示不同的body内容即可。所以它的一般使用方式如下:body采用PageView,当点击底部tab时切换PageView(这里假设已经会使用了)
class _BottomNavState extends State<BottomNavPage> {
List<BottomNavigationBarItem> naviItems = [];
PageController _pageController;
int currentIndex=0;
@override
void initState() {
super.initState();
_pageController = PageController();
buildItems();
}
void _pageChanged(int index) {
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView.builder(
itemBuilder: (context, index) {
return PageTest(
title: "Page $index",
index: index,
);
},
controller: _pageController,
itemCount: 5,
physics: NeverScrollableScrollPhysics(),
onPageChanged: _pageChanged,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
items: naviItems,
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
onTap: (int index){
currentIndex = index;
setState(() {
_pageController.jumpToPage(index);
});
},
),
);
}
void buildItems() {
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_dh.png", "Page1", "assets/xz_dh_icon_dh.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_dt.png", "Page2", "assets/xz_dh_icon_dt.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_xx.png", "Page3", "assets/xz_dh_icon_xx.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_xs.png", "Page4", "assets/xz_dh_icon_xs.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_wd.png", "Page5", "assets/xz_dh_icon_wd.png"));
}
BottomNavigationBarItem buildOriginalNavItem(String labelPath, String title,
String activeLabelPath) {
return BottomNavigationBarItem(
icon: Image.asset(
labelPath,
width: 22,
height: 22,
),
label: title,
activeIcon: Image.asset(
activeLabelPath,
width: 22,
height: 22,
));
}
}
PageView 的每一页我们都用了一个StatefullWidget,定义如下(实际中可以定义不同的页面,这里主要是为了方便)
class PageTest extends StatefulWidget {
final String title;
final int index;
PageTest({this.title, this.index = 0,Key key}):super(key: key);
@override
State<StatefulWidget> createState() {
print("${title} createState");
return _PageTestState();
}
}
class _PageTestState extends State<PageTest> with AutomaticKeepAliveClientMixin<PageTest>{
List<Color> colors = [
Colors.purple,
Colors.deepOrange,
Colors.orangeAccent,
Colors.lightGreenAccent,
Colors.pink[100]
];
@override
void initState() {
super.initState();
print("${widget.title} initState");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("${widget.title} didChangeDependencies");
}
@override
void didUpdateWidget(covariant PageTest oldWidget) {
super.didUpdateWidget(oldWidget);
print("${widget.title} didUpdateWidget");
}
@override
void dispose() {
super.dispose();
print("${widget.title} dispose");
}
@override
Widget build(BuildContext context) {
super.build(context);
print("${widget.title} build");
return Scaffold(
appBar: AppBar(
leading: GestureDetector(
child: Icon(Icons.arrow_back),
onTap: () => onBackPressed(context),
),
title: Text(widget.title),
),
body: Container(
color: colors[widget.index],
),
);
}
void onBackPressed(BuildContext context) {
NavigatorState navigatorState = Navigator.of(context);
if (navigatorState.canPop()) {
navigatorState.pop();
} else {
SystemNavigator.pop();
}
}
@override
bool get wantKeepAlive => true;
}
可以看到在它的State类的定义中我们使用了AutomaticKeepAliveClientMixin
,并且让wantKeepAlive
方法返回了true。这么做的目的主要是想在tab切换时不重复创建。同时我们在他的createState,initState,didChangeDependencies,didUpdateWidget,build,dispose
方法中都打印了log。那么这样到底能不能达到目的,我们可以看一下实现的效果以及从第1页切换到第5页的log
从第一页依次切换到第五页log
可以看到第一次加载时只是执行了第一页的createState,initState,didChangeDependencies,build
这四个方法,切换到第二页时,除了执行第二页的同第一页的四个方法之外还执行了第一页的didUpdateWidget,build
这两个方法,以后每切换一个新的页面除了自身执行上述四个方法,已经创建完的页面都会执行didUpdateWidget,build
。都创建完之后,每次切换tab,每个页面都会执行这两个方法。比如从1依次切换到5,然后点击3
看来仅仅让PageView的页面保存状态,也不能解决多次build的问题。既然多调用了didUpdateWidget
和build
方法。那么我们就先看看上述这6个方法分别在什么时候会被调用。这其实就是通常所说的Statefullwidget的生命周期
可以看到生命周期方法如图所示,这也是为什么上面的log会添加在这几个方法里。其实这些方法可以分为三个阶段:创建(插入视图树)、更新(在视图树中存在)、销毁(从视图树中移除)。
创建:
State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染。
- 构造方法是 State 生命周期的起点,Flutter 会通过调用 StatefulWidget.createState() 来创建一个 State。我们可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据。这些配置数据,决定了 Widget 最初的呈现效果。
- initState,会在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作,比如为状态变量设定默认值。
- didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。
- build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。
更新:
Widget 的状态更新,主要由 3 个方法触发:setState、didchangeDependencies 与 didUpdateWidget。
- setState:我们最熟悉的方法之一。当状态数据发生变化时,我们总是通过调用这个方法告诉 Flutter:“我这儿的数据变啦,请使用更新后的数据重建 UI!”
- didChangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。哪些情况下 State 对象的依赖关系会发生变化呢?这个“依赖”指的就是子widget是否使用了父widget中InheritedWidget的数据!如果使用了,则代表子widget依赖有依赖InheritedWidget;如果没有使用则代表没有依赖。这种机制可以使子组件在所依赖的InheritedWidget变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子widget的didChangeDependencies方法将会被调用。
- didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。一旦这三个方法被调用,Flutter 随后就会销毁老 Widget,并调用 build 方法重建 Widget。
从生命周期的方法调用分析可以看出之所以每次切换tab会调用didUpdateWidget,build
的原因是因为父widget触发了重建(因为我们在onTap方法中调用了setState)。如果我们不调用setState又无法更新tab的选中状态。所以我们的解决方法就是自定义一个bottomNavigationBar,首先我们看看Scaffold的这个属性(final Widget? bottomNavigationBar;
)其实就是一个widget,而且是一个StatefullWidget,所以我们仿照这个写个简易版的bar,他的构造方法如下
BottomNavigationBar({
Key? key,
required this.items,
this.onTap,
this.currentIndex = 0,
this.elevation,
this.type,
Color? fixedColor,
this.backgroundColor,
this.iconSize = 24.0,
Color? selectedItemColor,
this.unselectedItemColor,
this.selectedIconTheme,
this.unselectedIconTheme,
this.selectedFontSize = 14.0,
this.unselectedFontSize = 12.0,
this.selectedLabelStyle,
this.unselectedLabelStyle,
this.showSelectedLabels,
this.showUnselectedLabels,
this.mouseCursor,
})
具体的属性作用基本上可以猜到,我这里就不详述了。可以看到这里的items是必须的,它是一个list(final List<BottomNavigationBarItem> items
),而这里具体的item为
class BottomNavigationBarItem {
const BottomNavigationBarItem({
required this.icon,
@Deprecated(
'Use "label" instead, as it allows for an improved text-scaling experience. '
'This feature was deprecated after v1.19.0.'
)
this.title,
this.label,
Widget? activeIcon,
this.backgroundColor,
this.tooltip,
}) : activeIcon = activeIcon ?? icon,
assert(label == null || title == null),
assert(icon != null);
...
}
其实就一个单纯的class,定义了每个tab的元素,比如选中的图标,未选中图光,背景,用于显示title的widget。至此系统封装的BottomNavigationBar元素分析已经结束了。我们也按照这种方式来封装自己的,比如这里的Item,我们定义如下
class CustomBottomNavItem {
///图片下的标题
final String title;
///显示的图片或者是自定义的widget(未选中)
final Widget label;
///显示的图片或者是自定义的widget(选中)
final Widget activeLabel;
///文本样式(选中)
final TextStyle selectedStyle;
///文本样式(未选中)
final TextStyle unselectedStyle;
///当前item在整个itemBar中的位置
int index;
CustomBottomNavItem(
{Key key,
this.title,
this.label,
this.selectedStyle,
this.unselectedStyle,
this.index,
this.activeLabel});
}
这里主要声明了我们需要元素,那么真正的容器也按照系统封装的方式定义如下
class CustomNavBar extends StatefulWidget {
///所有的底部item
final List<CustomBottomNavItem> items;
///选中时的文本样式
final TextStyle selectedStyle;
///未选中时的文本样式
final TextStyle unselectedStyle;
///tab的点击
final ValueChanged<int> onTap;
///底部bar的整体高度
final double height;
///底部bar的背景
final Color bgColor;
CustomNavBar(
{Key key,
this.items,
this.selectedStyle,
this.unselectedStyle,
this.height,
this.bgColor,
this.onTap})
: super(key: key);
@override
State<StatefulWidget> createState() {
return CustomNavBarStatus();
}
}
接下来的重点是如何在它的父widget中不用调用setState来更新这个控件。如果不想采用setState更新,那么一般会有这两种方式,一种是通过可监听对象比如ValueNotifier,在对象变化时通过监听器来重建相应的view;另一种则是为这个Widget设置key,在父Widget中通过这个key拿到子控件,调用子控件的setState方法,从而只刷新子Widget。这里我们首先使用ValueNotifier来实现。其实之所以需要重建,是因为当前选中的tab也就是index在不断变化,所以我们的ValueNotifier修饰的应该是currentIndex,所以它的定义为
ValueNotifier<int> currentIndex = ValueNotifier<int>(0);
如果需要可配置当前选中的index,在CustomNavBar中定义一个属性传递进来即可,这里先写死。我们在看看开篇图实现的效果,每一个tab是图片和文字组成,每一个tab整体可以点击。所以我们先来定义这个子Widget,其实就是一个Column,为了点击我们需要用GestureDetector包裹一下。如果想要定义间距的话,直接在CustomNavBar中定义即可,这里就省略了。在点击的时候更改当前currentIndex的值,他就可以触发自动重建样式而不用外部setState
Widget buildChild(CustomBottomNavItem item,int value) {
return GestureDetector(
onTap: () {
if (widget.onTap != null) widget.onTap(item.index);
currentIndex.value = item.index;
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
item.index == value
? item.activeLabel == null
? item.label
: item.activeLabel
: item.label,
Text(
item.title,
style: item.index == currentIndex.value
? item.selectedStyle
: item.unselectedStyle,
),
],
),
);
}
上面提到,这里使用注册监听器的方式刷新,所以在点击的时候我们调用了currentIndex.value=item.index
他实际上是调用了ValueNotifier
的set
方法
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
也就是发送了通知。所以我们要接收这个通知,并且创建这个widget。这里我们采用ValueListenableBuilder
来监听这个通知。从而这个State中的build方法我们实现如下
@override
Widget build(BuildContext context) {
return Container(
key: barKey,
height: widget.height,
color: widget.bgColor,
child: widget.items != null && widget.items.length > 0
? ValueListenableBuilder(valueListenable: currentIndex, builder: buildChildren)
: Container(),
);
}
这里的buildChildren方法如下
Widget buildChildren(BuildContext context, int value, Widget child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widget.items.map((item) => buildChild(item,value)).toList(),
);
}
这里我们直接写死了。用row直接平分宽度,也就是原有控件的fixed模式,如果想滑动之类的buildChildren也可以采用ListView等。至此整个view就可以用了,我们试试看看是否达到了目的
class _BottomNavState extends State<BottomNavPage> {
List<CustomBottomNavItem> items = [];
PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController();
initItems();
}
CustomBottomNavItem buildNavItem(String labelPath, String title, String activeLabelPath,
) {
return CustomBottomNavItem(
label: Image.asset(
labelPath,
width: 22,
height: 22,
),
title: title,
activeLabel: Image.asset(
activeLabelPath,
width: 22,
height: 22,
));
}
void _pageChanged(int index) {
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView.builder(
itemBuilder: (context, index) {
return PageTest(
title: "Page $index",
index: index,
);
},
controller: _pageController,
itemCount: 5,
physics: NeverScrollableScrollPhysics(),
onPageChanged: _pageChanged,
),
bottomNavigationBar: CustomNavBar(
items: items,
height: 67,
onTap: (index) {
_pageController.jumpToPage(index);
},
selectedStyle: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Color(0xff414344)),
bgColor: Colors.greenAccent,
unselectedStyle: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: Color(0xff414344)),
),
);
}
void buildItems() {
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_dh.png", "Page1", "assets/xz_dh_icon_dh.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_dt.png", "Page2", "assets/xz_dh_icon_dt.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_xx.png", "Page3", "assets/xz_dh_icon_xx.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_xs.png", "Page4", "assets/xz_dh_icon_xs.png"));
naviItems.add(buildOriginalNavItem(
"assets/dh_icon_wd.png", "Page5", "assets/xz_dh_icon_wd.png"));
}
void initItem() {
items.add(buildNavItem(
"assets/dh_icon_dh.png", "Page1", "assets/xz_dh_icon_dh.png"));
items.add(buildNavItem(
"assets/dh_icon_dt.png", "Page2", "assets/xz_dh_icon_dt.png"));
items.add(buildNavItem(
"assets/dh_icon_xx.png", "Page3", "assets/xz_dh_icon_xx.png"));
items.add(buildNavItem(
"assets/dh_icon_xs.png", "Page4", "assets/xz_dh_icon_xs.png"));
items.add(buildNavItem(
"assets/dh_icon_wd.png", "Page5", "assets/xz_dh_icon_wd.png"));
}
}
这会实现同样的效果,不过们可以看看log
首次加载页面时只会调用创建阶段的四个方法,以后无论怎们切换都不会调用生命周期方法。也就是说我们成功了。但是这里还有一点瑕疵,我们的PageView
的physics
属性设置的是 NeverScrollableScrollPhysics()
也就是只能点击tab切换吗,而不能滑动页面切换tab,如果不设置这个physics上面的代码在页面滑动时候下面的tab并没有相应的变化。如果想要有变化那么就得让他刷新。我们都知道在PageView滑动的时候会回调onPageChanged
方法,所以在这里我们可以更新tab。这就用到了利用key来更新的方法。其实这个很简单,只需要在需要更新的State中定义一个key,并且把这个key用于build中,同时提供获取这个state的方法,比如这里的定义如下
static GlobalKey barKey = GlobalKey();
static currentInstance() {
State<CustomNavBar> state =
CustomNavBarStatus.barKey.currentContext.findAncestorStateOfType();
return state;
}
setCurrentIndex(int index) {
currentIndex.value = index;
}
只需在onPageChanged方法中调用
CustomNavBarStatus.currentInstance().setCurrentIndex(index);
即可更新这个tabbar。所以整个tabBar的完整代码为
class CustomNavBar extends StatefulWidget {
///所有的底部item
final List<CustomBottomNavItem> items;
///选中时的文本样式
final TextStyle selectedStyle;
///未选中时的文本样式
final TextStyle unselectedStyle;
///tab的点击
final ValueChanged<int> onTap;
///底部bar的整体高度
final double height;
///底部bar的背景
final Color bgColor;
CustomNavBar(
{Key key,
this.items,
this.selectedStyle,
this.unselectedStyle,
this.height,
this.bgColor,
this.onTap})
: super(key: key);
@override
State<StatefulWidget> createState() {
return CustomNavBarStatus();
}
}
class CustomNavBarStatus extends State<CustomNavBar> {
static GlobalKey barKey = GlobalKey();
static currentInstance() {
State<CustomNavBar> state =
CustomNavBarStatus.barKey.currentContext.findAncestorStateOfType();
return state;
}
setCurrentIndex(int index) {
currentIndex.value = index;
}
ValueNotifier<int> currentIndex = ValueNotifier<int>(0);
@override
void initState() {
super.initState();
refreshChild();
}
void refreshChild() {
for (int i = 0; i < widget.items.length; i++) {
widget.items[i].index = i;
}
}
@override
Widget build(BuildContext context) {
return Container(
key: barKey,
height: widget.height,
color: widget.bgColor,
child: widget.items != null && widget.items.length > 0
? ValueListenableBuilder(valueListenable: currentIndex, builder: buildChildren)
: Container(),
);
}
Widget buildChild(CustomBottomNavItem item,int value) {
return GestureDetector(
onTap: () {
if (widget.onTap != null) widget.onTap(item.index);
currentIndex.value = item.index;
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
item.index == value
? item.activeLabel == null
? item.label
: item.activeLabel
: item.label,
Text(
item.title,
style: item.index == currentIndex.value
? item.selectedStyle
: item.unselectedStyle,
),
],
),
);
}
Widget buildChildren(BuildContext context, int value, Widget child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widget.items.map((item) => buildChild(item,value)).toList(),
);
}
}
至此整个过程就完毕了,欢迎大家指正