１つのボタンをタップして上にスクロールしたり、前の画面に戻ったり

[Flutter Advent Calendar 2019](https://qiita.com/advent-calendar/2019/flutter)の14日目の記事です。

iOSアプリの[feather for Twitter](https://apps.apple.com/jp/app/feather-for-twitter/id793157344)のこの↓動きをFlutterでやってみます。
<img Src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17221/0a6a5b42-8276-1888-2425-f87303fd4b08.gif" width="40%">
これは左下のホームボタンを３回タップしています。
タップするたびに
1. 「画面の途中なのでトップまでスクロール」
2. 「トップにいるから一つ前の画面へ戻る」
3. 「画面の途中なのでトップまでスクロール」
を実行しています。

ホームボタンがある画面Widgetが親として存在して、その子孫にスクロールする画面が1つ以上存在するイメージでやっていきます。

ホームボタンがあるWidgetとスクロールするWidgetとは、親子よりも遠い関係なので今回のようにProviderを使った書き方をしています。

```dart:ホームボタンがある画面のWidget(Sample)
  // クラス内に宣言
  final StreamController _tapStreamController = StreamController.broadcast();
  Stream get tapStream => _tapStreamController.stream;

  @override
  Widget build(BuildContext context) {
    // Providerを挟んで子孫のどこでもタップイベントを受けられるように
    return SampleProvider(
      sampleState: this,
      child: ChildWidget(),// 子孫にスクロールしたい画面のWidgetが含まれていること
    );
  }
-----
  // タップイベントが発生するところに以下を記載
  // true → 今回はtrueであるかどうかに関わらないのでなんでも良い
  _tapStreamController.sink.add(true);

```

```dart:Providerを準備
// InheritedWidgetを使って、子孫のどこでもイベント通知を受けられるようにする
// Providerの書き方はベーシックな書き方です
class SampleProvider extends InheritedWidget {
  final Sample sampleState;

  const SampleProvider({
    Key key,
    this.sampleState,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(SampleProvider oldWidget) => true;

  static Sample of(BuildContext context) {
    final SampleProvider sampleProvider = context.ancestorWidgetOfExactType(SampleProvider);
    return sampleProvider.sampleState;
  }
}
```

```dart:スクロールしたい画面Widget
  // CustomListViewなどスクロースさせたいWidgetの
  // controllerに_scrollControllerをセットしてください
　final _scrollController = ScrollController();
  Stream tapStream;

  // 購読を解除する時のために変数に保持する
  StreamSubscription streamSubscription;

  @override
  void initState() {
    super.initState();

    // タップされたことを伝えるStream 
    tapStream = SampleProvider.of(context).tapStream;

    // タップされたらここが呼び出される
    // 今回はタップされたことだけ分かればいいので引数は使っていない
    streamSubscription = tapStream.listen((_) {

      // isCurrentで表示中の画面かどうかを判断
      // これがない場合同じStreamを購読している他の画面でもスクロールしてしまう
      // (もう少しいい風に書けないか..)
      if (context != null && ModalRoute.of(context).isCurrent) {

        // 一番上までスクロールしている場合は、画面を一つ戻る
        // これ以上戻るところがない画面の場合はpopは使わないようにする
        if (_scrollController.offset == 0.0) {
          Navigator.of(context).pop();

        } else {
          // アニメーションしながら一番上までスクロール
          _scrollController.animateTo(
            0.0,
            curve: Curves.easeOut,
            duration: const Duration(milliseconds: 300),
          );
        }
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    // 購読していたのを解除
    streamSubscription?.cancel();
    super.dispose();
  }
```
「スクロールしたい画面Widget」に書いてあることは、スクロールをさせたい画面それぞれに記載することで、それぞれをスクロールに対応させることができます。

**まとめ**

この書き方に至るまでなかなか頭を絞りましたが、出来上がりはシンプルでした。
正直まだInheritedWidget,Stream,Provider,Blocらへんは感覚で理解している感じです。うまく使えるときれいに書けたり考えたりできると思うので、ちゃんと理解して細かく説明できるようになりたいです。
普段あまりアウトプットしていないので良い機会となりました。

ありがとうございました。