Flutter 三種方式實(shí)現(xiàn)頁(yè)面切換后保持原頁(yè)面狀態(tài)

前言:

在 Flutter 應(yīng)用中呢铆,導(dǎo)航欄切換頁(yè)面后默認(rèn)情況下會(huì)丟失原頁(yè)面狀態(tài)颜凯,即每次進(jìn)入頁(yè)面時(shí)都會(huì)重新初始化狀態(tài),如果在initState中打印日志暑竟,會(huì)發(fā)現(xiàn)每次進(jìn)入時(shí)都會(huì)輸出斋射,顯然這樣增加了額外的開(kāi)銷(xiāo),并且?guī)?lái)了不好的用戶體驗(yàn)但荤。

在正文之前罗岖,先看一些常見(jiàn)的 App 導(dǎo)航,以喜馬拉雅 FM 為例:

image

它擁有一個(gè)固定的底部導(dǎo)航以及首頁(yè)的頂部導(dǎo)航纱兑,可以看到不管是點(diǎn)擊底部導(dǎo)航切換頁(yè)面還是在首頁(yè)左右側(cè)滑切換頁(yè)面呀闻,之前的頁(yè)面狀態(tài)都是始終維持的,下面就具體介紹下如何在 flutter 中實(shí)現(xiàn)類(lèi)似喜馬拉雅的導(dǎo)航效果

第一步:實(shí)現(xiàn)固定的底部導(dǎo)航

在通過(guò)flutter create生成的項(xiàng)目模板中,我們先簡(jiǎn)化一下代碼潜慎,將MyHomePage提取到一個(gè)單獨(dú)的home.dart文件捡多,并在Scaffold腳手架中添加bottomNavigationBar底部導(dǎo)航,在body中展示當(dāng)前選中的子頁(yè)面铐炫。

/// 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('首頁(yè)')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('聽(tīng)')),
    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]
    );
  }
}

其中的三個(gè)子頁(yè)面結(jié)構(gòu)相同垒手,均顯示一個(gè)計(jì)數(shù)器和一個(gè)加號(hào)按鈕,以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),
        )
    );
  }
}

當(dāng)前效果如下:

image

可以看到倒信,從第二頁(yè)切換回第一頁(yè)時(shí)科贬,第一頁(yè)的狀態(tài)已經(jīng)丟失

第二步:實(shí)現(xiàn)底部導(dǎo)航切換時(shí)保持原頁(yè)面狀態(tài)

可能有些小伙伴在搜索后會(huì)開(kāi)始直接使用官方推薦的AutomaticKeepAliveClientMixin,通過(guò)在子頁(yè)面的 State 類(lèi)重寫(xiě)wantKeepAlivetrue 鳖悠。 然而榜掌,如果你的代碼和我上面的類(lèi)似,body 中并沒(méi)有使用PageViewTabBarView乘综,很不幸的告訴你憎账,踩到坑了,這樣是無(wú)效的卡辰,原因后面再詳述“澹現(xiàn)在我們先來(lái)介紹另外兩種方式:

① 使用IndexedStack實(shí)現(xiàn)

IndexedStack繼承自Stack,它的作用是顯示第index個(gè)child九妈,其它child在頁(yè)面上是不可見(jiàn)的反砌,但所有child的狀態(tài)都被保持,所以這個(gè)Widget可以實(shí)現(xiàn)我們的需求萌朱,我們只需要將現(xiàn)在的bodyIndexedStack包裹一層即可

/// 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,
        ));
  }

保存后再次測(cè)試一下

image

② 使用Offstage實(shí)現(xiàn)

Offstage的作用十分簡(jiǎn)單宴树,通過(guò)一個(gè)參數(shù)來(lái)控制child是否顯示,所以我們同樣可以組合使用Offstage來(lái)實(shí)現(xiàn)該需求嚷兔,其實(shí)現(xiàn)原理與IndexedStack類(lèi)似

/// 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],
            ),
          ],
        ));
  }
}

在上面的兩種方式中都可以實(shí)現(xiàn)保持原頁(yè)面狀態(tài)的需求森渐,但這里有一些開(kāi)銷(xiāo)上的問(wèn)題做入,有經(jīng)驗(yàn)的小伙伴應(yīng)該能發(fā)現(xiàn)當(dāng)應(yīng)用第一次加載的時(shí)候,所有子頁(yè)狀態(tài)都被實(shí)例化了(>這里的細(xì)節(jié)并不是因?yàn)槲抑苯影炎禹?yè)實(shí)例化放在bodyList里...<)同衣,如果在子頁(yè)StateinitState中打印日志竟块,可以在終端看到一次性輸出了所有子頁(yè)的日志。下面就介紹另一種通過(guò)繼承AutomaticKeepAliveClientMixin的方式來(lái)更好的實(shí)現(xiàn)保持狀態(tài)耐齐。

第三步:實(shí)現(xiàn)首頁(yè)的頂部導(dǎo)航

首先我們通過(guò)配合使用TabBar+TabBarView+AutomaticKeepAliveClientMixin來(lái)實(shí)現(xiàn)頂部導(dǎo)航(注意:TabBarTabBarView需要提供controller浪秘,如果自己沒(méi)有定義,則必須使用DefaultTabController包裹)埠况。此處也可以選擇使用PageView耸携,后面會(huì)介紹。

我們先在home.dart文件移除Scaffold腳手架中的appBar頂部工具欄辕翰,然后開(kāi)始重寫(xiě)首頁(yè)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('小說(shuō)'), 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(), // 禁止滑動(dòng)
            ))
          ],
        ));
  }
}

其中推薦頁(yè)夺衍、VIP 頁(yè)、小說(shuō)頁(yè)喜命、直播頁(yè)的結(jié)構(gòu)仍和之前的首頁(yè)結(jié)構(gòu)相同沟沙,僅顯示一個(gè)計(jì)數(shù)器和一個(gè)加號(hào)按鈕,以推薦頁(yè)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('首頁(yè)推薦: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        ));
  }
}

保存后測(cè)試壁榕,

image

可以看到矛紫,現(xiàn)在添加了首頁(yè)頂部導(dǎo)航,且默認(rèn)支持左右側(cè)滑牌里,接下來(lái)再進(jìn)一步的完善狀態(tài)保持

第四步:實(shí)現(xiàn)首頁(yè)頂部導(dǎo)航切換時(shí)保持原頁(yè)面狀態(tài)

③ 使用AutomaticKeepAliveClientMixin實(shí)現(xiàn)

寫(xiě)到這里已經(jīng)很簡(jiǎn)單了颊咬,我們只需要在首頁(yè)導(dǎo)航內(nèi)需要保持頁(yè)面狀態(tài)的子頁(yè)State中,繼承AutomaticKeepAliveClientMixin并重寫(xiě)wantKeepAlivetrue即可牡辽。

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

以首頁(yè)推薦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('首頁(yè)推薦: $count', style: TextStyle(fontSize: 30))
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: add,
          child: Icon(Icons.add),
        ));
  }
}

再次保存測(cè)試喳篇,

image

現(xiàn)在已經(jīng)可以看到,不管是切換底部導(dǎo)航還是切換首頁(yè)頂部導(dǎo)航态辛,所有的頁(yè)面狀態(tài)都可以被保持杭隙,并且在應(yīng)用第一次加載時(shí),終端只看到recommend initState的日志因妙,第一次切換首頁(yè)頂部導(dǎo)航至 vip 頁(yè)面時(shí),終端輸出vip initState票髓,當(dāng)再次返回推薦頁(yè)時(shí)攀涵,不再輸出recommend initState

所以洽沟,使用TabBarView+AutomaticKeepAliveClientMixin這種方式既實(shí)現(xiàn)了頁(yè)面狀態(tài)的保持以故,又具有類(lèi)似惰性求值的功能,對(duì)于未使用的頁(yè)面狀態(tài)不會(huì)進(jìn)行實(shí)例化裆操,減小了應(yīng)用初始化時(shí)的開(kāi)銷(xiāo)怒详。

更新

前面在底部導(dǎo)航介紹了使用IndexedStackOffstage兩種方式實(shí)現(xiàn)保持頁(yè)面狀態(tài)炉媒,但它們的缺點(diǎn)在于第一次加載時(shí)便實(shí)例化了所有的子頁(yè)面State。為了進(jìn)一步優(yōu)化昆烁,下面我們使用PageView+AutomaticKeepAliveClientMixin重寫(xiě)之前的底部導(dǎo)航吊骤,其中PageViewTabBarView的實(shí)現(xiàn)原理類(lèi)似,具體選擇哪一個(gè)并沒(méi)有強(qiáng)制要求静尼。更新后的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('首頁(yè)')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('聽(tīng)')),
    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(), // 禁止滑動(dòng)
        ));
  }
}

然后在bodyList的子頁(yè)State中繼承AutomaticKeepAliveClientMixin并重寫(xiě)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,更新后保存運(yùn)行鼠渺,應(yīng)用第一次加載時(shí)不會(huì)輸出second initState鸭巴,僅當(dāng)?shù)谝淮吸c(diǎn)擊底部導(dǎo)航切換至該頁(yè)時(shí),該子頁(yè)的State被實(shí)例化拦盹。

至此鹃祖,如何實(shí)現(xiàn)一個(gè)類(lèi)似的 底部 + 首頁(yè)頂部導(dǎo)航 完結(jié) ~

轉(zhuǎn)發(fā)自 弧度里的微笑

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市普舆,隨后出現(xiàn)的幾起案子恬口,更是在濱河造成了極大的恐慌,老刑警劉巖奔害,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楷兽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡华临,警方通過(guò)查閱死者的電腦和手機(jī)芯杀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雅潭,“玉大人揭厚,你說(shuō)我怎么就攤上這事》龉” “怎么了筛圆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)椿浓。 經(jīng)常有香客問(wèn)我太援,道長(zhǎng),這世上最難降的妖魔是什么扳碍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任提岔,我火速辦了婚禮,結(jié)果婚禮上笋敞,老公的妹妹穿的比我還像新娘碱蒙。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布赛惩。 她就那樣靜靜地躺著哀墓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喷兼。 梳的紋絲不亂的頭發(fā)上篮绰,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音褒搔,去河邊找鬼阶牍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛星瘾,可吹牛的內(nèi)容都是我干的走孽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼琳状,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼磕瓷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起念逞,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤困食,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后翎承,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體硕盹,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年叨咖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘩例。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甸各,死狀恐怖垛贤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情趣倾,我是刑警寧澤聘惦,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站儒恋,受9級(jí)特大地震影響善绎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诫尽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一涂邀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箱锐,春花似錦、人聲如沸劳较。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至臊恋,卻和暖如春衣洁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抖仅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工坊夫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撤卢。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓环凿,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親放吩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子智听,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355