Preface:

In the Flutter application, the original page state will be lost by default after the navigation bar switches pages, that is, the page state will be reinitialized every time the page is entered. If the log is printed in initState, it will be printed every time the page is entered. This obviously adds extra overhead and creates a bad user experience.

Before the main text, let’s take a look at some common App navigation, taking Ximalaya FM as an example:

It has a fixed bottom navigation and the top navigation of the home page. You can see that the previous page state is always the same whether you click on the bottom navigation to switch pages or swipe left and right to switch pages on the home page. Here is how to achieve the Himalayan navigation effect in flutter

Step 1: Implement fixed bottom navigation

In the project template generated by flutter create, we first simplified the code by extracting MyHomePage into a single home.dart file and adding bottomNavigationBar navigation to the Scaffold Scaffold. Display the currently selected subpage in the 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('home')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('listen')),
    BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('message'))];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] ); }}Copy the code

Three of the child pages have the same structure, showing a counter and a plus button, using first_page.dart as an example:

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

The current effect is as follows:

As you can see, the state of the first page has been lost when you switch back from the second page

Step 2: Keep the original page state when switching the bottom navigation

There may be some friend after the search will begin using official recommended AutomaticKeepAliveClientMixin directly, through the State class in child pages rewrite wantKeepAlive to true. However, if your code is similar to mine and you don’t use PageView or TabBarView in your body, unfortunately, it’s not working, for reasons I’ll discuss later. Now let’s introduce the other two ways:

(1) usingIndexedStackimplementation

The IndexedStack is a descendant of the Stack. It displays the index child. The other children are not visible on the page, but the state of all children is kept. We just need to wrap the current body layer in 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,
        ));
  }
Copy the code

Save and test again

(2) useOffstageimplementation

The function of Offstage is very simple, with a single parameter controlling whether or not a child is displayed, so we can also use Offstage in combination to implement this requirement, similar to 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],),])); }}Copy the code

In both cases, you can maintain the original page state, but there are some overhead issues. Experienced users should notice that when the application is first loaded, all the child page states are instantiated. <), if the log is printed in initState of the child page State, the terminal can see that the log is printed for all the child pages at one time. The following will introduce another through inheritance AutomaticKeepAliveClientMixin way to better implementation status.

Step 3: Implement navigation at the top of the home page

First of all we done by using TabBar + + AutomaticKeepAliveClientMixin TabBarView top navigation (note: TabBar and TabBarView need to provide controllers, if they are not defined, they must be wrapped with DefaultTabController. You can also choose to use PageView here, as described below.

Dart we removed the Scaffold appBar top toolbar in the home.dart file and started rewriting the first page 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('recommendations'), body: RecommendPage()),
  _TabData(tab: Text('VIP'), body: VipPage()),
  _TabData(tab: Text('novel'), body: NovelPage()),
  _TabData(tab: Text('live'), 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) {
    super.build(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 (), / / slide is prohibited)))); }}Copy the code

The recommend_page, VIP page, novels page and webcast page have the same structure as the previous homepage, showing only a counter and a plus button.

/// 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 = $count, style: TextStyle(fontSize: 30)) ), floatingActionButton: FloatingActionButton( onPressed: add, child: Icon(Icons.add), )); }}Copy the code

Save and test,

Step 4: Keep the original page state when switching the navigation at the top of the home page

(3) usingAutomaticKeepAliveClientMixinimplementation

Write here already very simple, we only need the front page navigation within the child needs to maintain the State of the page page State, inheritance AutomaticKeepAliveClientMixin rewrite wantKeepAlive to 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)

Shame_page.dart is recommended for shame_recommend_page.

/// 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 = $count, style: TextStyle(fontSize: 30)) ), floatingActionButton: FloatingActionButton( onPressed: add, child: Icon(Icons.add), )); }}Copy the code

Save the test again,

Now you can see that all page states are maintained, whether you switch the bottom navigation or the top navigation of the home page, and when the application is first loaded, the terminal only sees the log of the Recommended initState. When you switch the top navigation of the home page to the VIP page, the terminal outputs the VIP initState. When you return to the recommendation page again, you no longer output the Recommend initState.

So, using TabBarView + AutomaticKeepAliveClientMixin this way not only to achieve the maintain the state of the page, and have similar inert evaluation function, will no longer be instantiated for unused pages, reduced the cost of application initialization time.

update

The previous navigation at the bottom described the use of IndexedStack and Offstage to maintain page State, but they have the disadvantage of instantiating all child page States on the first load. In order to further optimize, we use the below PageView + AutomaticKeepAliveClientMixin rewrite before the bottom of the navigation, the PageView and TabBarView similar to the implementation of the principle, the specific choice which one is not mandatory. The updated home.dart file looks like this:

/// 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('home')),
    BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('listen')),
    BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('message'))];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) {
    super.build(context);
    return Scaffold(
        bottomNavigationBar: BottomNavigationBar(
            items: items, currentIndex: currentIndex, onTap: onTap),
        // body: bodyList[currentIndex],
        body: PageView(
          controller: pageController,
          onPageChanged: onPageChanged,
          children: bodyList,
          physics: NeverScrollableScrollPhysics(), // Disable sliding)); }}Copy the code

Then in the child page State of bodyList inheritance AutomaticKeepAliveClientMixin rewrite wantKeepAlive, with second_page. Dart, for example:

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

Second initState is not printed when the application is first loaded. The State of the subpage is instantiated only when the bottom navigation is clicked for the first time to switch to the page.

So, how to achieve a similar bottom + home page top navigation end ~