BottomNavigationBar 的另一个视角
许多人甚至官方文档面对额外导航的方式与 Web Navigation 不同。它们不会呈现具有可用链接的通用和/或部分通用模板,以在单击后执行。相反,他们正在实现的是在额外的“tabs_screen”中预加载不同的屏幕(基本上只是一个额外的 Scaffold,一次包装和呈现一个屏幕),然后,根据 onTap 上的给定索引,甚至是 BottomNavigationBar,您将使用该索引来确定哪个屏幕将在该脚手架内实际呈现。就是这样。
相反,我建议以这种方式处理额外导航:
我像往常一样添加了 BottomNavigationBar,但我没有在 List _pages 上预加载/实例化屏幕,我所做的只是为 FavoritesScreen 添加另一个命名路由,
我设计了我的底部导航栏:
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(膳食)和/或进入特定膳食的 FoodCategoryShowScreen,然后我们在 IOS 上滑动或在两个平台上点击返回,活动选项卡功能将获得疯狂的。它将不再正确显示当前的活动标签。
为什么?
因为您是通过弹出路线到达那里的,而不是通过单击然后执行 onTab 事件,因此不会执行 setState 函数,因此不会更新 activeTab。
解决办法:
使用 RouteObserver 功能。这可以手动完成,但最简单的方法就是安装 route_observer_mixin 包。这就是我们接下来要做的:
- 在您的主文件中,您将使用上述 mixin 中包含的 RouteObserverProvider 包装整个应用程序。 (我现在刚刚添加到上面的[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;
注意:在收藏夹屏幕上,默认值为 1,而不是 0,因为收藏夹是第二个标签(索引 = 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 包将跟踪所有的推送和弹出,并根据用户单击的每次向后滑动或返回链接相应地执行正确的方法,从而相应地更新每个有状态屏幕上的 _activeTab 属性。
- 然后我们将简单地将 int _activeTab 属性作为命名参数传递给每个有状态屏幕内的每个 FeeddyScaffold。像这样:
lib/screens/food_category_index_screen.dart
.
.
.
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 的重新渲染,这将允许始终在 BottomNavigationBar 上相应地显示正确的选项卡索引,根据我们的时间,始终匹配当前路线与活动选项卡。在这种情况下,我们希望始终显示第一个选项卡,除非正在查看收藏夹屏幕。
因此,无论我们是推送还是弹出路由,它都会以非常一致的方式看起来总是这样:
FoodCategoriesIndexScreen:
FoodCategoryShowScreen:
FoodRecipeShowScreen:
FavoritesShow:
更多详情,您可以在 GitHub 上从here 克隆我的应用程序的源代码。
结束。