Flutter实现页面切换后保持原页面状态的3种方法

前言:

在Flutter应用中,导航栏切换页面后默认情况下会丢失原页面状态,即每次进入页面时都会重新初始化状态,如果在initState中打印日志,会发现每次进入时都会输出,显然这样增加了额外的开销,并且带来了不好的用户体验。
在正文之前,先看一些常见的App导航,以喜马拉雅FM为例:

它拥有一个固定的底部导航以及首页的顶部导航,可以看到不管是点击底部导航切换页面还是在首页左右侧滑切换页面,之前的页面状态都是始终维持的,下面就具体介绍下如何在flutter中实现类似喜马拉雅的导航效果

第一步:实现固定的底部导航

在通过flutter create生成的项目模板中,我们先简化一下代码,将MyHomePage提取到一个单独的home.dart文件,并在Scaffold脚手架中添加bottomNavigationBar底部导航,在body中展示当前选中的子页面。

/// home.dart
import 'package:flutter/material.dart';

import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 final items = [
 BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
 BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('听')),
 BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
 ];

 final bodyList = [FirstPage(), SecondPage(), ThirdPage()];

 int currentIndex = 0;

 void onTap(int index) {
 setState(() {
 currentIndex = index;
 });
 }

 @override
 Widget build(BuildContext context) {
 return Scaffold(
 appBar: AppBar(
  title: Text('demo'),
 ),
 bottomNavigationBar: BottomNavigationBar(
  items: items,
  currentIndex: currentIndex,
  onTap: onTap
 ),
 body: bodyList[currentIndex]
 );
 }
}

其中的三个子页面结构相同,均显示一个计数器和一个加号按钮,以first_page.dart为例:

/// first_page.dart
import 'package:flutter/material.dart';

class FirstPage extends StatefulWidget {
 @override
 _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
 int count = 0;

 void add() {
 setState(() {
 count++;
 });
 }

 @override
 Widget build(BuildContext context) {
 return Scaffold(
 body: Center(
  child: Text('First: $count', style: TextStyle(fontSize: 30))
 ),
 floatingActionButton: FloatingActionButton(
  onPressed: add,
  child: Icon(Icons.add),
 )
 );
 }
}

当前效果如下:

可以看到,从第二页切换回第一页时,第一页的状态已经丢失

第二步:实现底部导航切换时保持原页面状态

可能有些小伙伴在搜索后会开始直接使用官方推荐的AutomaticKeepAliveClientMixin,通过在子页面的State类重写wantKeepAlive为true 。 然而,如果你的代码和我上面的类似,body中并没有使用PageView或TabBarView,很不幸的告诉你,踩到坑了,这样是无效的,原因后面再详述。现在我们先来介绍另外两种方式:

① 使用IndexedStack实现

IndexedStack继承自Stack,它的作用是显示第index个child,其它child在页面上是不可见的,但所有child的状态都被保持,所以这个Widget可以实现我们的需求,我们只需要将现在的body用IndexedStack包裹一层即可

/// home.dart
class _MyHomePageState extends State<MyHomePage> {
 ...
 ...
 ...
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 appBar: AppBar(
  title: Text('demo'),
 ),
 bottomNavigationBar: BottomNavigationBar(
  items: items, currentIndex: currentIndex, onTap: onTap),
 // body: bodyList[currentIndex]
 body: IndexedStack(
  index: currentIndex,
  children: bodyList,
 ));
 }

保存后再次测试一下

② 使用Offstage实现

Offstage的作用十分简单,通过一个参数来控制child是否显示,所以我们同样可以组合使用Offstage来实现该需求,其实现原理与IndexedStack类似

/// home.dart
class _MyHomePageState extends State<MyHomePage> {
 ...
 ...
 ...
 @override
 Widget build(BuildContext context) {
 return Scaffold(
  appBar: AppBar(
   title: Text('demo'),
  ),
  bottomNavigationBar: BottomNavigationBar(
   items: items, currentIndex: currentIndex, onTap: onTap),
  // body: bodyList[currentIndex],
  body: Stack(
   children: [
   Offstage(
    offstage: currentIndex != 0,
    child: bodyList[0],
   ),
   Offstage(
    offstage: currentIndex != 1,
    child: bodyList[1],
   ),
   Offstage(
    offstage: currentIndex != 2,
    child: bodyList[2],
   ),
   ],
  ));
 }
}

在上面的两种方式中都可以实现保持原页面状态的需求,但这里有一些开销上的问题,有经验的小伙伴应该能发现当应用第一次加载的时候,所有子页状态都被实例化了(>这里的细节并不是因为我直接把子页实例化放在bodyList里...<),如果在子页State的initState中打印日志,可以在终端看到一次性输出了所有子页的日志。下面就介绍另一种通过继承AutomaticKeepAliveClientMixin的方式来更好的实现保持状态。

第三步:实现首页的顶部导航

首先我们通过配合使用TabBar+TabBarView+AutomaticKeepAliveClientMixin来实现顶部导航(注意:TabBar和TabBarView需要提供controller,如果自己没有定义,则必须使用DefaultTabController包裹)。此处也可以选择使用PageView,后面会介绍。

我们先在home.dart文件移除Scaffold脚手架中的appBar顶部工具栏,然后开始重写首页first_page.dart:

/// first_page.dart
import 'package:flutter/material.dart';

import './recommend_page.dart';
import './vip_page.dart';
import './novel_page.dart';
import './live_page.dart';

class _TabData {
 final Widget tab;
 final Widget body;
 _TabData({this.tab, this.body});
}

final _tabDataList = <_TabData>[
 _TabData(tab: Text('推荐'), body: RecommendPage()),
 _TabData(tab: Text('VIP'), body: VipPage()),
 _TabData(tab: Text('小说'), body: NovelPage()),
 _TabData(tab: Text('直播'), body: LivePage())
];

class FirstPage extends StatefulWidget {
 @override
 _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
 final tabBarList = _tabDataList.map((item) => item.tab).toList();
 final tabBarViewList = _tabDataList.map((item) => item.body).toList();

 @override
 Widget build(BuildContext context) {
 return DefaultTabController(
  length: tabBarList.length,
  child: Column(
   children: <Widget>[
   Container(
    width: double.infinity,
    height: 80,
    padding: EdgeInsets.fromLTRB(20, 24, 0, 0),
    alignment: Alignment.centerLeft,
    color: Colors.black,
    child: TabBar(
     isScrollable: true,
     indicatorColor: Colors.red,
     indicatorSize: TabBarIndicatorSize.label,
     unselectedLabelColor: Colors.white,
     unselectedLabelStyle: TextStyle(fontSize: 18),
     labelColor: Colors.red,
     labelStyle: TextStyle(fontSize: 20),
     tabs: tabBarList),
   ),
   Expanded(
    child: TabBarView(
    children: tabBarViewList,
    // physics: NeverScrollableScrollPhysics(), // 禁止滑动
   ))
   ],
  ));
 }
}

其中推荐页、VIP页、小说页、直播页的结构仍和之前的首页结构相同,仅显示一个计数器和一个加号按钮,以推荐页recommend_page.dart为例:

/// recommend_page.dart
import 'package:flutter/material.dart';

class RecommendPage extends StatefulWidget {
 @override
 _RecommendPageState createState() => _RecommendPageState();
}

class _RecommendPageState extends State<RecommendPage> {
 int count = 0;

 void add() {
 setState(() {
  count++;
 });
 }

 @override
 void initState() {
 super.initState();
 print('recommend initState');
 }

 @override
 Widget build(BuildContext context) {
 return Scaffold(
  body:Center(
   child: Text('首页推荐: $count', style: TextStyle(fontSize: 30))
  ),
  floatingActionButton: FloatingActionButton(
   onPressed: add,
   child: Icon(Icons.add),
  ));
 }
}

保存后测试,

可以看到,现在添加了首页顶部导航,且默认支持左右侧滑,接下来再进一步的完善状态保持

第四步:实现首页顶部导航切换时保持原页面状态

③ 使用AutomaticKeepAliveClientMixin实现

写到这里已经很简单了,我们只需要在首页导航内需要保持页面状态的子页State中,继承AutomaticKeepAliveClientMixin并重写wantKeepAlive为true即可。

notes:Subclasses must implement wantKeepAlive, and their build methods must call super.build (the return value will always return null, and should be ignored)

以首页推荐recommend_page.dart为例:

/// recommend_page.dart
import 'package:flutter/material.dart';

class RecommendPage extends StatefulWidget {
 @override
 _RecommendPageState createState() => _RecommendPageState();
}

class _RecommendPageState extends State<RecommendPage>
 with AutomaticKeepAliveClientMixin {
 int count = 0;

 void add() {
 setState(() {
  count++;
 });
 }

 @override
 bool get wantKeepAlive => true;

 @override
 void initState() {
 super.initState();
 print('recommend initState');
 }

 @override
 Widget build(BuildContext context) {
 super.build(context);
 return Scaffold(
  body:Center(
   child: Text('首页推荐: $count', style: TextStyle(fontSize: 30))
  ),
  floatingActionButton: FloatingActionButton(
   onPressed: add,
   child: Icon(Icons.add),
  ));
 }
}

再次保存测试,

现在已经可以看到,不管是切换底部导航还是切换首页顶部导航,所有的页面状态都可以被保持,并且在应用第一次加载时,终端只看到recommend initState的日志,第一次切换首页顶部导航至vip页面时,终端输出vip initState,当再次返回推荐页时,不再输出recommend initState。

所以,使用TabBarView+AutomaticKeepAliveClientMixin这种方式既实现了页面状态的保持,又具有类似惰性求值的功能,对于未使用的页面状态不会进行实例化,减小了应用初始化时的开销。

更新

前面在底部导航介绍了使用IndexedStack和Offstage两种方式实现保持页面状态,但它们的缺点在于第一次加载时便实例化了所有的子页面State。为了进一步优化,下面我们使用PageView+AutomaticKeepAliveClientMixin重写之前的底部导航,其中PageView和TabBarView的实现原理类似,具体选择哪一个并没有强制要求。更新后的home.dart文件如下:

/// home.dart
import 'package:flutter/material.dart';

import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 final items = [
 BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首页')),
 BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('听')),
 BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
 ];

 final bodyList = [FirstPage(), SecondPage(), ThirdPage()];

 final pageController = PageController();

 int currentIndex = 0;

 void onTap(int index) {
 pageController.jumpToPage(index);
 }

 void onPageChanged(int index) {
 setState(() {
  currentIndex = index;
 });
 }

 @override
 Widget build(BuildContext context) {
 return Scaffold(
  bottomNavigationBar: BottomNavigationBar(
   items: items, currentIndex: currentIndex, onTap: onTap),
  // body: bodyList[currentIndex],
  body: PageView(
   controller: pageController,
   onPageChanged: onPageChanged,
   children: bodyList,
   physics: NeverScrollableScrollPhysics(), // 禁止滑动
  ));
 }
}

然后在bodyList的子页State中继承AutomaticKeepAliveClientMixin并重写wantKeepAlive,以second_page.dart为例:

/// second_page.dart
import 'package:flutter/material.dart';

class SecondPage extends StatefulWidget {
 @override
 _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage>
 with AutomaticKeepAliveClientMixin {
 int count = 0;

 void add() {
 setState(() {
  count++;
 });
 }

 @override
 bool get wantKeepAlive => true;

 @override
 void initState() {
 super.initState();
 print('second initState');
 }

 @override
 Widget build(BuildContext context) {
 super.build(context);
 return Scaffold(
  body: Center(
   child: Text('Second: $count', style: TextStyle(fontSize: 30))
  ),
  floatingActionButton: FloatingActionButton(
   onPressed: add,
   child: Icon(Icons.add),
  ));
 }
}

Ok,更新后保存运行,应用第一次加载时不会输出second initState,仅当第一次点击底部导航切换至该页时,该子页的State被实例化。

至此,如何实现一个类似的 底部 + 首页顶部导航 完结 ~

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Flutter质感设计之进度条

    LinearProgressIndicator控件是质感设计中的线性进度指示器,具体内容如下 import 'package:flutter/material.dart'; class ActionViewEcology extends StatelessWidget { /* * 构建函数,传递参数 * 最大能量值 * 最大饥饿值 * 最大情绪值 * 当前能量值 * 当前饥饿值 * 当前情绪值 */ ActionViewEcology({ this.maximumEmergy, this.ma

  • Flutter质感设计之弹出菜单

    PopupMenuButton控件即弹出菜单控件,点击控件会出现菜单. import 'package:flutter/material.dart'; class MenusDemo extends StatefulWidget { @override _MenusDemoState createState() => new _MenusDemoState(); } class _MenusDemoState extends State<MenusDemo> { String _body

  • Flutter进阶之实现动画效果(一)

    上一篇文章我们了解了Flutter的动画基础,这一篇文章我们就来实现一个图表的动画效果. 首先,我们需要创建一个新项目myapp,然后把main.dart的内容替换成下面的代码 import 'package:flutter/material.dart'; import 'dart:math'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { @override Widget build(Bui

  • flutter实现仿boss直聘功能

    Flutter是Google使用Dart语言开发的移动应用开发框架,使用一套Dart代码就能构建高性能.高保真的iOS和Android应用程序,并且在排版.图标.滚动.点击等方面实现零差异. 2年前,RN刚出来时做了个仿拉钩的demo,react-native-lagou. 这次flutter来了,想感受一下,索性用目前flutter的版本写的一个仿boss直聘应用. 时间有限,没完全仿照,去掉了一些功能,但是界面风格一致,有参考价值. github地址:flutter仿boss直聘. 感悟 与

  • Flutter实现底部导航栏效果

    大家最近都在讨论新鲜技术-flutter,小编也在学习中,遇到大家都遇到的问题,底部导航.下面给大家贴出底部导航的编写,主要参考了lime这个项目. 上代码 一.在main.dart文件中 定义APP的基本信息 class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new

  • Flutter中ListView 的使用示例

    这个小例子使用的是豆瓣 API 中 正在上映的电影 的开放接口,要实现的主要效果如下: JSON 数据结构 Item 结构 Item 的结构是一个 Card 包含着一个 Row 然后这个 Row 里面左边是一个 Image ,右边是一个 Column 功能实现 material 库 Json 解析 网络请求 加载菊花 要实现上面四个功能,我们首先需要在 .dart 文件中引入如下代码 import 'dart:convert'; import 'package:http/http.dart' a

  • Flutter启动流程的深入解析

    前言 Flutter是怎么启动起来的,是在Android的Activity的启动之后吗?等等这样的问题,在这个文章中将被解答. 从MainActivity开始 新创建一个Flutter项目,在清单文件中默认被启动的Activity是MainActivity,而MainActivity继承的是FlutterActivity.那么问题好像简单了,我们分析一下FlutterActivity,下面是MainActivity的代码. public class MainActivity extends Fl

  • Flutter启动页(闪屏页)的具体实现及原理详析

    为什么要有启动页? 在以下文章中,启动页就是闪屏页. 现在大部分App都有启动页,那么为什么要有启动页?这是个值得思考的问题,如果没有启动页会怎样,大部分的App会白屏(也有可能是黑屏,主题设置有关系)非常短的时间,然后才能展示App的内容. 那么问题来了,一定要有启动页吗?答案:不是,而且是尽可能不要有启动页,因为启动页会让用户体验不够连贯,甚至IOS在开发手册上就不推荐使用启动页. 我们深入思考一下,既然不推荐为什么这样流行,答案非常简单,启动页的成本非常低,如果你想把的App启动优化到一个

  • Flutter路由的跳转、动画和传参详解(最简单)

    路由 做Android/iOS原生开发的时候,要打开一个新的页面,你得知道你的目标页面对象,然后初始化一个Intent或者ViewController,再通过startActivity或者pushViewController来推出一个新的页面,不能跟web一样,直接丢一个链接地址就跳转到新的页面.当然,可以自己去加一个中间层来实现这些功能. Flutter里面是原生支持路由的.Flutter的framework提供了路由跳转的实现.我们可以直接使用这些功能. Flutter路由介绍 Flutte

  • Flutter Android端启动白屏问题的解决

    问题描述 Flutter 应用在 Android 端上启动时会有一段很明显的白屏现象,白屏的时长由设备的性能决定,设备性能越差,白屏时间越长. 问题分析 其实启动白屏的问题在Android原生应用上也是一个常见问题,大致是因为从用户点击 Launcher Icon 到应用首页显示之间,Android 系统在完成应用的初始化工作,其流程如下: 在 Flutter Android 端上,白屏的问题会更加严重,因为除了 Android 应用启动耗时外,还增加了 Flutter 初始化耗时. 直到 Fl

随机推荐