Dynamic scrolling of NestedScrollView based on the content of TabBarView

129

TabBarView requires finite height while wrapping with scrollable widget and on others cases all tabs become scrollable. Also trying with IndexedStack provide the same behavior.

I am not using TabBarView.

I am loading widgets for tabs inside initState and just passing inside body.

Run on dartPad.

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  final length = 8;

  late final TabController controller;
  final List<String> _tabs = <String>['Tab 1', 'Tab 2', 'Tab 3'];

  List<Widget> tabViews = [];

  @override
  void initState() {
    controller = TabController(
      length: _tabs.length,
      vsync: this,
    )..addListener(() {
        setState(() {});
      });

    tabViews = List.generate(
        _tabs.length,
        (index) => Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                ...List.generate(
                  index * 3 + 2,
                  (itb) => Container(
                    alignment: Alignment.center,
                    height: 100,
                    width: double.infinity,
                    color: Color(Random().nextInt(0xffffffff)),
                    child: Text("Tab: $index item  $itb"),
                  ),
                )
              ],
            ));
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Column(
              children: [
                ...List.generate(
                  5,
                  (index) => Container(
                    height: 50,
                    color: Colors.deepPurple,
                    width: double.infinity,
                    padding: const EdgeInsets.all(10),
                    child: Text(" top item $index"),
                  ),
                ),
              ],
            ),
            Container(
              color: Colors.primaries.first,
              height: kToolbarHeight,
              child: TabBar(
                tabs: _tabs.map((e) => Text(e)).toList(),
                controller: controller,
              ),
            ),
            tabViews[controller.index],
          ],
        ),
      ),
    );
  }
}
Share:
129
happy_san
Author by

happy_san

Github: https://github.com/happy-san

Updated on January 02, 2023

Comments

  • happy_san
    happy_san over 1 year

    I've got a screen having some content on top of a TabBar.

    enter image description here

    Both the content above TabBar and in TabBarView can be of dynamic height. My use case is that the upper content should only be scrollable when all of the content is not visible and only up to the point that all of it becomes visible and not beyond that. So in the following example, only Tab 1 should be scrollable.

    dartpad

    Setting the scrollphysics to NeverScrollableScrollPhysics wouldn't work since I can't determine the scroll behavior beforehand because of the dynamic height of the contents. Using SliverAppBar also doesn't work for the same reason.

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const HomePage(),
        );
      }
    }
    
    class HomePage extends StatelessWidget {
      final length = 5;
    
      const HomePage({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        final List<String> _tabs = <String>['Tab 1', 'Tab 2', 'Tab 3'];
        return DefaultTabController(
          length: _tabs.length,
          child: Scaffold(
            body: NestedScrollView(
              headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
                return <Widget>[
                  SliverOverlapAbsorber(
                    handle:
                        NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                    sliver: SliverToBoxAdapter(
                      child: Column(
                        children: [
                          Container(
                            width: double.infinity,
                            alignment: Alignment.center,
                            child: Column(
                              children: [
                                const Text('Upper Content'),
                                ListView.builder(
                                  shrinkWrap: true,
                                  itemCount: length,
                                  itemBuilder: (_, __) => Container(
                                    padding: const EdgeInsets.all(5),
                                    alignment: Alignment.center,
                                    child: const Text('Items'),
                                  ),
                                )
                              ],
                            ),
                          ),
                          Container(
                            color: Colors.blue,
                            child: TabBar(
                              tabs: _tabs
                                  .map(
                                    (String name) => Tab(
                                      text: name,
                                    ),
                                  )
                                  .toList(),
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ];
              },
              body: TabBarView(
                children: _tabs.map((String name) {
                  return name.split(' ')[1] != '3'
                      ? SafeArea(
                          top: false,
                          bottom: false,
                          child: Builder(
                            builder: (BuildContext context) {
                              return CustomScrollView(
                                key: PageStorageKey<String>(name),
                                slivers: <Widget>[
                                  SliverOverlapInjector(
                                    handle: NestedScrollView
                                        .sliverOverlapAbsorberHandleFor(context),
                                  ),
                                  SliverPadding(
                                    padding: const EdgeInsets.all(8.0),
                                    sliver: SliverFixedExtentList(
                                      itemExtent: 48.0,
                                      delegate: SliverChildBuilderDelegate(
                                        (BuildContext context, int index) {
                                          return ListTile(
                                            title: Text('Item $index'),
                                          );
                                        },
                                        childCount:
                                            name.split(' ')[1] != '2' ? 15 : 5,
                                      ),
                                    ),
                                  ),
                                ],
                              );
                            },
                          ),
                        )
                      : Container(
                          height: 50,
                          width: 50,
                          color: Colors.yellow,
                        );
                }).toList(),
              ),
            ),
          ),
        );
      }
    }