LoginSignup
13
7

More than 1 year has passed since last update.

【Flutter】ExtendedNestedScrollViewを使用してタブ画面の実装をしてみた

Last updated at Posted at 2023-04-18

株式会社Neverの近藤です。

株式会社Neverはモバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にこちらへお問合せください。

今日は FlutterでExtendedNestedScrollViewを用いて、Twitterのようなプロフィール画面とタブ画面の実装 について解説します。

目次

1.成果物
2.概要
3.実装手順
4.まとめ
5.おわりに

成果物

概要

実装したプロフィール画面で実装したい要件

  • AppBarとTabBarの間にプロフィール画面を実装してスクロールした時にTabBarを上に固定
  • タブを切り替えてもスクロールした位置を状態を維持

最初はNestedScrollViewで実装を試みたのですが、下記の2点のissueもあり今回はこのissueを改善したExtendedNestedScrollViewを使用して実装しました。

実装手順

ExtendedNestedScrollViewの追加

extended_nested_scroll_viewpubspec.yamlに追加します。

dependencies:
  extended_nested_scroll_view: ^latest_version

スクロールした時にTabBarを上に固定

headerSliverBuilderに、画面上部のプロフィール情報を表示し、スクロール時にタブが上部に固定されるように設定されています。
また、TabBarを継続的に表示させるためにSliverPersistentHeaderを使用することで実装しました。
SliverPersistentHeaderの使用でAppBarとTabBarの間にプロフィール画面を実装も可能になります。

ExtendedNestedScrollView(
  // スクロール位置のすべてをまとめてスクロールするのを避ける
  onlyOneScrollInBody: true,
  // タブを上で固定するためにヘッダーの合計の高さを指定
  pinnedHeaderSliverHeightBuilder: () {
   return kToolbarHeight + MediaQuery.of(context).padding.top;
  },  
  headerSliverBuilder: (context, innerBoxIsScrolled) {
    return <Widget>[
      SliverAppBar(
        pinned: true, // スクロールした時に上にAppBarが表示されたままになる
        elevation: 0,
        title: Text(
          'プロフィール',
          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Colors.white,
              ),
        ),
      ),
      // プロフィール画面
      const SliverToBoxAdapter(
        child: _ProfileTile(
          userName: '株式会社Never',
          userId: '@never_inc',
          selfIntroduction:
              '株式会社Neverとはモバイルアプリケーションをメインに開発、運用をおこなっております。',
        ),
      ),
      // タブの情報
      SliverPersistentHeader(
        pinned: true,
        delegate: _StickyTabBarDelegate(
          TabBar(
            controller: _tabController,
              tabs:
                TabItem.values.map((e) => Tab(text: e.title)).toList(),
              indicatorColor: Colors.blue,
              indicatorSize: TabBarIndicatorSize.label,
              labelColor: Colors.blue,
              labelPadding: EdgeInsets.zero,
          ),
        ),
      ),
    ],
  },
  // タブごとにウィジェットを設定
  body: TabBarView(
    controller: _tabController,
    children: <Widget>[
      ListView.separated(
        key: PageStorageKey<String>(TabItem.post.title),
        itemCount: 100,
        itemBuilder: (BuildContext context, int index) {
          return _PostTile(
            index: index,
            userName: '株式会社Never',
            userId: '@never_inc',
          );
        },
        separatorBuilder: (context, _) {
          return const Divider(height: 1);
        },
      ),
      ListView.separated(
        key: PageStorageKey<String>(TabItem.favorite.title),
        itemCount: 100,
        itemBuilder: (BuildContext context, int index) {
          return _PostTile(
            index: index,
            userName: 'HOGEHOGE',
            userId: '@hoge_hoge',
          );
        },
        separatorBuilder: (context, _) {
          return const Divider(height: 1);
        },
      ),
    ],
  ),
),

※ _ProfileTileについてはの実装はこちら
※ _PostTileについてはの実装はこちら

_StickyTabBarDelegateについて

SliverPersistentHeaderDelegateを継承した_StickyTabBarDelegateクラスを使用して、TabBarの動作を制御します。このクラスでは、TabBarの高さを計算し、buildメソッドでTabBarを含むウィジェットを返します。
また、shouldRebuildでは、古いデリゲートと新しいデリゲートが異なる場合にのみウィジェットを再構築するように設定します。

class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  const _StickyTabBarDelegate(this.tabBar);

  final TabBar tabBar;

  @override
  double get minExtent => tabBar.preferredSize.height * 0.7;

  @override
  double get maxExtent => tabBar.preferredSize.height * 0.7;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    return ColoredBox(
      color: Theme.of(context).scaffoldBackgroundColor,
      child: tabBar,
    );
  }

  @override
  bool shouldRebuild(_StickyTabBarDelegate oldDelegate) {
    return tabBar != oldDelegate.tabBar;
  }
}

タブを切り替えてもスクロールした位置を状態を維持

ListViewPageStorageKeyを渡すことで、タブを切り替えるたびにスクロール位置を維持することができます。

ListView.separated(
  key: PageStorageKey<String>(TabItem.favorite.title),
  itemCount: 100,
  itemBuilder: (BuildContext context, int index) {
    return _PostTile(
      index: index,
      userName: '株式会社Never',
      userId: '@never_inc',
    );
  },
  separatorBuilder: (context, _) {
    return const Divider(height: 1);
  },
),

まとめ

ExtendedNestedScrollViewを使用してプロフィール画面を作成する方法を説明しました。
画面をスクロールした時にタブを上に固定して、タブを切り替えた際にスクロール位置が保持されるプロフィール画面を作成することができました。

今回のソースコードはこちらのGitHubにて公開してます。

おわりに

最後までお読みいただきありがとうございます。
間違っている点などあればコメントにて指摘いただけますと幸いです。

アプリ設計や実装でお困りの際はお気楽にお問合せください。

13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7