flutterappbarflutter-ui

Scroll issue Flutter : Custom Sliver app bar and header with tab bar and Silver fill remain widget


Here is my code want to implement custom header which goes up when user scroll.

And stick tabbar to top and bottom view widget will show.. for that I user Sliver Widgets..

Here are some code snippets:

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

  @override
  State<ProfileMyGroupDetailsScreen> createState() => _ProfileMyGroupDetailsScreenState();
}

class _ProfileMyGroupDetailsScreenState extends State<ProfileMyGroupDetailsScreen> with SingleTickerProviderStateMixin {
  TabController? _tabController;

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _tabController = TabController(initialIndex: 0, length: 4, vsync: this);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: CustomChatAppBar(
        title: AppLabels.myGroupDetails,
        systemOverlayStyle: Global.whiteOverLay,
        actions: [],
      ),
      body: buildDataTabWidget(),
    );
  }

  DefaultTabController buildDataTabWidget() {
    return DefaultTabController(
      length: 4,
      child: CustomScrollView(
        slivers: [
          SliverToBoxAdapter(
            child: Column(
              children: [
                ListTileType1(
                  padding: padXY(24, 8.5),
                  imageUrl: AppImages.dummyProfileImage1,
                  imageWidth: 44,
                  imageHeight: 44,
                  title1: 'Meme team',
                  title2: 'Jack & Chris',
                ),
                Padding(
                  padding: padXY(24, 16),
                  child: CustomImage(
                    imageUrl: AppImages.dummyProfileImage1,
                    height: MediaQuery.of(context).size.width - (24 * 2),
                    width: MediaQuery.of(context).size.width,
                    fit: BoxFit.cover,
                    borderRadius: 12,
                  ),
                ),
              ],
            ),
          ),
          SliverPersistentHeader(
            pinned: true,
            delegate: _SliverAppBarDelegate(
              TabBar(
                controller: _tabController,
                dividerColor: AppColors.kBorderColor,
                dividerHeight: 2,
                isScrollable: true,
                padding: EdgeInsets.zero,
                tabAlignment: TabAlignment.center,
                indicatorSize: TabBarIndicatorSize.tab,
                indicatorWeight: 1,
                labelColor: AppColors.kSecondaryColor,
                unselectedLabelColor: AppColors.kAppGrey,
                labelStyle: context.bodyLarge.w500,
                unselectedLabelStyle: context.bodyLarge,
                onTap: (index) {
                  // moduleDetailController.selectedIndex.value = index;
                  // moduleDetailController.selectedIndex.refresh();
                },
                tabs: [
                  Tab(text: AppLabels.basicInformation),
                  Tab(text: AppLabels.myLikes),
                  Tab(text: AppLabels.matches),
                  Tab(text: AppLabels.sentRequests),
                ],
              ),
            ),
          ),
          SliverFillRemaining(
            child: TabBarView(
              controller: _tabController,
              children: [
                BuildBasicInfoTab(),
                MyLikeTab(),
                MatchesTab(),
                SentRequestTab(),
              ],
            ),
          )
        ],
      ),
    );
  }
}

And I made a custom delegate method to stick tab bar:

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate(this._tabBar);

  final TabBar _tabBar;

  @override
  double get minExtent => _tabBar.preferredSize.height;

  @override
  double get maxExtent => _tabBar.preferredSize.height;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(color: AppColors.kWhiteColor, child: _tabBar);
  }

  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return false;
  }
}

But main issue is that when tab bar pin to tap after that it scroll under to it.

And then not able to scroll down the content.

Please assist me how we can achieve this type of UI ?

enter image description here


Solution

  • as Salman said you have to use NestedScrollView, please refer for more details NestedScroll Widget

    import 'package:flutter/material.dart';
    
    class ProfileMyGroupDetailsScreen extends StatefulWidget {
    const ProfileMyGroupDetailsScreen({Key? key}) : super(key: key);
    
    @override
    State<ProfileMyGroupDetailsScreen> createState() => 
    _ProfileMyGroupDetailsScreenState();
    }
    
    class _ProfileMyGroupDetailsScreenState extends 
    State<ProfileMyGroupDetailsScreen> with SingleTickerProviderStateMixin {
    late TabController _tabController;
    
    @override
    void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
    }
    
    @override
    Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: CustomChatAppBar(
        title: AppLabels.myGroupDetails,
        systemOverlayStyle: Global.whiteOverLay,
        actions: [],
      ),
      body: buildDataTabWidget(),
    );
    }
    
    Widget buildDataTabWidget() {
    return NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return [
          SliverToBoxAdapter(
            child: Column(
              children: [
                ListTileType1(
                  padding: padXY(24, 8.5),
                  imageUrl: AppImages.dummyProfileImage1,
                  imageWidth: 44,
                  imageHeight: 44,
                  title1: 'Meme team',
                  title2: 'Jack & Chris',
                ),
                Padding(
                  padding: padXY(24, 16),
                  child: CustomImage(
                    imageUrl: AppImages.dummyProfileImage1,
                    height: MediaQuery.of(context).size.width - (24 * 2),
                    width: MediaQuery.of(context).size.width,
                    fit: BoxFit.cover,
                    borderRadius: 12,
                  ),
                ),
              ],
            ),
          ),
          SliverPersistentHeader(
            pinned: true,
            delegate: _SliverAppBarDelegate(
              TabBar(
                controller: _tabController,
                dividerColor: AppColors.kBorderColor,
                dividerHeight: 2,
                isScrollable: true,
                padding: EdgeInsets.zero,
                tabAlignment: TabAlignment.center,
                indicatorSize: TabBarIndicatorSize.tab,
                indicatorWeight: 1,
                labelColor: AppColors.kSecondaryColor,
                unselectedLabelColor: AppColors.kAppGrey,
                labelStyle: context.bodyLarge.w500,
                unselectedLabelStyle: context.bodyLarge,
                tabs: [
                  Tab(text: AppLabels.basicInformation),
                  Tab(text: AppLabels.myLikes),
                  Tab(text: AppLabels.matches),
                  Tab(text: AppLabels.sentRequests),
                ],
              ),
            ),
          ),
        ];
      },
      body: TabBarView(
        controller: _tabController,
        children: [
          BuildBasicInfoTab(),
          MyLikeTab(),
          MatchesTab(),
          SentRequestTab(),
         ],
       ),
      );
     }
    }
    
    class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
    _SliverAppBarDelegate(this._tabBar);
    
    final TabBar _tabBar;
    
    @override
    double get minExtent => _tabBar.preferredSize.height;
    
    @override
    double get maxExtent => _tabBar.preferredSize.height;
    
    @override
    Widget build(BuildContext context, double shrinkOffset, bool 
    overlapsContent) {
    return Container(color: AppColors.kWhiteColor, child: _tabBar);
    }
    
    @override
    bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return false;
     }
    }