Flutter сохраняет исходное состояние страницы после переключения страниц тремя способами.

Flutter

Предисловие:

В приложениях Flutter состояние исходной страницы по умолчанию будет утеряно после переключения страниц панелью навигации, то есть состояние будет повторно инициализироваться каждый раз при входе на страницу.initStateЕсли вы распечатаете журнал, вы обнаружите, что он будет выводиться каждый раз, когда вы входите, что, очевидно, добавляет дополнительные накладные расходы и создает плохой пользовательский опыт.

Прежде чем приступить к тексту, давайте рассмотрим некоторые распространенные способы навигации в приложении на примере Himalaya FM:

0.gif

Он имеет фиксированную нижнюю навигацию и верхнюю навигацию домашней страницы.Вы можете видеть, что независимо от того, щелкаете ли вы нижнюю навигацию для переключения страниц или проводите влево и вправо на домашней странице для переключения страниц, предыдущий статус страницы всегда сохраняется. гималайский навигационный эффект в

Шаг 1. Внедрите фиксированную нижнюю навигацию.

проходя через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),
        )
    );
  }
}

Текущий эффект выглядит следующим образом:

1.gif

Видно, что при переходе со второй страницы обратно на первую страницу состояние первой страницы сбилось

Шаг 2. Сохраните исходное состояние страницы при реализации нижнего навигационного переключателя

Возможно, некоторые друзья начнут пользоваться официальной рекомендацией сразу после поиска.AutomaticKeepAliveClientMixin, переопределив класс State на дочерней страницеwantKeepAliveдляtrue. Однако, если ваш код похож на мой выше, тело не использует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,
        ));
  }

протестировать снова после сохранения

2.gif

② использовать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способ лучше достичь сохранения состояния.

Шаг 3. Реализуйте верхнюю навигацию на главной странице

Во-первых, мы используем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) {
    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(), // 禁止滑动
            ))
          ],
        ));
  }
}

Структура рекомендуемой страницы, 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),
        ));
  }
}

тест после сохранения,

3.gif
Видно, что теперь добавлена ​​верхняя навигация домашней страницы, а левый и правый слайды поддерживаются по умолчанию, а дальше состояние будет улучшаться.

Шаг 4. Сохраняйте исходное состояние страницы при переключении верхней навигации на главной странице.

③ Использование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),
        ));
  }
}


Сохраните тест еще раз,

4.gif

Теперь видно, что независимо от того, переключается ли нижняя навигация или переключается на верхнюю навигацию домашней страницы, все состояния страницы могут сохраняться, и когда приложение загружается в первый раз, терминал видит толькоrecommend initStateЖурнал первого перехода в верхнюю часть домашней страницы и перехода на вип-страницу, терминал выводит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) {
    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(), // 禁止滑动
        ));
  }
}

затем вbodyListподстраницыStateНаследованиеAutomaticKeepAliveClientMixinи переписатьwantKeepAlivesecond_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),
        ));
  }
}

Ок, сохраняем и запускаем после обновления, приложение не выведется при первой загрузкеsecond initState, только при первом щелчке нижней навигации для переключения на страницу подстраницаStateсоздается экземпляр.

Итак, как реализовать аналогичную навигацию внизу + на главной странице. Конец ~