@@ -16,6 +16,7 @@ import 'package:flutter/services.dart';
1616import 'package:flutter_gettext/flutter_gettext/context_ext.dart' ;
1717import 'package:flutter_gettext/flutter_gettext/gettext_localizations.dart' ;
1818import 'package:logger/logger.dart' ;
19+ import 'package:persistent_bottom_nav_bar/persistent_bottom_nav_bar.dart' ;
1920import 'package:provider/provider.dart' ;
2021import 'package:flutter_native_splash/flutter_native_splash.dart' ;
2122
@@ -27,6 +28,7 @@ import '../../features/devices/views/devices_screen.dart';
2728import '../../features/my/views/my_screen.dart' ;
2829
2930import '../view_models/main_view_model.dart' ;
31+ import '../../routes/route_manager.dart' ;
3032
3133enum PlusMenuIndexes { addScene, addGroup, addDevice }
3234
@@ -143,86 +145,101 @@ class MainScreen extends StatefulWidget {
143145}
144146
145147class _MainScreenState extends State <MainScreen > {
146- late final PageController _pageController;
148+ // controller for persistent_bottom_nav_bar package
149+ late final PersistentTabController _persistentController;
150+
151+ // One GlobalKey per tab so we can interrogate each tab's Navigator state
152+ // and manually pop sub-pages when the Android back button is pressed.
153+ final List <GlobalKey <NavigatorState >> _tabNavKeys = List .generate (3 , (_) => GlobalKey <NavigatorState >());
147154
148155 @override
149156 void initState () {
150157 super .initState ();
151- _pageController = PageController (initialPage : 0 );
158+ _persistentController = PersistentTabController (initialIndex : 0 );
152159 }
153160
154161 @override
155162 void dispose () {
156- _pageController.dispose ();
157163 super .dispose ();
158164 }
159165
160166 Widget buildScaffold (BuildContext context) {
161167 final mainVM = context.read <MainViewModel >();
168+ final routeManager = context.read <RouteManager >();
169+
170+ List <Widget > buildScreens () => const [
171+ ProvideScenesViewModel (child: ScenesScreen (key: ValueKey ('scenes' ))),
172+ DevicesScreen (key: ValueKey ('devices' )),
173+ MyScreen (key: ValueKey ('my' )),
174+ ];
175+
176+ // Each tab needs its own RouteAndNavigatorSettings so that
177+ // Navigator.of(context).pushNamed(...) calls inside tabs can resolve
178+ // named routes (device detail pages, discovery screen, etc.).
179+ // We also pass individual GlobalKeys so the outer PopScope can check
180+ // whether a tab's navigator can pop before running the exit logic.
181+ RouteAndNavigatorSettings tabNavSettings (int index) =>
182+ RouteAndNavigatorSettings (onGenerateRoute: routeManager.onGenerateRoute, navigatorKey: _tabNavKeys[index]);
183+
184+ List <PersistentBottomNavBarItem > navBarItems () => [
185+ PersistentBottomNavBarItem (
186+ icon: const Icon (Icons .house),
187+ inactiveIcon: const Icon (Icons .house_outlined),
188+ title: context.translate ('Scenes' ),
189+ activeColorPrimary: Theme .of (context).colorScheme.primary,
190+ inactiveColorPrimary: Theme .of (context).colorScheme.onSurface,
191+ routeAndNavigatorSettings: tabNavSettings (0 ),
192+ ),
193+ PersistentBottomNavBarItem (
194+ icon: const Icon (Icons .device_hub),
195+ inactiveIcon: const Icon (Icons .device_hub_outlined),
196+ title: context.translate ('Devices' ),
197+ activeColorPrimary: Theme .of (context).colorScheme.primary,
198+ inactiveColorPrimary: Theme .of (context).colorScheme.onSurface,
199+ routeAndNavigatorSettings: tabNavSettings (1 ),
200+ ),
201+ PersistentBottomNavBarItem (
202+ icon: const Icon (Icons .person),
203+ inactiveIcon: const Icon (Icons .person_outline),
204+ title: context.translate ('My' ),
205+ activeColorPrimary: Theme .of (context).colorScheme.primary,
206+ inactiveColorPrimary: Theme .of (context).colorScheme.onSurface,
207+ routeAndNavigatorSettings: tabNavSettings (2 ),
208+ ),
209+ ];
210+
162211 return Selector <MainViewModel , TabIndices >(
163212 selector: (context, vm) => vm.currentTabIndex,
164213 builder: (context, tabIndex, child) {
165- WidgetsBinding .instance.addPostFrameCallback ((_) {
166- if (_pageController.hasClients) {
167- final current = _pageController.page? .round () ?? _pageController.initialPage;
168- if (current != tabIndex.index) {
169- _pageController.animateToPage (
170- tabIndex.index,
171- duration: const Duration (milliseconds: 300 ),
172- curve: Curves .easeOutCubic,
173- );
174- }
175- }
176- });
214+ // keep controller in sync with view model state
215+ if (_persistentController.index != tabIndex.index) {
216+ _persistentController.jumpToTab (tabIndex.index);
217+ }
177218
178219 return AnnotatedRegion <SystemUiOverlayStyle >(
179220 value: const SystemUiOverlayStyle (
180221 statusBarColor: Colors .transparent,
181222 statusBarIconBrightness: Brightness .dark,
182223 ),
183- child: Scaffold (
184- appBar: null ,
185- body: PageView (
186- controller: _pageController,
187- // Enable user swipe to switch tabs
188- physics: const PageScrollPhysics (),
189- onPageChanged: (index) {
190- if (index != tabIndex.index) {
191- mainVM.setIndex (TabIndices .values[index]);
192- }
193- },
194- children: const [
195- ProvideScenesViewModel (child: ScenesScreen (key: ValueKey ('scenes' ))),
196- DevicesScreen (key: ValueKey ('devices' )),
197- MyScreen (key: ValueKey ('my' )),
198- ],
199- ),
200-
201- bottomNavigationBar: BottomNavigationBar (
202- currentIndex: tabIndex.index,
203- onTap: (index) {
204- if (index != tabIndex.index) {
205- _pageController.jumpToPage (index);
206- }
207- },
208- items: [
209- BottomNavigationBarItem (
210- icon: const Icon (Icons .house_outlined),
211- activeIcon: const Icon (Icons .house),
212- label: context.translate ('Scenes' ),
213- ),
214- BottomNavigationBarItem (
215- icon: const Icon (Icons .device_hub_outlined),
216- activeIcon: const Icon (Icons .device_hub),
217- label: context.translate ('Devices' ),
218- ),
219- BottomNavigationBarItem (
220- icon: const Icon (Icons .person_outline),
221- activeIcon: const Icon (Icons .person),
222- label: context.translate ('My' ),
223- ),
224- ],
225- ),
224+ child: PersistentTabView (
225+ context,
226+ controller: _persistentController,
227+ screens: buildScreens (),
228+ items: navBarItems (),
229+ backgroundColor: Theme .of (context).colorScheme.surfaceContainerHighest,
230+ //Theme.of(context).bottomNavigationBarTheme.backgroundColor ??
231+ //Theme.of(context).colorScheme.surfaceContainerHighest,
232+ // Back-button handling is fully managed by the outer PopScope
233+ // with per-tab navigator-key checks, so we disable the built-in
234+ // handler to avoid double-handling.
235+ handleAndroidBackButtonPress: false ,
236+ resizeToAvoidBottomInset: true ,
237+ onItemSelected: (index) {
238+ if (index != tabIndex.index) {
239+ mainVM.setIndex (TabIndices .values[index]);
240+ }
241+ },
242+ navBarStyle: NavBarStyle .style9,
226243 ),
227244 );
228245 },
@@ -304,6 +321,15 @@ class _MainScreenState extends State<MainScreen> {
304321 onPopInvokedWithResult: (didPop, _) async {
305322 if (didPop) return ;
306323
324+ // If the current tab's inner Navigator has pages above root,
325+ // pop those first – no exit prompt in that case.
326+ final tabNavState = _tabNavKeys[_persistentController.index].currentState;
327+ if (tabNavState != null && tabNavState.canPop ()) {
328+ tabNavState.pop ();
329+ return ;
330+ }
331+
332+ // We're at the navigation root: apply double-back-to-exit.
307333 final shouldPop = await vm.handleWillPop ();
308334 if (! shouldPop) {
309335 if (context.mounted) {
@@ -313,11 +339,7 @@ class _MainScreenState extends State<MainScreen> {
313339 ).showInfo (context.translate ('Press back again to exit' ));
314340 }
315341 } else if (context.mounted) {
316- if (Navigator .of (context).canPop ()) {
317- Navigator .of (context).pop ();
318- } else {
319- SystemNavigator .pop ();
320- }
342+ SystemNavigator .pop ();
321343 }
322344 },
323345 child: Selector <MainViewModel , bool >(
0 commit comments