Flutter Bottom Navigation Bar with Multiple Navigators

emintolgahanpolat
4 min readMar 8, 2023

Bu blog yazısında Bottom Navigation Bar ile birlikte çoklu navigasyon nasıl yapılacağını anlatacağım.

codewithandrea bunun için güzel bir örnek oluşturmuş. Projemizde kullanmak için biraz karışık ve uygulama ilk açıldığında bottom bar da tanımladığımız sayfalar aynı anda yüklendiği için performansı olumsuz yönde etkilemektedir.

Öncelikte aşağıdaki kodu projemize eklememiz gerekiyor. Bu kodu ben yazmadım. CupertinoTabScaffold içerisinden kullanılan bir sınıf. Bottom Bar’a eklediğimiz sayfaların ilk kez görüntülendiğinde oluşturulmasını sağlıyor.



import 'package:flutter/material.dart';

class TabSwitchingView extends StatefulWidget {
const TabSwitchingView({
super.key,
required this.currentTabIndex,
required this.tabCount,
required this.tabBuilder,
}) : assert(tabCount > 0);

final int currentTabIndex;
final int tabCount;
final IndexedWidgetBuilder tabBuilder;

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

class TabSwitchingViewState extends State<TabSwitchingView> {
final List<bool> shouldBuildTab = <bool>[];
final List<FocusScopeNode> tabFocusNodes = <FocusScopeNode>[];

// When focus nodes are no longer needed, we need to dispose of them, but we
// can't be sure that nothing else is listening to them until this widget is
// disposed of, so when they are no longer needed, we move them to this list,
// and dispose of them when we dispose of this widget.
final List<FocusScopeNode> discardedNodes = <FocusScopeNode>[];

@override
void initState() {
super.initState();
shouldBuildTab.addAll(List<bool>.filled(widget.tabCount, false));
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_focusActiveTab();
}

@override
void didUpdateWidget(TabSwitchingView oldWidget) {
super.didUpdateWidget(oldWidget);

// Only partially invalidate the tabs cache to avoid breaking the current
// behavior. We assume that the only possible change is either:
// - new tabs are appended to the tab list, or
// - some trailing tabs are removed.
// If the above assumption is not true, some tabs may lose their state.
final int lengthDiff = widget.tabCount - shouldBuildTab.length;
if (lengthDiff > 0) {
shouldBuildTab.addAll(List<bool>.filled(lengthDiff, false));
} else if (lengthDiff < 0) {
shouldBuildTab.removeRange(widget.tabCount, shouldBuildTab.length);
}
_focusActiveTab();
}

// Will focus the active tab if the FocusScope above it has focus already. If
// not, then it will just mark it as the preferred focus for that scope.
void _focusActiveTab() {
if (tabFocusNodes.length != widget.tabCount) {
if (tabFocusNodes.length > widget.tabCount) {
discardedNodes.addAll(tabFocusNodes.sublist(widget.tabCount));
tabFocusNodes.removeRange(widget.tabCount, tabFocusNodes.length);
} else {
tabFocusNodes.addAll(
List<FocusScopeNode>.generate(
widget.tabCount - tabFocusNodes.length,
(index) => FocusScopeNode(
debugLabel: 'Tab ${index + tabFocusNodes.length}'),
),
);
}
}
FocusScope.of(context).setFirstFocus(tabFocusNodes[widget.currentTabIndex]);
}

@override
void dispose() {
for (final FocusScopeNode focusScopeNode in tabFocusNodes) {
focusScopeNode.dispose();
}
for (final FocusScopeNode focusScopeNode in discardedNodes) {
focusScopeNode.dispose();
}
super.dispose();
}

@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: List<Widget>.generate(widget.tabCount, (index) {
final bool active = index == widget.currentTabIndex;
shouldBuildTab[index] = active || shouldBuildTab[index];

return HeroMode(
enabled: active,
child: Offstage(
offstage: !active,
child: TickerMode(
enabled: active,
child: FocusScope(
node: tabFocusNodes[index],
child: Builder(
builder: (context) => shouldBuildTab[index]
? widget.tabBuilder(context, index)
: Container()),
),
),
),
);
}),
);
}
}

Proje içerisinde naming route kullacağız. Bunun için kendi geliştirdiğim https://pub.dev/packages/route_map paketini kullanacağım. Bu paket Flutter’in varsayılan navigator sınıfında kullanılmak için rota yollarını bizim için oluşturur. TabSwitchingView paket içerisinde mevcut.

dependencies:  
# add route_map to your dependencies
route_map:

dev_dependencies:
# add the generator to your dev_dependencies
route_map_generator:
# add build runner if not already added
build_runner:

route_map.dart adında bir dosya oluşturarak aşağıdaki kodu ekliyoruz.

import 'route_map.config.dart';

@RouteMapInit()
Route? onGenerateRoute(RouteSettings routeSettings) => $onGenerateRoute(routeSettings);

Daha sonra rota yoluna eklenmesini istediğimiz sayfaların başına @RouteMap() etiketini ekliyoruz. Burada dikkat etmemiz gereken bir kaç konu var. “name“ alanı rotamızın tanımlayıcısıdır bu alanı boş tanımlamadığımızda paket bunu “/sayfaAdı” şeklinde rota yoluna ekleyecektir.

Flutter route name üzerinden işlemleri şu şekilde yapmaktadır.

"splash" // öncesinde sayfa olmadığı anlamına gelir kök dizinden önceye eklenebilir
"/" // Kök dizin olarak tanımlanır
"/home" // Kök dizinden sonra oluşur.

Uygulamanın initial route olarak “/home” sayfasından başlamasını istersek öncelikle “/” kök sayfa oluşacaktır sonra “/home” sayfası oluşur. Geri tuşuna bastığımızda “/” kök dizine yönlendirir.

RouteMap(name: "splash")
class SplashPage extends StatefulWidget {}

@RouteMap(name: "/")
class RootPage extends StatefulWidget {}

@RouteMap(name: "home")
class HomePage extends StatefulWidget {}

@RouteMap(name: "/search", fullScreenDialog: true)
class SearchPage extends StatefulWidget {}

Bottom navigation barda kullanacağımız sayfaların name alanını başında “/” işareti olmadan oluşturmamız gerekiyor. Bu paketi kullanmasanız bile material app içerisindeki on generator için manuel olarak tanımladığınızda da aynı işlemi yapmanız gerekiyor.

Rota yolunu oluşturmak için şu komutu çalıştırıyoruz.

flutter packages pub run build_runner build --delete-conflicting-outputs

Material app barıda aşağıdaki gibi tanımlamamız gerekiyor.

MaterialApp(
initialRoute: RouteMaps.root,
onGenerateRoute: onGenerateRoute, // route_map.dart olarak oluşturuduğumuz fonksiyonu buraya ekliyoruz.
)

Kodu biraz dinamik yazmak istiyorum. Bottom bar içerisine uygulama kullanılırken öge ekleyip çıkarabilirsiniz.

Ögeleri belirtmek için model oluşturuyoruz.

class NavItemModel {
NavItemModel({
required this.icon,
required this.label,
required this.route,
required this.key,
});
final IconData icon;
final String label;
final String route;
final GlobalKey<NavigatorState> key;
}

Icon ve Label bottom barda gösterilecek alanlar ek olarak badge ekleyebilirsiniz.

Root sayfayı oluşturuyoruz ve etike olarak @RouteMap(name: ‘/’) ekliyoruz.(Tekrar build runner çalıştırmak gerekiyor.)

@RouteMap(name: '/')
class RootPage extends StatefulWidget {
const RootPage({super.key});

@override
State<RootPage> createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {
final List<NavItemModel> _items = [
NavItemModel(
icon: FontAwesomeIcons.qrcode,
label: 'Scanner',
route: RouteMaps.scanner,
key: GlobalKey<NavigatorState>()),
NavItemModel(
icon: FontAwesomeIcons.clockRotateLeft,
label: 'History',
route: RouteMaps.history,
key: GlobalKey<NavigatorState>()),
NavItemModel(
icon: FontAwesomeIcons.heart,
label: 'My Code',
route: RouteMaps.myCode,
key: GlobalKey<NavigatorState>()),
NavItemModel(
icon: FontAwesomeIcons.gear,
label: 'Settings',
route: RouteMaps.settings,
key: GlobalKey<NavigatorState>()),
];
int currentIndex = 0;
Widget get content => TabSwitchingView(
currentTabIndex: currentIndex,
tabCount: _items.length,
tabBuilder: (c, index) => Navigator(
key: _items[index].key,
initialRoute: _items[index].route,
onUnknownRoute: (settings) => MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('unknown')),
)),
onGenerateRoute: onGenerateRoute));
@override
Widget build(BuildContext context) => Scaffold(
body: content,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex,
onTap: (i){
currentIndex = i;
setState((){});
},
items: _items
.map((e) => BottomNavigationBarItem(
icon: Icon(e.icon),
label: e.label,
))
.toList(),
));
}

--

--

No responses yet