Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bottom navbar item list is not growable. #156

Open
RB-93 opened this issue May 7, 2024 · 4 comments
Open

Bottom navbar item list is not growable. #156

RB-93 opened this issue May 7, 2024 · 4 comments

Comments

@RB-93
Copy link

RB-93 commented May 7, 2024

Version

5.2.1

Flutter Doctor Output

PS C:\Users\rohit\VSCodeProjects\kisanlight> flutter doctor -v
[√] Flutter (Channel stable, 3.19.5, on Microsoft Windows [Version 10.0.19045.4355], locale en-IN)
    • Flutter version 3.19.5 on channel stable at C:\Users\rohit\Documents\Flutter SDK\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 300451adae (6 weeks ago), 2024-03-27 21:54:07 -0500
    • Engine revision e76c956498
    • Dart version 3.3.3
    • DevTools version 2.31.1

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at C:\Users\rohit\AppData\Local\Android\sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
    • Java version OpenJDK Runtime Environment (build 17.0.9+0--11185874) 
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[X] Visual Studio - develop Windows apps
    X Visual Studio not installed; this is necessary to develop Windows apps.
      Download at https://visualstudio.microsoft.com/downloads/.
      Please install the "Desktop development with C++" workload, including all of its default components

[√] Android Studio (version 2023.2)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0--11185874)

[√] VS Code (version 1.89.0)
    • VS Code at C:\Users\rohit\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.88.0

[√] Connected device (4 available)
    • Moto Z2 Play (mobile) • 192.168.29.253:5555 • android-arm    • Android 8.0.0 (API 26)
    • Windows (desktop)     • windows             • windows-x64    • Microsoft Windows [Version 10.0.19045.4355]
    • Chrome (web)          • chrome              • web-javascript • Google Chrome 124.0.6367.119
    • Edge (web)            • edge                • web-javascript • Microsoft Edge 124.0.2478.80

[√] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

What platforms are you seeing the problem on?

Android

What happened?

On a fresh run (reinstall with data clear) with this pkg build,
It shows red screen with "cannot add to a fixed-length list" error.

My app use case:
I have maintained bottom bar widget and kept a toggling boolean to one of the tab option to hide or show it on UI
which is listening values using Provider state management.

I tried to set growable flag to true in addAll() method (PFA 1)

and here too (PFA 2)

But after this it throws this exception
could be because focusNodes index doesn't get updated with provider updated values of tablist.

════════ Exception caught by widgets library ═══════════════════════════════════ RangeError (index): Invalid value: Not in inclusive range 0..1: 2 The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════

The list of tab options should support 'growable' list functionality, so that Tab list can handled dynamically in the code.

Steps to reproduce

Run provided code with
where any one option should be handled
with a boolean flag in the Firebase Realtime database
listened using Provider state management.

Code to reproduce the problem

PersistentTabView code

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

  
  @override
  State<MainScreen> createState() => _MainScreenState();

  //final BottomTabsController controller;
}

class _MainScreenState extends State<MainScreen> {
@override
  Widget build(BuildContext context) {
    // final controller = context.read<BottomTabsController>();
    return Consumer2<MasterControlModel, BottomTabsController>(
            builder: (context, master, tabsController, child) {
              context.read<BottomTabsController>().initScreens(
                  showWebinar:
                      context.read<MasterControlModel>().showWebinarTile ??
                          false);
              return PopScope(
                canPop: false,
                onPopInvoked: (didPop) {
                  showDialog(
                    context: context,
                    builder: (context) => Dialog(
                      child: Center(
                        child: ElevatedButton(
                          child: const Text("Close"),
                          onPressed: () {
                            Navigator.pop(context);
                          },
                        ),
                      ),
                    ),
                  );
                },
                child: PersistentTabView(
                  //backgroundColor: white,
                  /* navBarOverlap: NavBarOverlap.custom(
                      overlap: navBarDecoration.exposedHeight()), */
                  //avoidBottomPadding: false,
                  controller: tabsController.tabController,
                  margin: settings.margin,
                  stateManagement: settings.stateManagement,
                  hideNavigationBar:
                      !context.watch<BottomTabsController>().showBottonTabbar,
                  tabs: tabsController.tabs.map(
                    (tabItem) {
                      return PersistentTabConfig(
                        screen: tabItem.screen,
                        item: ItemConfig(
                          icon: tabItem.icon,
                          title: loaded ? tabItem.name.tr() : '',
                          activeForegroundColor: white,
                          activeColorSecondary: Colors.transparent,
                          inactiveForegroundColor:
                              Colors.white.withOpacity(0.5),
                          textStyle: GoogleFonts.poppins(
                            color: white,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      );
                    },
                  ).toList(),
                  navBarBuilder: (navBarConfig) => settings.navBarBuilder(
                    navBarConfig,
                    navBarDecoration,
                    const ItemAnimation(),
                    const NeumorphicProperties(),
                  ),
                ),
              );
            },
          )
}


-------------------
BottomTabController code

class BottomTabsController extends ChangeNotifier {
  PersistentTabController tabController = PersistentTabController();

  bool showBottonTabbar = true;

  void initScreens({bool showIOS = true}) {
    _tabseq.clear();
    _tabseq.addAll([
      KTab.home,
      if (showIOS) KTab.webinar,
      KTab.shortlisted,
      // KTab.windows,
    ]);
  }

  final List<KTab> _tabseq = [];

  List<BottomTab> get tabs {
    List<BottomTab> tablist = [];

    for (final val in _tabseq) {
      tablist.add(BottomTab.fromEnum(val));
    }

    return tablist;
  }

  KTab get currentTab {
    return _tabseq[tabController.index];
  }

  void changePage(KTab tab) {
    if (tab == currentTab) {
      return;
    }

    final tabIndex = _tabseq.indexOf(tab);

    tabController.jumpToTab(tabIndex);
  }

  void drawerChanged(bool isOpened) {
    if (isOpened) {
      showBottonTabbar = false;
    } else {
      showBottonTabbar = true;
    }
    notifyListeners();
  }
}

class BottomTab extends Equatable {
  final Widget icon;
  final String name;
  final KTab tabEnum;

  final Widget screen;
  const BottomTab({
    this.icon,
    required this.name,
    required this.tabEnum,
    required this.screen,
  });

  factory BottomTab.fromEnum(KTab val) {
    switch (val) {
      case KTab.android:
        {
          return const BottomTab(
              // icon: Icon(Icons.home_rounded),
              name: 'Android',
              tabEnum: KTab.android,
              screen: AndroidScreen());
        }
      case KTab.ios:
        {
          return const BottomTab(
            // icon: Icon(Icons.videocam_rounded),
            name: 'iOS',
            tabEnum: KTab.ios,
            screen: IOSScreen(),
          );
        }
      case KTab.web:
        {
          return const BottomTab(
            // icon: Icon(Icons.favorite),
            name: 'Web',
            tabEnum: KTab.shortlisted,
            screen: WebScreen(),
          );
        }
      case KTab.windows:
        {
          return const BottomTab(
            // icon: Icon(Icons.phone_callback_rounded),
            name: 'Windows',
            tabEnum: KTab.windows,
            screen: WindowsScreen(),
          );
        }
    }
  }

  @override
  List<Object?> get props => [tabEnum];
}

enum KTab {
  android,
  ios,
  web,
  windows,
}

The issue is in 
initScreen Method when the tabs are added using addAll() > PersistentTabViewScaffold (shouldBuildTab.addAll())

Relevant log output

After making changes in internal classes > made list to growable
as shown in attached screenshots

════════ Exception caught by widgets library ═══════════════════════════════════
RangeError (index): Invalid value: Not in inclusive range 0..1: 2
The relevant error-causing widget was:
════════════════════════════════════════════════════════════════════════════════

Screenshots

(PFA 1)
image

(PFA 2)
image

@jb3rndt
Copy link
Owner

jb3rndt commented May 26, 2024

Hi, thank you for opening this issue.
You are right, dynamically chaning the number of tabs is not supported as of now. I assumed that a user expects a persistent bottom navigation bar to be static and not change while the app is running. Thus, changing the content of the navigation bar results in bad UX (see official Material 3 docs).
Do you disagree or have a justifying use case to allow that?

@lukehutch
Copy link

I also need the ability to dynamically change the number of tabs in my app.

I assumed that a user expects a persistent bottom navigation bar to be static and not change while the app is running.

It was my expectation that the tab bar should indeed be persistent, i.e. not need rebuilding from scratch ever, while also supporting dynamically changing numbers of tabs.

It is standard practice in Flutter to allow different numbers of items in any sort of container between different calls to build, and the PersistentTabView API basically looks like the ListView.builder API, in other words, requiring the user to specify the number of items, and the content of each item, each time build is called.

In ListView, if items are deleted or inserted into the list, the widgets that are rendered may be structurally identical, so you may need to use a ValueKey for the updates to actually be detected and rendered. Nevertheless, the builder doesn't care about how many items were in the list last time build was called.

With PersistentBottomNavBarV2, a lot of things assume the number of tabs never changes, so I got all sorts of errors (mostly ! used on a null value, but I think also an array index out of bounds issue) when I tried changing the number of tabs dynamically. I was starting to work through all these issues to fix them, and instead, I ended up wrapping the whole PersistentTabView in a ValueListenableBuilder, also assigning a new unique key in PersistentTabView every time the ValueListenableBuilder was rebuilt, to force the entire PersistentTabView to be torn down and rebuilt. This is inefficient, but it's the only solution to the problem I could come up with.

@jb3rndt
Copy link
Owner

jb3rndt commented Jun 2, 2024

Yes it will indeed need quite some amount of work to do that, so that will probably take some time, sorry... I'll try looking into fixing that.
But to be honest I am still concerned about the UX aspect, so personally I would advise against changing the tabs dynamically.

@lukehutch
Copy link

UX is always contextual. In my app, continuing to show a tab for a feature that the user just manually disabled doesn't make any sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants