Flutter web tabbarview scrollcontroller not responding to keyboard scrolling

Issue

  • I’ve created two tabs.
  • In each tab I have SingleChildScrollView wrapped with Scrollbar.
  • I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
  • For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
  • For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
  • But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn’t respond to keyboard page up /down keys.

Please check my code below. Guide me on how to achieve keyboard scrolling for Tab TWO.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TabExample(),
    );
  }
}

class TabExample extends StatefulWidget {
  const TabExample({Key key}) : super(key: key);

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

class _TabExampleState extends State<TabExample> {
  ScrollController _scrollController;

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

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: [
              Tab(icon: Text('Tab ONE')),
              Tab(icon: Text('Tab TWO')),
            ],
          ),
          title: Text('Tabs Demo'),
        ),
        body: TabBarView(
          children: [
            _buildWidgetA(),
            _buildWidgetB(),
          ],
        ),
      ),
    );
  }

  Widget _buildWidgetA() {
    List<Widget> children = [];
    for (int i = 0; i < 20; i++) {
      children.add(
        Padding(
          padding: EdgeInsets.symmetric(vertical: 16),
          child: Container(
            height: 100,
            width: double.infinity,
            color: Colors.black,
          ),
        ),
      );
    }
    return Scrollbar(
      isAlwaysShown: true,
      showTrackOnHover: true,
      child: SingleChildScrollView(
        child: Column(
          children: children,
        ),
      ),
    );
  }

  Widget _buildWidgetB() {
    List<Widget> children = [];
    for (int i = 0; i < 20; i++) {
      children.add(
        Padding(
          padding: EdgeInsets.symmetric(vertical: 16),
          child: Container(
            height: 100,
            width: double.infinity,
            color: Colors.green,
          ),
        ),
      );
    }
    return Scrollbar(
      controller: _scrollController,
      isAlwaysShown: true,
      showTrackOnHover: true,
      child: SingleChildScrollView(
        controller: _scrollController,
        child: Column(
          children: children,
        ),
      ),
    );
  }
}

Solution

You don’t need to create an explicit ScrollController to achieve this.

One trick is to change which SingleChildScrollView is going to use the PrimaryScrollController whenever the Tab changes it’s index.

So, when we listen that tab has changed to index 0, we will set that the first SingleChildScrolView is the primary one. When it changes to 1, we will set the other on as primary.

First create a new State variable like this,

int currentIndex = 0; // This will be the index of tab at a point in time

To listen to the change event, you need to add Listener to the TabController.

DefaultTabController(
  length: 2,
  child: Builder(  // <---- Use a Builder Widget to get the context this this DefaultTabController
    builder: (ctx) {

      // Here we need to use ctx instead of context otherwise it will give null
      final TabController tabController = DefaultTabController.of(ctx);

      tabController.addListener(() {
        if (!tabController.indexIsChanging) {

          // When the tab has changed we are changing our currentIndex to the new index
          setState(() => currentIndex = tabController.index);
        }
      });

      return Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: [
              Tab(icon: Text('Tab ONE')),
              Tab(icon: Text('Tab TWO')),
            ],
          ),
          title: Text('Tabs Demo'),
        ),
        body: TabBarView(
          children: [
            _buildWidgetA(),
            _buildWidgetB(),
          ],
        ),
      );
    },
  ),
);

Finally, depending on the currentIndex set primary: true to each SingleChildScrollView.

For _buildWidgetA,

Scrollbar(
  isAlwaysShown: true,
  showTrackOnHover: true,
  child: SingleChildScrollView(
    primary: currentIndex == 0,  // <--- This will be primary if currentIndex = 0
    child: Column(
      children: children,
    ),
  ),
);

For _buildWidgetB,

Scrollbar(
  isAlwaysShown: true,
  showTrackOnHover: true,
  child: SingleChildScrollView(
    primary: currentIndex == 1,  // <--- This will be primary if currentIndex = 1
    child: Column(
      children: children,
    ),
  ),
);

Now, you should be able to control both of the tabs with your keyboard.

Full code here

Answered By – Nisanth Reddy

Answer Checked By – Senaida (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *