Flutter 更新 BottomNavigationBar
Flutter update BottomNavigationBar
我将 BottomNavigationBar 与 TabController 一起使用。
通过单击 BottomNavigationBar 的不同选项卡,TabView 正在更改内容。
但是,如果我在 TabView 上滑动以切换到另一个 view/tab,BottomNavigationBar 不会更新到我滑动到的选项卡。
我已经向 TabController 添加了一个侦听器来检测更改。
但是如何以编程方式更新 BottomNavigationBar 以反映更改?
我认为在你的情况下使用 PageView
比 TabBarView
更优雅。
class BottomBarExample extends StatefulWidget {
@override
_BottomBarExampleState createState() => new _BottomBarExampleState();
}
class _BottomBarExampleState extends State<BottomBarExample> {
int _page = 0;
PageController _c;
@override
void initState(){
_c = new PageController(
initialPage: _page,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _page,
onTap: (index){
this._c.animateToPage(index,duration: const Duration(milliseconds: 500),curve: Curves.easeInOut);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(icon: new Icon(Icons.supervised_user_circle), title: new Text("Users")),
new BottomNavigationBarItem(icon: new Icon(Icons.notifications), title: new Text("Alerts")),
new BottomNavigationBarItem(icon: new Icon(Icons.email), title: new Text("Inbox")),
],
),
body: new PageView(
controller: _c,
onPageChanged: (newPage){
setState((){
this._page=newPage;
});
},
children: <Widget>[
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.supervised_user_circle),
new Text("Users")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.notifications),
new Text("Alerts")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.mail),
new Text("Inbox")
],
),
),
],
),
);
}
}
当您在 TabView 上滑动时,您应该更新 bottomNavigationBar 的当前索引以匹配 tabview 的新索引
class _TabBarWithBottomNavBarState extends State<TabBarWithBottomNavBar> with SingleTickerProviderStateMixin {
int _bottomNavBarIndex = 0;
TabController _tabController;
void _tabControllerListener(){
setState(() {
_bottomNavBarIndex = _tabController.index;
});
}
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(_tabControllerListener);
super.initState();
}
@override
void dispose() {
_tabController.dispose();
_tabController.removeListener(_tabControllerListener);
super.dispose();
}
对于正在寻找简短解决方案的人来说,这是我的,也许对某人有用:
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
int currentTab = 0;
void _selectTab(int index) {
debugPrint (" index = $index ");
setState(() {
currentTab = index;
});
switch (currentTab) {
case 0:
debugPrint (" my index 0 ");
break;
case 1:
debugPrint (" my index 1 ");
break;
case 2:
debugPrint (" my index 2 ");
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: _selectTab,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text("Home"),
),
BottomNavigationBarItem(
icon: Icon(Icons.message), title: Text("Message"),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), title: Text("Settings"),
),
],
),
);
}
Widget _buildBody() {
// return a widget representing a page
}
}
还有我们别忘了主要的,应该是这样的:
import 'package:flutter/material.dart';
import 'App.dart';
void main() {
runApp( MaterialApp(
home: App(),
)
);
}
BottomNavigationBar的另一种视角
许多人甚至官方文档面对额外导航的方式与 Web 导航不同。他们没有呈现一个通用 and/or 部分通用模板,其中包含可用 link 以在单击后执行。相反,他们正在实施的是在额外的“tabs_screen”中预加载不同的屏幕(基本上只是一个额外的脚手架,当时包装和渲染一个屏幕)然后,根据 onTap 上的给定索引甚至BottomNavigationBar 的索引,您将使用该索引来确定哪个屏幕将在该脚手架内实际呈现。就这些了。
相反,我建议以这种方式处理额外导航:
我像往常一样添加了 BottomNavigationBar,但是我没有 preloading/instantiating List _pages 上的屏幕,我只是为 FavoritesScreen 添加了另一个命名路由,
我设计了我的 bottomNavigationBar:
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
然后在 onTap 上我什至调用了 onTapSelectNavigation 方法。
在该方法中,根据给定的点击标签索引,我将决定调用哪条路由。此 BottomNavigationBar 可用于整个应用程序。为什么?因为我在我的 FeeddyScaffold 上实现了它,这对所有屏幕都很常见,因为 FeeddyScaffold 包装了所有这些屏幕上的所有内部小部件。在每个屏幕上,我实例化 FeeddyScaffold,将 Widget 列表作为命名参数传递。这样我保证脚手架对于每个屏幕都是通用的,如果我实现了那个通用脚手架的额外导航,那么它将对屏幕可用。这是我的 FeeddyScaffold 组件:
// Packages:
import 'package:feeddy_flutter/_inner_packages.dart';
import 'package:feeddy_flutter/_external_packages.dart';
// Screens:
import 'package:feeddy_flutter/screens/_screens.dart';
// Models:
import 'package:feeddy_flutter/models/_models.dart';
// Components:
import 'package:feeddy_flutter/components/_components.dart';
// Helpers:
import 'package:feeddy_flutter/helpers/_helpers.dart';
// Utilities:
import 'package:feeddy_flutter/utilities/_utilities.dart';
class FeeddyScaffold extends StatefulWidget {
// Properties:
final bool _showPortraitOnly = false;
final String appTitle;
final Function onPressedAdd;
final String objectName;
final int objectsLength;
final List<Widget> innerWidgets;
final int activeIndex;
// Constructor:
const FeeddyScaffold({
Key key,
this.appTitle,
this.onPressedAdd,
this.objectName,
this.objectsLength,
this.innerWidgets,
this.activeIndex,
}) : super(key: key);
@override
_FeeddyScaffoldState createState() => _FeeddyScaffoldState();
}
class _FeeddyScaffoldState extends State<FeeddyScaffold> {
final bool _showPortraitOnly = false;
int _activeIndex;
@override
void initState() {
// TODO: implement initState
super.initState();
_activeIndex = widget.activeIndex;
}
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
@override
Widget build(BuildContext context) {
AppData appData = Provider.of<AppData>(context, listen: true);
Function closeAllThePanels = appData.closeAllThePanels; // Drawer related:
bool deviceIsIOS = DeviceHelper.deviceIsIOS(context);
// WidgetsFlutterBinding.ensureInitialized(); // Without this it might not work in some devices:
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
// DeviceOrientation.portraitDown,
if (!_showPortraitOnly) ...[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
]);
FeeddyAppBar appBar = FeeddyAppBar(
appTitle: widget.appTitle,
onPressedAdd: widget.onPressedAdd,
objectName: widget.objectName,
);
return Scaffold(
appBar: appBar,
onDrawerChanged: (isOpened) {
if (!isOpened) {
closeAllThePanels();
}
},
drawer: FeeddyDrawer(),
body: NativeDeviceOrientationReader(
builder: (context) {
final orientation = NativeDeviceOrientationReader.orientation(context);
bool safeAreaLeft = DeviceHelper.isLandscapeLeft(orientation);
bool safeAreaRight = DeviceHelper.isLandscapeRight(orientation);
bool isLandscape = DeviceHelper.isLandscape(orientation);
return SafeArea(
left: safeAreaLeft,
right: safeAreaRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.innerWidgets,
),
);
},
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
// FAB
floatingActionButton: deviceIsIOS
? null
: FloatingActionButton(
tooltip: 'Add ${widget.objectName.inCaps}',
child: Icon(Icons.add),
onPressed: () => widget.onPressedAdd,
),
// floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButtonLocation: deviceIsIOS ? null : FloatingActionButtonLocation.endDocked,
);
}
}
因此,在 onTapSelectNavigation 方法上,根据给定的选项卡索引,并使用简单的 Switch case 语句,我决定调用哪个命名路由。就这么简单。
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
现在,这还不够。为什么?因为当我为整个应用程序提供额外导航时,会发生一些奇怪的事情。
如果我们只使用一个简单的 setState 在我们的本地状态中本地设置和存储这个公共脚手架上的 activeTab,那么当我们在两个选项卡之间穿梭时,它会起到很好的作用。但是,当我们从 FoodCategoryIndexScreen 单击进入特定的 FoodCategory 时,访问包含 FoodRecipesList(膳食)and/or 的 FoodCategoryShowScreen 进入特定的膳食,然后我们向后滑动 IOS 或在两个平台上单击返回,活动选项卡功能变得疯狂。它将不再正确显示当前活动标签。
为什么?
因为您是通过弹出路线到达那里的,而不是通过单击然后执行 onTab 事件,因此不会执行 setState 函数,因此不会更新 activeTab。
解决方法:
使用 RouteObserver 功能。这可以手动完成,但最简单的方法就是安装 route_observer_mixin 包。这就是我们接下来要做的:
- 在你的主文件中,你将使用 RouteObserverProvider 包装你的整个应用程序,包含在提到的 mixin 中。 (我现在刚刚添加到上面的[RouteObserverProvider],其余的我之前已经添加了):
void main() {
runApp(MultiProvider(
providers: [
RouteObserverProvider(),
// Config about the app:
ChangeNotifierProvider<AppData>(
create: (context) => AppData(),
),
// Data related to the FoodCategoriesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesData>(
create: (context) => FoodCategoriesData(),
),
// Data related to the FoodCategoriesFoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesFoodRecipesData>(
create: (context) => FoodCategoriesFoodRecipesData(),
),
// Data related to the FoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodRecipesData>(
create: (context) => FoodRecipesData(),
),
// Data related to the FoodIngredientsData objects: (sqlite)
ChangeNotifierProvider<FoodIngredientsData>(
create: (context) => FoodIngredientsData(),
),
// Data related to the RecipeStepsData objects: (sqlite)
ChangeNotifierProvider<RecipeStepsData>(
create: (context) => RecipeStepsData(),
),
],
// child: MyApp(),
child: InitialSplashScreen(),
));
}
- 在包含额外导航的每个屏幕上(显示
BottomNavigationBar)你将添加 RouteAware 和
RouteObserverMixin 到你的状态。
示例:
.
.
.
class _FoodCategoryIndexScreenState extends State<FoodCategoryIndexScreen> with RouteAware, RouteObserverMixin {
.
.
.
在每个有状态屏幕(FavoritesScreen 除外)上,我们将 属性 添加到本地状态:
int _activeTab = 0;
注意:在 FavoritesScreen 上默认为 1,而不是 0,当然,因为 Favorites 是第二个标签(索引 = 1)。所以在 FavoritesScreen 中是这样的:
int _activeTab = 1;
- 然后您将在每个有状态屏幕上覆盖 RouteAware 方法(我们只需要 didPopNext 和 didPush):
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
当然在FavoritesScreen中是这样的:
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
RouterObserver 包将跟踪所有的推送和弹出,并根据用户每次向后滑动或向后点击 link 相应地执行正确的方法,因此相应地更新 _activeTab 属性 在每个状态屏幕上。
- 然后我们将简单地将 int _activeTab 属性 作为命名参数传递给每个有状态屏幕内的每个 FeeddyScaffold。像这样:
lib/screens/food_category_index_screen.飞镖
.
.
.
return FeeddyScaffold(
activeIndex: _activeTab,
appTitle: widget.appTitle,
innerWidgets: [
// Food Categories Grid:
Expanded(
flex: 5,
child: FoodCategoriesGrid(),
),
],
objectsLength: amountTotalFoodCategories,
objectName: 'category',
onPressedAdd: () => _showModalNewFoodCategory(context),
);
}
.
.
.
基于 setState 执行的 _activeTab 属性 的每次更新,将始终引发 UI 的 re-rendering,这将允许始终相应地显示正确的选项卡索引BottomNavigationBar,基于我们的时间,始终将当前路由与活动选项卡匹配。在这种情况下,我们希望始终显示第一个选项卡处于活动状态,除非正在查看avoritesScreen.
因此,无论我们是推送还是弹出路由,它看起来总是以非常一致的方式:
有关更多详细信息,您可以从 here.
在 GitHub 克隆我的应用程序的源代码
结束。
我将 BottomNavigationBar 与 TabController 一起使用。 通过单击 BottomNavigationBar 的不同选项卡,TabView 正在更改内容。 但是,如果我在 TabView 上滑动以切换到另一个 view/tab,BottomNavigationBar 不会更新到我滑动到的选项卡。 我已经向 TabController 添加了一个侦听器来检测更改。 但是如何以编程方式更新 BottomNavigationBar 以反映更改?
我认为在你的情况下使用 PageView
比 TabBarView
更优雅。
class BottomBarExample extends StatefulWidget {
@override
_BottomBarExampleState createState() => new _BottomBarExampleState();
}
class _BottomBarExampleState extends State<BottomBarExample> {
int _page = 0;
PageController _c;
@override
void initState(){
_c = new PageController(
initialPage: _page,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _page,
onTap: (index){
this._c.animateToPage(index,duration: const Duration(milliseconds: 500),curve: Curves.easeInOut);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(icon: new Icon(Icons.supervised_user_circle), title: new Text("Users")),
new BottomNavigationBarItem(icon: new Icon(Icons.notifications), title: new Text("Alerts")),
new BottomNavigationBarItem(icon: new Icon(Icons.email), title: new Text("Inbox")),
],
),
body: new PageView(
controller: _c,
onPageChanged: (newPage){
setState((){
this._page=newPage;
});
},
children: <Widget>[
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.supervised_user_circle),
new Text("Users")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.notifications),
new Text("Alerts")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.mail),
new Text("Inbox")
],
),
),
],
),
);
}
}
当您在 TabView 上滑动时,您应该更新 bottomNavigationBar 的当前索引以匹配 tabview 的新索引
class _TabBarWithBottomNavBarState extends State<TabBarWithBottomNavBar> with SingleTickerProviderStateMixin {
int _bottomNavBarIndex = 0;
TabController _tabController;
void _tabControllerListener(){
setState(() {
_bottomNavBarIndex = _tabController.index;
});
}
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(_tabControllerListener);
super.initState();
}
@override
void dispose() {
_tabController.dispose();
_tabController.removeListener(_tabControllerListener);
super.dispose();
}
对于正在寻找简短解决方案的人来说,这是我的,也许对某人有用:
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
int currentTab = 0;
void _selectTab(int index) {
debugPrint (" index = $index ");
setState(() {
currentTab = index;
});
switch (currentTab) {
case 0:
debugPrint (" my index 0 ");
break;
case 1:
debugPrint (" my index 1 ");
break;
case 2:
debugPrint (" my index 2 ");
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: _selectTab,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text("Home"),
),
BottomNavigationBarItem(
icon: Icon(Icons.message), title: Text("Message"),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), title: Text("Settings"),
),
],
),
);
}
Widget _buildBody() {
// return a widget representing a page
}
}
还有我们别忘了主要的,应该是这样的:
import 'package:flutter/material.dart';
import 'App.dart';
void main() {
runApp( MaterialApp(
home: App(),
)
);
}
BottomNavigationBar的另一种视角
许多人甚至官方文档面对额外导航的方式与 Web 导航不同。他们没有呈现一个通用 and/or 部分通用模板,其中包含可用 link 以在单击后执行。相反,他们正在实施的是在额外的“tabs_screen”中预加载不同的屏幕(基本上只是一个额外的脚手架,当时包装和渲染一个屏幕)然后,根据 onTap 上的给定索引甚至BottomNavigationBar 的索引,您将使用该索引来确定哪个屏幕将在该脚手架内实际呈现。就这些了。
相反,我建议以这种方式处理额外导航:
我像往常一样添加了 BottomNavigationBar,但是我没有 preloading/instantiating List _pages 上的屏幕,我只是为 FavoritesScreen 添加了另一个命名路由,
我设计了我的 bottomNavigationBar:
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
然后在 onTap 上我什至调用了 onTapSelectNavigation 方法。
在该方法中,根据给定的点击标签索引,我将决定调用哪条路由。此 BottomNavigationBar 可用于整个应用程序。为什么?因为我在我的 FeeddyScaffold 上实现了它,这对所有屏幕都很常见,因为 FeeddyScaffold 包装了所有这些屏幕上的所有内部小部件。在每个屏幕上,我实例化 FeeddyScaffold,将 Widget 列表作为命名参数传递。这样我保证脚手架对于每个屏幕都是通用的,如果我实现了那个通用脚手架的额外导航,那么它将对屏幕可用。这是我的 FeeddyScaffold 组件:
// Packages:
import 'package:feeddy_flutter/_inner_packages.dart';
import 'package:feeddy_flutter/_external_packages.dart';
// Screens:
import 'package:feeddy_flutter/screens/_screens.dart';
// Models:
import 'package:feeddy_flutter/models/_models.dart';
// Components:
import 'package:feeddy_flutter/components/_components.dart';
// Helpers:
import 'package:feeddy_flutter/helpers/_helpers.dart';
// Utilities:
import 'package:feeddy_flutter/utilities/_utilities.dart';
class FeeddyScaffold extends StatefulWidget {
// Properties:
final bool _showPortraitOnly = false;
final String appTitle;
final Function onPressedAdd;
final String objectName;
final int objectsLength;
final List<Widget> innerWidgets;
final int activeIndex;
// Constructor:
const FeeddyScaffold({
Key key,
this.appTitle,
this.onPressedAdd,
this.objectName,
this.objectsLength,
this.innerWidgets,
this.activeIndex,
}) : super(key: key);
@override
_FeeddyScaffoldState createState() => _FeeddyScaffoldState();
}
class _FeeddyScaffoldState extends State<FeeddyScaffold> {
final bool _showPortraitOnly = false;
int _activeIndex;
@override
void initState() {
// TODO: implement initState
super.initState();
_activeIndex = widget.activeIndex;
}
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
@override
Widget build(BuildContext context) {
AppData appData = Provider.of<AppData>(context, listen: true);
Function closeAllThePanels = appData.closeAllThePanels; // Drawer related:
bool deviceIsIOS = DeviceHelper.deviceIsIOS(context);
// WidgetsFlutterBinding.ensureInitialized(); // Without this it might not work in some devices:
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
// DeviceOrientation.portraitDown,
if (!_showPortraitOnly) ...[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
]);
FeeddyAppBar appBar = FeeddyAppBar(
appTitle: widget.appTitle,
onPressedAdd: widget.onPressedAdd,
objectName: widget.objectName,
);
return Scaffold(
appBar: appBar,
onDrawerChanged: (isOpened) {
if (!isOpened) {
closeAllThePanels();
}
},
drawer: FeeddyDrawer(),
body: NativeDeviceOrientationReader(
builder: (context) {
final orientation = NativeDeviceOrientationReader.orientation(context);
bool safeAreaLeft = DeviceHelper.isLandscapeLeft(orientation);
bool safeAreaRight = DeviceHelper.isLandscapeRight(orientation);
bool isLandscape = DeviceHelper.isLandscape(orientation);
return SafeArea(
left: safeAreaLeft,
right: safeAreaRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.innerWidgets,
),
);
},
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
// FAB
floatingActionButton: deviceIsIOS
? null
: FloatingActionButton(
tooltip: 'Add ${widget.objectName.inCaps}',
child: Icon(Icons.add),
onPressed: () => widget.onPressedAdd,
),
// floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButtonLocation: deviceIsIOS ? null : FloatingActionButtonLocation.endDocked,
);
}
}
因此,在 onTapSelectNavigation 方法上,根据给定的选项卡索引,并使用简单的 Switch case 语句,我决定调用哪个命名路由。就这么简单。
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
现在,这还不够。为什么?因为当我为整个应用程序提供额外导航时,会发生一些奇怪的事情。
如果我们只使用一个简单的 setState 在我们的本地状态中本地设置和存储这个公共脚手架上的 activeTab,那么当我们在两个选项卡之间穿梭时,它会起到很好的作用。但是,当我们从 FoodCategoryIndexScreen 单击进入特定的 FoodCategory 时,访问包含 FoodRecipesList(膳食)and/or 的 FoodCategoryShowScreen 进入特定的膳食,然后我们向后滑动 IOS 或在两个平台上单击返回,活动选项卡功能变得疯狂。它将不再正确显示当前活动标签。
为什么?
因为您是通过弹出路线到达那里的,而不是通过单击然后执行 onTab 事件,因此不会执行 setState 函数,因此不会更新 activeTab。
解决方法:
使用 RouteObserver 功能。这可以手动完成,但最简单的方法就是安装 route_observer_mixin 包。这就是我们接下来要做的:
- 在你的主文件中,你将使用 RouteObserverProvider 包装你的整个应用程序,包含在提到的 mixin 中。 (我现在刚刚添加到上面的[RouteObserverProvider],其余的我之前已经添加了):
void main() {
runApp(MultiProvider(
providers: [
RouteObserverProvider(),
// Config about the app:
ChangeNotifierProvider<AppData>(
create: (context) => AppData(),
),
// Data related to the FoodCategoriesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesData>(
create: (context) => FoodCategoriesData(),
),
// Data related to the FoodCategoriesFoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesFoodRecipesData>(
create: (context) => FoodCategoriesFoodRecipesData(),
),
// Data related to the FoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodRecipesData>(
create: (context) => FoodRecipesData(),
),
// Data related to the FoodIngredientsData objects: (sqlite)
ChangeNotifierProvider<FoodIngredientsData>(
create: (context) => FoodIngredientsData(),
),
// Data related to the RecipeStepsData objects: (sqlite)
ChangeNotifierProvider<RecipeStepsData>(
create: (context) => RecipeStepsData(),
),
],
// child: MyApp(),
child: InitialSplashScreen(),
));
}
- 在包含额外导航的每个屏幕上(显示 BottomNavigationBar)你将添加 RouteAware 和 RouteObserverMixin 到你的状态。
示例:
.
.
.
class _FoodCategoryIndexScreenState extends State<FoodCategoryIndexScreen> with RouteAware, RouteObserverMixin {
.
.
.
在每个有状态屏幕(FavoritesScreen 除外)上,我们将 属性 添加到本地状态:
int _activeTab = 0;
注意:在 FavoritesScreen 上默认为 1,而不是 0,当然,因为 Favorites 是第二个标签(索引 = 1)。所以在 FavoritesScreen 中是这样的:
int _activeTab = 1;
- 然后您将在每个有状态屏幕上覆盖 RouteAware 方法(我们只需要 didPopNext 和 didPush):
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
当然在FavoritesScreen中是这样的:
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
RouterObserver 包将跟踪所有的推送和弹出,并根据用户每次向后滑动或向后点击 link 相应地执行正确的方法,因此相应地更新 _activeTab 属性 在每个状态屏幕上。
- 然后我们将简单地将 int _activeTab 属性 作为命名参数传递给每个有状态屏幕内的每个 FeeddyScaffold。像这样:
lib/screens/food_category_index_screen.飞镖
.
.
.
return FeeddyScaffold(
activeIndex: _activeTab,
appTitle: widget.appTitle,
innerWidgets: [
// Food Categories Grid:
Expanded(
flex: 5,
child: FoodCategoriesGrid(),
),
],
objectsLength: amountTotalFoodCategories,
objectName: 'category',
onPressedAdd: () => _showModalNewFoodCategory(context),
);
}
.
.
.
基于 setState 执行的 _activeTab 属性 的每次更新,将始终引发 UI 的 re-rendering,这将允许始终相应地显示正确的选项卡索引BottomNavigationBar,基于我们的时间,始终将当前路由与活动选项卡匹配。在这种情况下,我们希望始终显示第一个选项卡处于活动状态,除非正在查看avoritesScreen.
因此,无论我们是推送还是弹出路由,它看起来总是以非常一致的方式:
有关更多详细信息,您可以从 here.
在 GitHub 克隆我的应用程序的源代码结束。