jb3rndt / PersistentBottomNavBarV2

A highly customizable persistent bottom navigation bar for Flutter
https://pub.dev/packages/persistent_bottom_nav_bar_v2
BSD 3-Clause "New" or "Revised" License
49 stars 54 forks source link

[Bug]: popAllScreensOnTapOfSelectedTab and onSelectedTabPressWhenNoScreensPushed not working well #147

Closed lucasfcardozo closed 5 months ago

lucasfcardozo commented 5 months ago

Version

5.2.2

What platforms are you seeing the problem on?

No response

What happened?

popAllScreensOnTapOfSelectedTab will only work, if we don't define navigatorConfig.initialRoute. In this example, I removed inicialRoute from home tab. There works very well.

But if you go on second tab, tap then twice, onSelectedTabPressWhenNoScreensPushed will nerver fired. It seems that the part that detects that it is on the first screen works (you can check this if you tap on "go to 2nd screen" and then tap on the "Messages" tab.), however it doesn't understand that it's to call 'onSelectedTabPressWhenNoScreensPushed'.

The funniest thing comes now: still on Messages tab, if you tap on the back button from the top of the screen, and tap on the messages tab, onSelectedTabPressWhenNoScreensPushed will be fired but all screen will be gone. And if you tap again ...

FlutterError (Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.)

Steps to reproduce

  1. run the code and then, tap Home tab. See I/flutter ( ): scrolled to top log.
  2. Tap 2x on Messages tab. The messages tab will be displayed, but scrolled to top log will never be printed.
  3. Tap on "Go to 2nd screen" and then, tap on Messages tab. The popAllScreensOnTapOfSelectedTab will be fired returning to previous screen.
  4. Now, tap on back button (to the left of the Messages title) and then, tap on messages tab. The scrolled to top log will be printed but screen will be gone.

Code to reproduce the problem

import 'package:flutter/material.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Flutter Demo',
    debugShowCheckedModeBanner: false,
    routes: {
      "/home": (context) => const ScreenPage(title: "Home", contentMessage: "Home"),
      "/home/second": (context) => const ScreenPage(title: "Home 2nd", contentMessage: "Home 2nd"),

      "/messages": (context) => const ScreenPage(title: "Messages", contentMessage: "Messages"),
      "/messages/second": (context) => const ScreenPage(title: "Messages 2nd", contentMessage: "Messages 2nd"),

      "/settings": (context) => const ScreenPage(title: "Settings", contentMessage: "Settings"),
      "/settings/second": (context) => const ScreenPage(title: "Settings 2nd", contentMessage: "Settings 2nd"),
    },
    home: const MyHomePage(),
  );
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final globalKeys = [GlobalKey(), GlobalKey(), GlobalKey()];

  @override
  Widget build(BuildContext context) => PopScope(
    canPop: false,
    child: PersistentTabView(
      navBarHeight: 48,
      popAllScreensOnTapOfSelectedTab: true,
      tabs: [
        PersistentTabConfig(
          screen: ScreenPage(key: globalKeys[0], title: "Home", contentMessage: "Home 1"),
          item: ItemConfig(
            icon: Icon(Icons.home),
            title: "Home",
          ),

          navigatorConfig: NavigatorConfig(
            // initialRoute: "/home",
            navigatorObservers: [AnalyticsNavigatorObserver()]
          ),
          onSelectedTabPressWhenNoScreensPushed: () {
            (globalKeys[0].currentState as IScrollToTop).scrollToTop();
          },
        ),
        PersistentTabConfig(
          screen: ScreenPage(key: globalKeys[1], title: "Messages", contentMessage: "Messages"),
          item: ItemConfig(
            icon: Icon(Icons.message),
            title: "Messages",
          ),
          navigatorConfig: NavigatorConfig(
            initialRoute: "/messages",
            navigatorObservers: [AnalyticsNavigatorObserver()]
          ),
          onSelectedTabPressWhenNoScreensPushed: () {
            (globalKeys[1].currentState as IScrollToTop).scrollToTop();
          },
        ),
        PersistentTabConfig(
          screen: ScreenPage(key: GlobalKey(), title: "Settings", contentMessage: "Settings"),
          item: ItemConfig(
            icon: Icon(Icons.settings),
            title: "Settings",
          ),

          navigatorConfig: NavigatorConfig(
            initialRoute: "/settings",
            navigatorObservers: [AnalyticsNavigatorObserver()]
          ),
        ),
      ],
      handleAndroidBackButtonPress: true,
      popActionScreens: PopActionScreensType.all,
      backgroundColor: Theme.of(context).colorScheme.background,
      navBarBuilder: (config) => Style2BottomNavBar(navBarConfig: config),
    ),
  );
}

abstract class IScrollToTop {
  void scrollToTop();
}

class ScreenPage extends StatefulWidget  {
  final String title;
  final String contentMessage;

  const ScreenPage({super.key, required this.title, required this.contentMessage});

  @override
  State<ScreenPage> createState() => _ScreenPageState();
}

class _ScreenPageState extends State<ScreenPage> implements IScrollToTop {

  final ScrollController _scrollController = ScrollController();

  @override
  void scrollToTop() {
    _scrollController.animateTo(
      0,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut
    );
    print("scrolled to top");
  }

  @override
  void initState() {
    _scrollController.dispose();
    super.initState();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: SingleChildScrollView(
      controller: _scrollController,
      child: Container(
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(widget.contentMessage),
            if (!widget.title.endsWith("2nd"))
              TextButton(
                onPressed: () {
                  Navigator.of(context).pushNamed("/${widget.title.toLowerCase()}/second");
                },
                child: Text("Go to 2nd screen"),
              )
          ],
        ),
      ),
    ),
  );
}

class AnalyticsNavigatorObserver extends NavigatorObserver {

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print("didPush");
    print("new route: ${route.settings.name}");
    print("previous route: ${previousRoute?.settings.name}");
    print("------------------------------------------------");
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print("didPop");
    print("new route: ${route.settings.name}");
    print("previous route: ${previousRoute?.settings.name}");
    print("------------------------------------------------");
  }

  @override
  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print("didRemove");
    print("new route: ${route.settings.name}");
    print("previous route: ${previousRoute?.settings.name}");
    print("------------------------------------------------");
  }

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    print("didReplace");
    print("new route: ${newRoute?.settings.name}");
    print("previous route: ${oldRoute?.settings.name}");
    print("------------------------------------------------");
  }
}

Relevant log output

Restarted application in 724ms.
I/flutter ( 3180): didPush
I/flutter ( 3180): new route: /
I/flutter ( 3180): previous route: null
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): scrolled to top // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
I/flutter ( 3180): didPush
I/flutter ( 3180): new route: /
I/flutter ( 3180): previous route: null
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): didPush
I/flutter ( 3180): new route: /messages
I/flutter ( 3180): previous route: /
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): didPush
I/flutter ( 3180): new route: /messages/second
I/flutter ( 3180): previous route: /messages
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): didPop
I/flutter ( 3180): new route: /messages/second
I/flutter ( 3180): previous route: /messages
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): didPop
I/flutter ( 3180): new route: /messages
I/flutter ( 3180): previous route: /
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): scrolled to top
I/flutter ( 3180): didPop
I/flutter ( 3180): new route: /
I/flutter ( 3180): previous route: null
I/flutter ( 3180): ------------------------------------------------
I/flutter ( 3180): scrolled to top // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

════════ Exception caught by gesture ═══════════════════════════════════════════
The following assertion was thrown while handling a gesture:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack:
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4729:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4743:6)
#2      Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:4814:12)
#3      Navigator.of (package:flutter/src/widgets/navigator.dart:2685:40)
#4      _PersistentTabViewState.popAllScreens (package:persistent_bottom_nav_bar_v2/components/persistent_tab_view.dart:408:22)
#5      _PersistentTabViewState.navigationBarWidget.<anonymous closure> (package:persistent_bottom_nav_bar_v2/components/persistent_tab_view.dart:324:19)
#6      Style2BottomNavBar.build.<anonymous closure>.<anonymous closure> (package:persistent_bottom_nav_bar_v2/styles/style_2_bottom_navbar.dart:74:44)
#7      _InkResponseState.handleTap (package:flutter/src/material/ink_well.dart:1183:21)
#8      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:275:24)
#9      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:652:11)
#10     BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:309:5)
#11     BaseTapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:242:7)
#12     PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:630:9)
#13     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
#14     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9)
#15     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
#16     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
#17     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
#18     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:488:19)
#19     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:468:22)
#20     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:439:11)
#21     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:413:7)
#22     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:376:5)
#23     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:323:7)
#24     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:292:9)
#25     _invoke1 (dart:ui/hooks.dart:328:13)
#26     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:410:7)
#27     _dispatchPointerDataPacket (dart:ui/hooks.dart:262:31)

Handler: "onTap"
Recognizer: TapGestureRecognizer#5654c
    debugOwner: GestureDetector
    state: possible
    won arena
    finalPosition: Offset(207.0, 799.8)
    finalLocalPosition: Offset(67.5, 28.7)
    button: 1
    sent tap down
════════════════════════════════════════════════════════════════════════════════

Screenshots

No response

jb3rndt commented 5 months ago

Hi, thank you for reporting this and providing the code to reproduce. I successfully identified the bug and fixed it. It is available in version 5.2.3