一个正常的Flutter项目中,通过底部导航栏(BottomNavigationBar
)或者标签栏(TabBar
)组件来切换页面内容,是很正常的操作。但是大家是否有发现,每次导航栏或标签栏切换页面时,之前的页面就被清理了。比如,第一个页面的列表视图(ListView
)已经滑动到底部,切换到第二个页面以后再回来,第一个页面的列表又回到了顶部。
出现上面问题的原因是,页面的状态(State
)没有被保留下来,所以每次页面,都会初始化一次状态。我们可以通过自动保持活动客户端混合(AutomaticKeepAliveClientMixin
)抽象类来解决这个问题。
首先是新建一个dart文件,把Flutter应用和页面的架子搭起来,然后运行调试,确认应用可以正常跑起来。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo 主页'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// TODO:当前页面视图的相关变量。
// TODO:切换页面视图的方法。
// TODO:集中管理导航项目列表和页面组件列表。
@override
Widget build(BuildContext context) {
return Scaffold(
// TODO:页面视图的主要内容。
);
}
}
下面再新建一个dart文件,作为导航项对应的页面组件,并再里面放上一个列表视图(ListView
)组件,用来实验后面的代码是否实现保持页面的状态(State
)。
自动保持活动客户端混合(AutomaticKeepAliveClientMixin
)抽象类,为自动保持活动(AutomaticKeepAlive
)的客户端提供直接可以使用的方法,与状态(State
)子类一起使用,可以避免作为父组件的页面视图(PageView
)组件切换时被重新绘制。
自动保持活动客户端混合(AutomaticKeepAliveClientMixin
)抽象类的想要保持活动(wantKeepAlive
)属性,用于设置当前实例是否应保持活动状态,不因父组件的切换而重新绘制。这一步很关键。
/// 自定义的导航页面组件。
class NavigationPage extends StatefulWidget {
@override
_NavigationPageState createState() => _NavigationPageState();
}
/// 与自定义的导航页面组件关联的状态子类。
class _NavigationPageState extends State<NavigationPage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: ListView.builder(
padding: EdgeInsets.all(8.0),
itemExtent: 20.0,
itemBuilder: (BuildContext context, int index) {
return Text('项目 $index');
},
),
);
}
}
接下来要配置底部导航栏(BottomNavigationBar
)组件的项目(items
)属性需要的项目列表,以及对应的页面组件。
// TODO:集中管理导航项目列表和页面组件列表。
/// 统一管理导航项目的列表。
List<BottomNavigationBarItem> navigationItem = [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('首页'),
),
BottomNavigationBarItem(
icon: Icon(Icons.comment),
title: Text('消息'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('设置'),
),
];
/// 统一管理导航项目对应的组件列表。
final _widgetOptions = [
NavigationPage(),
NavigationPage(),
NavigationPage(),
];
然后定义一下页面视图(PageView
)组件的选择索引、控制器及资源释放函数。
// TODO:当前页面视图的相关变量。
/// 当前页面选择的索引。
int _selectedIndex = 0;
/// 页面控制器(`PageController`)组件,页面视图(`PageView`)的控制器。
PageController _controller = PageController();
/// 释放相关资源。
@override
void dispose() {
_controller.dispose();
super.dispose();
}
因为我们使用来页面视图(PageView
)组件,而页面视图组件不会自动切换页面,因此我们要自己写一下切换页面的方法。
// TODO:切换页面视图的方法。
void _onItemTapped(int index) {
// 创建底部导航栏的组件需要跟踪当前索引并调用`setState`以使用新提供的索引重建它。
setState(() {
_selectedIndex = index;
// 跳到页面(`jumpToPage`)方法,更改显示在的页面视图(`PageView`)组件中页面。
_controller.jumpToPage(index);
});
}
最后完成脚手架(Scaffold
)组件上的主体内容,把上面的内容都派上用场。
// TODO:页面视图的主要内容。
// 页面视图(`PageView`)组件,可逐页工作的可滚动列表,每个子项都被强制与视窗大小相同。
body: PageView.builder(
// 物理(`physics`)属性,页面视图应如何响应用户输入。
// 从不可滚动滚动物理(`NeverScrollableScrollPhysics`)类,不允许用户滚动。
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return _widgetOptions.elementAt(index);
},
itemCount: _widgetOptions.length,
// 控制器(`controller`)属性,用于控制滚动此页面视图位置的对象。
controller: _controller,
),
// 底部导航栏(`bottomNavigationBar`)属性,显示在脚手架(`Scaffold`)组件的底部。
// 底部导航栏(`BottomNavigationBar`)组件,显示在应用程序底部的组件,
// 用于在几个屏幕之间中进行选择,通常在三到五之间,再多就不好看了。
bottomNavigationBar: BottomNavigationBar(
// 项目(`items`)属性,位于底部导航栏中的交互组件,其中每一项都有一个图标和标题。
items: navigationItem,
// 目前的索引(`currentIndex`)属性,当前活动项的项目索引。
currentIndex: _selectedIndex,
// 固定颜色(`fixedColor`)属性,当BottomNavigationBarType.fixed时所选项目的颜色。
fixedColor: Color(0xffFE7C30),
// 在点击(`onTap`)属性,点击项目时调用的回调。
onTap: _onItemTapped,
// 定义底部导航栏(`BottomNavigationBar`)组件的布局和行为。
type: BottomNavigationBarType.fixed,
),
运行代码,实现效果如下面的图片。