Flutter bottomNavigator с индексированным стеком

Вопрос относительно перехода между вкладками с использованием индексированного стека для отображения соответствующей страницы. Я делаю это для того, чтобы сохранить прокрутку / состояние страниц. Это прекрасно работает. Я могу изменить текущую отображаемую страницу, щелкнув вкладку, а также могу перемещаться внутри каждой страницы (каждая страница имеет собственный навигатор). Это код для рендеринга страниц.

Widget build(BuildContext context) {
return IndexedStack(
    index: widget.selectedIndex,
    children: List.generate(widget._size, (index) {
  return _buildNavigator(index);
}));

}

Проблема Mu в том, что IndexedStack строит сразу все страницы. На некоторых из моих страниц я хочу загружать данные из API, я хочу сделать это при первом построении виджета и только в том случае, если страница в данный момент видна. Как это сделать? в моей текущей реализации все виджеты создаются одновременно, поэтому все мои вызовы API вызываются даже для страниц, которые в данный момент не отрисованы.

Не уверен, что мне что-то здесь не хватает, или есть лучший способ реализовать нижнюю панель навигации. Кстати, я также использую Provider для управления состоянием.

6 ответов

Я столкнулся с той же проблемой. Мое решение состояло в том, чтобы сохранить список загруженных вкладок, а затем использовать его для создания списка IndexedStack дети внутри Widget build(BuildContext context)метод. Тогда в onTap метод BottomNavigationBar, Я звонил setState()чтобы обновить список загруженных вкладок, а также текущую индексную переменную. См. ниже:

      class Index extends StatefulWidget {
  const Index({Key? key}) : super(key: key);

  @override
  _IndexState createState() => _IndexState();
}

class _IndexState extends State<Index> {
  int _currentIndex = 0;
  List loadedPages = [0,];

  @override
  Widget build(BuildContext context) {
    var screens = [
      const FirstTab(),
      loadedPages.contains(1) ? const SecondTab() : Container(),
      loadedPages.contains(2) ? const ThirdTab() : Container(),
      loadedPages.contains(3) ? const FourthTab() : Container(),
    ];
    return Scaffold(
      appBar: AppBar(
        // AppBar
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: screens,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          var pages = loadedPages;
          if (!pages.contains(index)) {
            pages.add(index);
          }
          setState(() {
            _currentIndex = index;
            loadedPages = pages;
          });
        },
        items: const [
          // items
        ],
      ),
    );
  }
}

Теперь вызовы API на второй, третьей и четвертой вкладках не вызываются, пока не будет выполнен переход к.

Используйте PageView вместо IndexedStack

       PageView(
            physics: const NeverScrollableScrollPhysics(),
            controller: _pageController,
            children: const [
              Page1(),
              Page2(),
              Page3(),
            ],
          ),

вы можете переключать страницы с помощью pageController

      _pageController.jumpToPage(0);

@tsahnar, да, я также столкнулся с той же проблемой, что и индексированный виджет с вызовом api, отображающий все виджеты, предоставленные его дочерним элементам, сразу, поэтому, когда отдельные страницы независимо извлекают данные из api, тогда возникает проблема

попробуй это :

  • создать список виджетов, которые перемещаются по вашей панели навигации (каждый виджет с ключевым конструктором, где определить PageStorageKey(<key>) для каждого виджета)
      var widgetList = <Widget>[
Page01(key:PageStorageKey(<key>)),
Page02(key:PageStorageKey(<key>))
];
  • затем создайте PageStorageBucket() который хранит состояние ваших виджетов и предоставляет его в будущем всякий раз, когда он нам понадобится в течение жизни приложения, даже если виджет удаляется из дерева

final _bucket = PageStorageBucket();

  • потом var currentIndex = 0;

  • затем на вашей основной базовой странице, где нижняя панель навигации существует в вашем теле вместо IndexedStack использовать body:PageStorage(bucket: _bucket,child:widgetsList[currentIndex])

и создайте нижнюю панель навигации на этой главной базовой странице, а затем на вкладке значка панели навигации управляйте предварительным состоянием индексной страницы с помощью setState((){}) текущее состояние к currentIndex

он должен решить вашу проблему, но через год будет уже слишком поздно

ты нашел решение?
Я обнаружил ту же проблему, что и вы, и я попробовал этот обходной путь (я еще не обнаружил с ним никаких проблем)
. Идея состоит в том, чтобы создать новый виджет для управления состоянием видимости виджетов, которые вызывали api, и создавать его, когда он стало видно.
В вашей IndexedStack заверните свой _buildNavigator с таким виджетом:

      class BaseTabPage extends StatefulWidget {

  final bool isVisible;
  final Widget child;

  BaseTabPage({Key key, this.child, this.isVisible});

  @override
  State<StatefulWidget> createState() => _BaseTabPageState();

}

/*
  This state is to prevent tab pages creation before show them. It'll only add the
  child widget to the widget tree when isVisible is true at least one time
  i.e. if the child widget makes an api call, it'll only do when isVisible is true
  for the first time
 */
class _BaseTabPageState extends State<BaseTabPage> {

  bool alreadyShowed = false;

  @override
  Widget build(BuildContext context) {
    alreadyShowed = widget.isVisible ? true : alreadyShowed;

    return alreadyShowed ? widget.child : Container();
  }
}

Например, в моем коде у меня есть что-то вроде этого для создания навигаторов для каждой вкладки, где _selectedIndex это выбранное положение и tabPosition позиция этой страницы в BottomNavigationBar

      Widget _buildTabPage(int tabPosition) {
    final visibility = _selectedIndex == tabPosition;
    return BaseTabPage(
      isVisible: visibility,
      child: _buildNavigator(tabPosition),
    );
  }

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

Если мы представим, что страница 2 вашего IndexedStack - это страница, на которой вам нужны данные вызова API...

Чтобы вызвать API только на странице 2, выполните вызов API в обработчике onTap BottomNavigationBar:

      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        items: []
        onTap: (index) {
          if (index == 2 && !_apiCallCompleted) {
            makeApiCall();
          }
          setState(() {
            _currentIndex = index;
          });
        },
      ),

Здесь обработчик onTap ищет индекс "2" и то, что вы еще не выполнили вызов API.

Имейте логическое значение внутри вашего метода вызова API, установив _apiCallCompleted = true.


В ответ на ваш комментарий о желании сохранить вызов API внутри виджета, вы можете передать индекс IndexedStack каждому виджету.

Каждый виджет будет проверять, является ли он "активным" / видимым виджетом, используя предоставленный индекс. Если active/visible и apiCallDone == false, выполните вызов API.

Изменить. В предыдущей версии второго предложения я помещал вызов API вbuild() случайно, чего никогда не следует делать, так как его можно вызывать до 60 раз в секунду. initState()это место, где должна происходить инициализация / доступ к данным, если таковой имеется. Поскольку была логическая проверка, чтобы увидеть, был ли уже выполнен вызов API, это не было бы катастрофой, но в любом случае это не лучшая практика.

        child: IndexedStack(
          index: _currentIndex,
          children: [
            WidgetOne(_currentIndex), 
            WidgetTwo(_currentIndex)
          ],
        ),
class WidgetOne extends StatefulWidget {
  final int index;

  WidgetOne(this.index);

  @override
  _WidgetOneState createState() => _WidgetOneState();
}

class _WidgetOneState extends State<WidgetOne> {
  bool apiCallDone = false;

  @override
  void initState() {
    super.initState();
    if (widget.index == 1) {
      // todo - make API call
      apiCallDone = true;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    // todo - display results of API call
  }
}

class WidgetTwo extends StatefulWidget {
  final int index;

  WidgetTwo(this.index);

  @override
  _WidgetTwoState createState() => _WidgetTwoState();
}

class _WidgetTwoState extends State<WidgetTwo> {
  bool apiCallDone = false;

  @override
  void initState() {
    super.initState();
    if (widget.index == 2) {
      // todo - make API call
      apiCallDone = true;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    // todo - display results of API call
  }
}

Flutter будет создавать все виджеты внутри стека, поэтому, если ваши страницы выполняют вызов API внутри initState, он будет запускаться при сборке.

Что вы можете сделать, так это создать отдельную функцию для вызова API. Затем вызовите функцию из навигатора или из управления состоянием.

Надеюсь, это поможет вам понять, как это реализовать.

Другие вопросы по тегам