skip to Main Content

I have spent many hours researching this issue.
There are similar threads but I don’t think it is the same situation, so let me ask you a question.

What I want to do is use TabControler within the HookConsumerWidget to add a tab when the plus button is clicked and go to the last tab added.

But on startup and when the plus button is clicked I am getting the following error:

Controller’s length property (0) does not match the number of tabs (3)
present in TabBar’s tabs property.

  • 3 is the number of times the plus button is clicked.

The controller length does not seem to change from zero.

Here is the code:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class SampleListNotifier extends StateNotifier<List<String>> {
  SampleListNotifier() : super(<String>[]);

  // 値の操作を行う
  void createSample(String title) {
    state = [...state, title];
  }
}

final sampleListProvider = StateNotifierProvider<SampleListNotifier, List<String>>(
      (ref) => SampleListNotifier(),
);

class CustomTabController extends HookConsumerWidget {
  const CustomTabController({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final samples = ref.watch(sampleListProvider);
    final controller = useTabController(initialLength: samples.length);
    final index = useState(samples.length);
    final key = GlobalKey();

    controller.addListener(() {
      index.value = controller.index;
    });

    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        leading: IconButton(
            icon: const Icon(Icons.add),
            onPressed: () async {
              const result = 'test';
              if (result?.isNotEmpty ?? true) {
                ref.read(sampleListProvider.notifier).createSample(result!);
                controller.animateTo(samples.length-1);
              }
            }),
        title: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            TabBar(
              onTap: (index) {},
              controller: controller,
              isScrollable: true,
              tabs: samples
                  .map((String sample) => Tab(text: sample))
                  .toList(),
            ),
          ],
        ),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () async {
              print('pressed!');
            },
          ),
        ],
      ),
      body: TabBarView(
        key: key,
        controller: controller,
        children: samples
            .map((String sample) =>
            TabPage(key: UniqueKey() , title: sample))
            .toList(),
      ),
    );
  }
}

class TabPage extends StatelessWidget {
  const TabPage({required Key key, required this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Center(
        child: Text(title)
    );
  }
}

Thanks in advance.

2

Answers


  1. Chosen as BEST ANSWER

    I have been struggling with this for a long time and finally solved it. I am not sure yet if this is the right way to do it. Thank you very much for those who responded.

    But if anyone else has the same problem, please check this out. Also, if you have any problems with this method, please let me know.

    I changed to flutter_riverpod instead of flutter_hook during the solution.

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    void main() {
      runApp(const ProviderScope(child: MyApp()));
    }
    
    class TabContent {
      String title;
    
      TabContent.name(this.title);
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends ConsumerStatefulWidget {
      const MyHomePage({super.key});
    
      @override
      ConsumerState<MyHomePage> createState() => _MyHomePage();
    }
    
    class _MyHomePage extends ConsumerState<MyHomePage> with TickerProviderStateMixin {
      //List<TabContent> myList = [];
      final myListProvider = StateProvider<List<TabContent>>((ref) => []);
    
      late TabController _tabController;
      late TabPageSelector _tabPageSelector;
      int tabCount = 1;
    
      @override
      void initState() {
        super.initState();
    
        _tabController = TabController(vsync: this, length: 0);
    
        WidgetsBinding.instance.addPostFrameCallback((_) async {
          final notifier = ref.read(myListProvider.notifier);
          notifier.state = [...notifier.state, TabContent.name(tabCount.toString())];
    
          _tabController = TabController(vsync: this, length: notifier.state.length);
          _tabPageSelector = TabPageSelector(controller: _tabController);
    
        });
      }
    
      @override
      void dispose() {
        _tabController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final myList = ref.watch(myListProvider);
    
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.add),
              onPressed: () {
                tabCount++;
    
                final notifier = ref.read(myListProvider.notifier);
                notifier.state = [...notifier.state, TabContent.name(tabCount.toString())];
    
                setState(() {
                  _tabController =
                      TabController(vsync: this, length: notifier.state.length);
                  _tabPageSelector =
                      TabPageSelector(controller: _tabController);
                  _tabController.animateTo(notifier.state.length - 1);
                });
              },
            ),
            title: GestureDetector(
              onLongPress:() {
                print('long pressed!');
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [ TabBar(
                  controller: _tabController,
                  isScrollable: true,
                  tabs: myList.map((TabContent tab) {
                    return Tab(
                      text: tab.title,
                    );
                  }).toList(),
                ),
                ],
              ),
            ),
          ),
          body: TabBarView(
            controller: _tabController,
            children: myList.isEmpty
                ? <Widget>[]
                : myList.map((dynamicContent) {
              return TabPage(key: UniqueKey() , tab: dynamicContent);
            }).toList(),
          ),
        );
      }
    }
    
    class TabPage extends StatelessWidget {
      const TabPage({required Key key, required this.tab}) : super(key: key);
      final TabContent tab;
    
      @override
      Widget build(BuildContext context) {
        return Center(
            child: Text(tab.title)
        );
      }
    }
    

  2. Add Loader i.e CircularProgressIndicator() instead of Tabbar and TabBarView at the time of adding new tab into the list. then call controller.animateTo(samples.length-1) after loader stops

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search