【Flutter】任意の場所まで自動スクロール(後編)

# 概要
Flutterで縦に長いページを作成する場合、一番上まで自動でスクロールさせたいユースケースがあると思います。
例えば、Twitter, Instagram, Facebook, Slackなど、そうそうたるアプリは、AppBarを押すと最上部に遷移する実装がなされています。

本記事では、それらのアプリが実現している【AppBarを押すと画面最上部まで自動でスクロール】の実装内容を整理してみました。
後編のみご覧いただくだけでも問題ありませんが、実現にはいくつかの要素を含むため、
本記事の内容が難解であると感じた場合、まずは[前編](https://qiita.com/ko-izumi/items/00bc40d0ee8bc5118f2f)をご覧いただくことをおすすめします。


[前編](https://qiita.com/ko-izumi/items/00bc40d0ee8bc5118f2f)に引き続き、今回も【[scroll_to_index](https://pub.dev/packages/scroll_to_index)】を利用します。

スクロールについては、前後編で以下の内容で整理しています。

 | 内容
---|---
[前編](https://qiita.com/ko-izumi/items/00bc40d0ee8bc5118f2f)|【同一Widget内】で、任意のスクロール場所へ遷移する方法
後編(本記事)|【表示と異なるWidget】で、任意のスクロール場所へ遷移する方法<br>※具体的にはAppBarを押すとページ最上部に遷移する

# 実装イメージ
AppBarをタップしたら、画面最上部に遷移するデモとなります。
(1回目は23まで手動でスクロール、その後AppBarタップ)
(2回目は99まで手動でスクロール、その後AppBarタップ)
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1424230/6852951e-7623-7b09-4f31-590c24099087.gif)



# 環境

```shell-session
$ flutter --version
Flutter 2.5.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision ffb2ecea52 (3 weeks ago) • 2021-09-17 15:26:33 -0400
Engine • revision b3af521a05
Tools • Dart 2.14.2
```

pubspec.yaml

```yaml:
dependencies:
  # ✨✨✨↓追加✨✨✨
  scroll_to_index: ^2.1.0
  hooks_riverpod: ^1.0.0-dev.10
```

# 実装
ポイントは、以下の3つです。

1. [スクロールのcontrollerの状態管理](#スクロールのcontrollerの状態管理)
1. [AppBar押下のコールバック取得(+スクロールイベント発火)](#appbar押下のコールバック取得スクロールイベント発火)
1. [スクロールさせたい画面表示](#スクロールさせたい画面表示)

順に説明します。注意してほしい箇所はコメントアウトに記載しているので、その辺りも見ていただければと。
[GitHub](https://github.com/ko-izumi/scroll_to_jump/blob/master/lib/main.dart)にも全文公開しているのでご参考まで

##スクロールのcontrollerの状態管理
本記事の状態管理はriverpodを採用しています。
riverpodあるあるとして、runAppのchildに【ProviderScope】をかませることを忘れないように注意してください。
※筆者はよく忘れます。自分への戒めとして明記しました。

```dart:
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

/// scroll用のproviderを宣言
final scrollControllerProvider = Provider((_) => AutoScrollController());

void main() {
  runApp(
    /// ✨✨✨ProviderScopeは、かなり忘れがちなので注意✨✨✨
    const ProviderScope(
      child: MyApp(),
    ),
  );
}
```

##AppBar押下のコールバック取得(+スクロールイベント発火)
デフォルトのAppBarは、Tap検出できないため、AppBarをカスタムします。

```dart:
/// ✨✨✨デフォルトのAppBarは、Tap検出できないため、AppBarをカスタムする(GestureDetectorをかませた)
class ScrollAppBar extends StatelessWidget implements PreferredSizeWidget {
  final VoidCallback onTap;
  final AppBar appBar;

  const ScrollAppBar({Key? key, required this.onTap, required this.appBar})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(onTap: onTap, child: appBar);
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
```

本クラスの呼び出し方法は、以下をご覧ください。
また、AppBarが押下(onTapがコール)されたら、先ほど宣言したcontrollerに対して、最上部へスクロールさせる実装します。
※ref.watchは必ずbuildメソッド内で実行してください。これも忘れがち。

```dart:
class MyApp extends HookConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// riverpodからcontrollerを参照
    final controller = ref.watch(scrollControllerProvider);

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        /// カスタムAppBarの呼び出し
        appBar: ScrollAppBar(
          appBar: AppBar(
            title: const Text('Sample of Scroll to Jump'),
          ),
          onTap: () {
            /// ボタンを押したら先頭(=ListViewの0番目)にジャンプできる。
            /// AutoScrollPosition.beginがListのindexの頭に表示される。他に、middleとendが存在する
            controller.scrollToIndex(
              0,
              preferPosition: AutoScrollPosition.begin,
            );

            /// なお、【scroll_to_index】は、FlutterデフォルトAPIのScrollControllerを継承しているため、
            /// 前編にてジャンプ使用不可と記載しましたが、先頭に遷移させるだけであれば、実はjumpToが簡単に使えます。
            /// TwitterやInstagramは、アニメーション方式を採用している、且つユーザ体験もanimationの方が好ましいため、今回はコメントアウトしますが、お知りおきを。
            // controller.jumpTo(0);
          },
        ),

        /// 呼び出し元のWidget
        body: const ScrollWidget(),
      ),
    );
  }
}
```

##スクロールさせたい画面表示
こちらは、ほぼ[前編](https://qiita.com/ko-izumi/items/00bc40d0ee8bc5118f2f)と同様の内容になっています。
変更点としては、

- AppBarでスクロール動作させるようになったので、ListView内のボタンを削除
- スクロール用のcontrollerをriverpodから参照

となります。
※ref.watchは必ずbuildメソッド内で実行してください。これも忘れがち。

```dart:
class ScrollWidget extends HookConsumerWidget {
  const ScrollWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// riverpodからcontrollerを参照
    final controller = ref.watch(scrollControllerProvider);

    return ListView.builder(
      controller: controller,

      /// とりあえず100個だけ表示するように実装
      itemCount: 100,
      itemBuilder: (context, index) {
        /// AutoScrollTagをかませる
        return AutoScrollTag(
          key: ValueKey(index),
          controller: controller,
          index: index,
          child: Column(
            children: [
              SizedBox(
                width: double.infinity,
                height: 80,

                /// 行頭に遷移されることがわかりやすいようにCardウィジェットを採用しています。適宜変更してください。
                child: Card(
                  child: Text(
                    '$index',
                    style: const TextStyle(fontSize: 24),
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}
```

#おまけ
お気づきの方もいらっしゃるかもしれませんが、画面最上部に持ってくるだけなら、scroll_to_indexのプラグインは正直不要です。
ただし、任意の場所まで遷移させることを考えた時に、拡張性があるという点でscroll_to_index使えば良いという点で採用しています。
※前後編の繋がりも考えて、採用したというのも大いにあったりしますが、、、