Flutter: BottomNavigationBar перестраивает страницу при изменении вкладки

У меня проблема с моим BottomNavigationBar в Flutter. Я хочу сохранить свою страницу, если я изменю вкладки.

здесь моя реализация

BottomNavigation

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _HomeState();
  }
}

class _HomeState extends State<Home> {
  int _currentIndex = 0;
  List<Widget> _children;
  final Key keyOne = PageStorageKey("IndexTabWidget");

  @override
  void initState() {
    _children = [
      IndexTabWidget(key: keyOne),
      PlaceholderWidget(Colors.green),
      NewsListWidget(),
      ShopsTabWidget(),
      PlaceholderWidget(Colors.blue),
    ];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(MyApp.appName),
        textTheme: Theme.of(context).textTheme.apply(
              bodyColor: Colors.black,
              displayColor: Colors.blue,
            ),
      ),
      body: _children[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        onTap: onTabTapped,
        key: IHGApp.globalKey,
        fixedColor: Colors.green,
        type: BottomNavigationBarType.fixed,
        currentIndex: _currentIndex,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Container(height: 0.0),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            title: Container(height: 0.0),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            title: Container(height: 0.0),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.perm_contact_calendar),
            title: Container(height: 0.0),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            title: Container(height: 0.0),
          ),
        ],
      ),
    );
  }

  void onTabTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  Column buildButtonColumn(IconData icon) {
    Color color = Theme.of(context).primaryColor;

    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
      ],
    );
  }
}

Это моя индексная страница (первая вкладка):

class IndexTabWidget extends StatefulWidget {
  IndexTabWidget({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return new IndexTabState();
  }
}

class IndexTabState extends State<IndexTabWidget>
    with AutomaticKeepAliveClientMixin {
  List<News> news = List();
  FirestoreNewsRepo newsFirestore = FirestoreNewsRepo();

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.white,
      child: new Container(
        child: new SingleChildScrollView(
          child: new ConstrainedBox(
            constraints: new BoxConstraints(),
            child: new Column(
              children: <Widget>[
                HeaderWidget(
                  CachedNetworkImageProvider(
                    'https://static1.fashionbeans.com/wp-content/uploads/2018/04/50-barbershop-top-savill.jpg',
                  ),
                  "",
                ),
                AboutUsWidget(),
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: SectionTitleWidget(title: StringStorage.salonsTitle),
                ),
                StreamBuilder(
                  stream: newsFirestore.observeNews(),
                  builder: (context, snapshot) {
                    if (!snapshot.hasData) {
                      return CircularProgressIndicator();
                    } else {
                      news = snapshot.data;
                      return Column(
                        children: <Widget>[
                          ShopItemWidget(
                            AssetImage('assets/images/picture.png'),
                            news[0].title,
                            news[0],
                          ),
                          ShopItemWidget(
                            AssetImage('assets/images/picture1.png'),
                            news[1].title,
                            news[1],
                          )
                        ],
                      );
                    }
                  },
                ),
                Padding(
                  padding: const EdgeInsets.only(
                      left: 16.0, right: 16.0, bottom: 16.0),
                  child: SectionTitleWidget(title: StringStorage.galleryTitle),
                ),
                GalleryCategoryCarouselWidget(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

Поэтому, если я переключаюсь со своей вкладки индексов на любую другую вкладку и обратно на вкладку индекса, вкладка индекса всегда будет перестраиваться. Я отладил его и увидел, что функция сборки всегда вызывается с помощью переключателя табуляции.

Не могли бы вы, ребята, помочь мне решить эту проблему?

Большое спасибо Albo

Ответ 1

Ни один из предыдущих ответов не сработал для меня.

Решение, позволяющее поддерживать живость страниц при переключении вкладок, - это обернуть ваши страницы в IndexedStack.

class Tabbar extends StatefulWidget {
  Tabbar({this.screens});

  static const Tag = "Tabbar";
  final List<Widget> screens;
  @override
  State<StatefulWidget> createState() {
  return _TabbarState();
  }
}

class _TabbarState extends State<Tabbar> {
  int _currentIndex = 0;
  Widget currentScreen;

  @override
  Widget build(BuildContext context) {
    var _l10n = PackedLocalizations.of(context);

    return Scaffold(
  body: IndexedStack(
    index: _currentIndex,
    children: widget.screens,
  ),
  bottomNavigationBar: BottomNavigationBar(
    fixedColor: Colors.black,
    type: BottomNavigationBarType.fixed,
    onTap: onTabTapped,
    currentIndex: _currentIndex,
    items: [
      BottomNavigationBarItem(
        icon: new Icon(Icons.format_list_bulleted),
        title: new Text(_l10n.tripsTitle),
      ),
      BottomNavigationBarItem(
        icon: new Icon(Icons.settings),
        title: new Text(_l10n.settingsTitle),
      )
    ],
  ),
);
  }

  void onTabTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }
}

Ответ 2

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

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final int _pageCount = 2;
  int _pageIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _body(),
      bottomNavigationBar: _bottomNavigationBar(),
    );
  }

  Widget _body() {
    return Stack(
      children: List<Widget>.generate(_pageCount, (int index) {
        return IgnorePointer(
          ignoring: index != _pageIndex,
          child: Opacity(
            opacity: _pageIndex == index ? 1.0 : 0.0,
            child: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return new MaterialPageRoute(
                  builder: (_) => _page(index),
                  settings: settings,
                );
              },
            ),
          ),
        );
      }),
    );
  }

  Widget _page(int index) {
    switch (index) {
      case 0:
        return Page1();
      case 1:
        return Page2();
    }

    throw "Invalid index $index";
  }

  BottomNavigationBar _bottomNavigationBar() {
    final theme = Theme.of(context);
    return new BottomNavigationBar(
      fixedColor: theme.accentColor,
      currentIndex: _pageIndex,
      items: [
        BottomNavigationBarItem(
          icon: Icon(Icons.list),
          title: Text("Page 1"),
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.account_circle),
          title: Text("Page 2"),
        ),
      ],
      onTap: (int index) {
        setState(() {
          _pageIndex = index;
        });
      },
    );
  }
}

Страницы будут пересозданы, но вы должны отделить свою бизнес-логику от вашего интерфейса. Я предпочитаю использовать шаблон BLoC, но вы также можете использовать Redux, ScopedModel или InhertedWidget.

Ответ 3

Я не уверен, но CupertinoTabBar поможет.
Если вы этого не хотите, это видео будет хорошим URL.

import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => new _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final List<dynamic> pages = [
    new Page1(),
    new Page2(),
    new Page3(),
    new Page4(),
  ];

  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return new WillPopScope(
      onWillPop: () async {
        await Future<bool>.value(true);
      },
      child: new CupertinoTabScaffold(
        tabBar: new CupertinoTabBar(
          iconSize: 35.0,
          onTap: (index) {
            setState(() => currentIndex = index);
          },
          activeColor: currentIndex == 0 ? Colors.white : Colors.black,
          inactiveColor: currentIndex == 0 ? Colors.green : Colors.grey,
          backgroundColor: currentIndex == 0 ? Colors.black : Colors.white,
          currentIndex: currentIndex,
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.looks_one),
              title: Text(''),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.looks_two),
              title: Text(''),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.looks_3),
              title: Text(''),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.looks_4),
              title: Text(''),
            ),
          ],
        ),
        tabBuilder: (BuildContext context, int index) {
          return new DefaultTextStyle(
            style: const TextStyle(
              fontFamily: '.SF UI Text',
              fontSize: 17.0,
              color: CupertinoColors.black,
            ),
            child: new CupertinoTabView(
              routes: <String, WidgetBuilder>{
                '/Page1': (BuildContext context) => new Page1(),
                '/Page2': (BuildContext context) => new Page2(),
                '/Page3': (BuildContext context) => new Page3(),
                '/Page4': (BuildContext context) => new Page4(),
              },
              builder: (BuildContext context) {
                return pages[currentIndex];
              },
            ),
          );
        },
      ),
    );
  }
}

class Page1 extends StatefulWidget {
  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> {

  String title;

 @override
  void initState() {
    title = 'Page1';
    super.initState();
  }
   @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
          title: new Text(title),
          leading: new IconButton(
            icon: new Icon(Icons.text_fields),
            onPressed: () {
              Navigator.of(context)
                  .push(MaterialPageRoute(builder: (context) => Page13()));
            },
          )),
      body: new Center(
        child: new Text(title),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
            title: new Text('Page2'),
            leading: new IconButton(
              icon: new Icon(Icons.airline_seat_flat_angled),
              onPressed: () {
                Navigator.of(context)
                    .push(MaterialPageRoute(builder: (context) => Page12()));
              },
            )),
        body: new Center(
          child: Column(
            children: <Widget>[
              CupertinoSlider(
                      value: 25.0,
                      min: 0.0,
                      max: 100.0,
                      onChanged: (double value) {
                        print(value);
                      }
                    ),
            ],
          ),
        ),
    );
  }
}

class Page3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Page3'),
      ),
      body: new Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new RaisedButton(
                child: new Text('Cupertino'),
                textColor: Colors.white,
                color: Colors.red,
                onPressed: () {
                  List<int> list = List.generate(10, (int i) => i + 1);    
                  list.shuffle();
                  var subList = (list.sublist(0, 5));
                  print(subList);
                  subList.forEach((li) => list.remove(li));
                  print(list);
                }
            ),
            new SizedBox(height: 30.0),
            new RaisedButton(
                child: new Text('Android'),
                textColor: Colors.white,
                color: Colors.lightBlue,
                onPressed: () {
                  var mes = 'message';
                  var messa = 'メッセージ';
                  var input = 'You have a new message';
                  if (input.contains(messa) || input.contains(mes)) {
                    print('object');
                  } else {
                    print('none');
                  }
                }
            ),
          ],
        ),
      ),
    );
  }
}

class Page4 extends StatelessWidget {
  static List<int> ints = [1, 2, 3, 4, 5];

  static _abc() {
    print(ints.last);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Page4'),
      ),
      body: new Center(
          child: new RaisedButton(
        child: new Text('Static', style: new TextStyle(color: Colors.white)),
        color: Colors.lightBlue,
        onPressed: _abc,
      )),
    );
  }
}

class Page12 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Page12'),
        actions: <Widget>[
          new FlatButton(
            child: new Text('GO'),
            onPressed: () {
              Navigator.of(context)
                  .push(MaterialPageRoute(builder: (context) => Page13()));
            },
          )
        ],
      ),
      body: new Center(
          child: new RaisedButton(
        child: new Text('Swiper', style: new TextStyle(color: Colors.white)),
        color: Colors.redAccent,
        onPressed: () {},
      )),
    );
  }
}

class Page13 extends StatefulWidget {
  @override
  _Page13State createState() => _Page13State();
}

class _Page13State extends State<Page13> with SingleTickerProviderStateMixin {
 final List<String> _productLists = Platform.isAndroid
      ? [
          'android.test.purchased',
          'point_1000',
          '5000_point',
          'android.test.canceled',
        ]
      : ['com.cooni.point1000', 'com.cooni.point5000'];

  String _platformVersion = 'Unknown';
  List<IAPItem> _items = [];
  List<PurchasedItem> _purchases = [];

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion = await FlutterInappPurchase.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    var result = await FlutterInappPurchase.initConnection;
    print('result: $result');

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });

    // refresh items for android
    String msg = await FlutterInappPurchase.consumeAllItems;
    print('consumeAllItems: $msg');
  }

  Future<Null> _buyProduct(IAPItem item) async {
    try {
      PurchasedItem purchased = await FlutterInappPurchase.buyProduct(item.productId);
      print('purchased: ${purchased.toString()}');
    } catch (error) {
      print('$error');
    }
  }

  Future<Null> _getProduct() async {
    List<IAPItem> items = await FlutterInappPurchase.getProducts(_productLists);
    print(items);
    for (var item in items) {
      print('${item.toString()}');
      this._items.add(item);
    }

    setState(() {
      this._items = items;
      this._purchases = [];
    });
  }

  Future<Null> _getPurchases() async {
    List<PurchasedItem> items = await FlutterInappPurchase.getAvailablePurchases();
    for (var item in items) {
      print('${item.toString()}');
      this._purchases.add(item);
    }

    setState(() {
      this._items = [];
      this._purchases = items;
    });
  }

  Future<Null> _getPurchaseHistory() async {
    List<PurchasedItem> items = await FlutterInappPurchase.getPurchaseHistory();
    for (var item in items) {
      print('${item.toString()}');
      this._purchases.add(item);
    }

    setState(() {
      this._items = [];
      this._purchases = items;
    });
  }

  List<Widget> _renderInApps() {
    List<Widget> widgets = this
        ._items
        .map((item) => Container(
              margin: EdgeInsets.symmetric(vertical: 10.0),
              child: Container(
                child: Column(
                  children: <Widget>[
                    Container(
                      margin: EdgeInsets.only(bottom: 5.0),
                      child: Text(
                        item.toString(),
                        style: TextStyle(
                          fontSize: 18.0,
                          color: Colors.black,
                        ),
                      ),
                    ),
                    FlatButton(
                      color: Colors.orange,
                      onPressed: () {
                        print("---------- Buy Item Button Pressed");
                        this._buyProduct(item);
                      },
                      child: Row(
                        children: <Widget>[
                          Expanded(
                            child: Container(
                              height: 48.0,
                              alignment: Alignment(-1.0, 0.0),
                              child: Text('Buy Item'),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ))
        .toList();
    return widgets;
  }

  List<Widget> _renderPurchases() {
    List<Widget> widgets = this
        ._purchases
        .map((item) => Container(
              margin: EdgeInsets.symmetric(vertical: 10.0),
              child: Container(
                child: Column(
                  children: <Widget>[
                    Container(
                      margin: EdgeInsets.only(bottom: 5.0),
                      child: Text(
                        item.toString(),
                        style: TextStyle(
                          fontSize: 18.0,
                          color: Colors.black,
                        ),
                      ),
                    )
                  ],
                ),
              ),
            ))
        .toList();
    return widgets;
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width-20;
    double buttonWidth=(screenWidth/3)-20;

    return new Scaffold(
      appBar: new AppBar(),
      body: Container(
      padding: EdgeInsets.all(10.0),
      child: ListView(
        children: <Widget>[
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              Container(
                child: Text(
                  'Running on: $_platformVersion\n',
                  style: TextStyle(fontSize: 18.0),
                ),
              ),
              Column(
                children: <Widget>[
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      Container(
                        width: buttonWidth,
                        height: 60.0,
                        margin: EdgeInsets.all(7.0),
                        child: FlatButton(
                          color: Colors.amber,
                          padding: EdgeInsets.all(0.0),
                          onPressed: () async {
                            print("---------- Connect Billing Button Pressed");
                            await FlutterInappPurchase.initConnection;
                          },
                          child: Container(
                            padding: EdgeInsets.symmetric(horizontal: 20.0),
                            alignment: Alignment(0.0, 0.0),
                            child: Text(
                              'Connect Billing',
                              style: TextStyle(
                                fontSize: 16.0,
                              ),
                            ),
                          ),
                        ),
                      ),
                      Container(
                        width: buttonWidth,
                        height: 60.0,
                        margin: EdgeInsets.all(7.0),
                        child: FlatButton(
                          color: Colors.amber,
                          padding: EdgeInsets.all(0.0),
                          onPressed: () async {
                            print("---------- End Connection Button Pressed");
                            await FlutterInappPurchase.endConnection;
                            setState(() {
                              this._items = [];
                              this._purchases = [];
                            });
                          },
                          child: Container(
                            padding: EdgeInsets.symmetric(horizontal: 20.0),
                            alignment: Alignment(0.0, 0.0),
                            child: Text(
                              'End Connection',
                              style: TextStyle(
                                fontSize: 16.0,
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                  Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: <Widget>[
                        Container(
                            width: buttonWidth,
                            height: 60.0,
                            margin: EdgeInsets.all(7.0),
                            child: FlatButton(
                              color: Colors.green,
                              padding: EdgeInsets.all(0.0),
                              onPressed: () {
                                print("---------- Get Items Button Pressed");
                                this._getProduct();
                              },
                              child: Container(
                                padding: EdgeInsets.symmetric(horizontal: 20.0),
                                alignment: Alignment(0.0, 0.0),
                                child: Text(
                                  'Get Items',
                                  style: TextStyle(
                                    fontSize: 16.0,
                                  ),
                                ),
                              ),
                            )),
                        Container(
                            width: buttonWidth,
                            height: 60.0,
                            margin: EdgeInsets.all(7.0),
                            child: FlatButton(
                              color: Colors.green,
                              padding: EdgeInsets.all(0.0),
                              onPressed: () {
                                print(
                                    "---------- Get Purchases Button Pressed");
                                this._getPurchases();
                              },
                              child: Container(
                                padding: EdgeInsets.symmetric(horizontal: 20.0),
                                alignment: Alignment(0.0, 0.0),
                                child: Text(
                                  'Get Purchases',
                                  style: TextStyle(
                                    fontSize: 16.0,
                                  ),
                                ),
                              ),
                            )),
                        Container(
                            width: buttonWidth,
                            height: 60.0,
                            margin: EdgeInsets.all(7.0),
                            child: FlatButton(
                              color: Colors.green,
                              padding: EdgeInsets.all(0.0),
                              onPressed: () {
                                print(
                                    "---------- Get Purchase History Button Pressed");
                                this._getPurchaseHistory();
                              },
                              child: Container(
                                padding: EdgeInsets.symmetric(horizontal: 20.0),
                                alignment: Alignment(0.0, 0.0),
                                child: Text(
                                  'Get Purchase History',
                                  style: TextStyle(
                                    fontSize: 16.0,
                                  ),
                                ),
                              ),
                            )),
                      ]),
                ],
              ),
              Column(
                children: this._renderInApps(),
              ),
              Column(
                children: this._renderPurchases(),
              ),
            ],
          ),
        ],
      ),
    ),
    );
  }
}

Ответ 4

Если вам просто нужно запомнить положение прокрутки внутри списка, лучше всего просто использовать объект PageStoreKey для свойства key:

  @override
  Widget build(BuildContext context) {
    return Container(
      child: ListView.builder(
        key: PageStorageKey<String>('some-list-key'),
        scrollDirection: Axis.vertical,
        shrinkWrap: true,
        itemCount: items.length,
        itemBuilder: (BuildContext context, int index) {
          return GestureDetector(
            onTap: () => _onElementTapped(index),
            child: makeCard(items[index])
          );
        },
      ),
    );
  }

Согласно https://docs.flutter.io/flutter/widgets/PageStorageKey-class.html, это должно работать на ЛЮБОМ прокручиваемом виджете.