11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FlutterAdvent Calendar 2019

Day 14

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

Last updated at Posted at 2019-12-13

Flutter Advent Calendar 2019の14日目の記事です。

iOSアプリのfeather for Twitterのこの↓動きをFlutterでやってみます。

これは左下のホームボタンを3回タップしています。
タップするたびに

  1. 「画面の途中なのでトップまでスクロール」
  2. 「トップにいるから一つ前の画面へ戻る」
  3. 「画面の途中なのでトップまでスクロール」
    を実行しています。

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

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

ホームボタンがある画面の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);

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;
  }
}
スクロールしたい画面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らへんは感覚で理解している感じです。うまく使えるときれいに書けたり考えたりできると思うので、ちゃんと理解して細かく説明できるようになりたいです。
普段あまりアウトプットしていないので良い機会となりました。

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

11
6
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
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?