Flutter Advent Calendar 2019の14日目の記事です。
iOSアプリのfeather for Twitterのこの↓動きをFlutterでやってみます。
これは左下のホームボタンを3回タップしています。
タップするたびに
- 「画面の途中なのでトップまでスクロール」
- 「トップにいるから一つ前の画面へ戻る」
- 「画面の途中なのでトップまでスクロール」
を実行しています。
ホームボタンがある画面Widgetが親として存在して、その子孫にスクロールする画面が1つ以上存在するイメージでやっていきます。
ホームボタンがあるWidgetとスクロールするWidgetとは、親子よりも遠い関係なので今回のようにProviderを使った書き方をしています。
// クラス内に宣言
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);
// 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;
}
}
// 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らへんは感覚で理解している感じです。うまく使えるときれいに書けたり考えたりできると思うので、ちゃんと理解して細かく説明できるようになりたいです。
普段あまりアウトプットしていないので良い機会となりました。
ありがとうございました。