この記事はFlutterKaigiで発表した動画の書き起こしです。動画の方がわかりやすい部分もあると思うので、ご興味のある方はそちらもご覧ください。
こんにちは、本日お話しさせていただく塚本武志と申します。
はじめに
Flutter でスクロールできる Widget といえば多くの場合 ListView が一番に浮かぶと思いますが、ある程度以上に複雑なレイアウトを組もうと思って調べると Sliver というキーワードを目にすることが多いです。
Flutter はサンプルコードが豊富にあるのでそれらを参考にするだけでもかなりのケースに対応できますが、Sliver についてきちんと理解することでより適切な実装ができるようになったり既存の Widget だけでは対応できないようなオリジナル性の高いレイアウトが作れるようになるはずです。
今日はちょっととっつきづらい Sliver という概念についてできるだけわかりやすく説明してみようと思います。
そもそもスクロールできるレイアウトって作るの難しいよね
Sliver の詳細な説明に入る前に、そもそもスクロールするレイアウトを作るのって難しいよねっていう話から、Flutter がスクロールするレイアウトをどうやって実現しているかを見ていきたいと思います。
スクロールするレイアウトはアプリにとって必須とも言える要素ですが、他のレイアウトにはない特徴をいくつか持っています。その中でも重要なものを 3 つ挙げると、
まずはユーザーのジェスチャーに応じて表示するコンテンツが変わっていくこと、これは当然ですね。当然すぎて意識することはほとんどないと思いますが、他のレイアウトと比べて重要な特徴だと思います。
次に無限大の長さのコンテンツを持つことができること。Twitter のタイムラインを考えるとわかりやすいかなと思いますが、スクロールすることでいくらでもコンテンツを表示することができます。
そして、一度に表示されるのはコンテンツのうち一部であるということ。タイムラインにはたくさんのツイートが並んでいますが、スマホの画面に一度に表示されるのはせいぜい 2~3 ツイートだと思います。後ほど詳しく説明しますが、こういったスクロールするレイアウトの中で現在画面に表示されている範囲のことをviewPort
と呼びます。CSS などでもお馴染みの用語ですね。
スクロールできるレイアウトを作ろうとするとこれらのことを考慮しなくてはいけません。特に問題になるのは 2 番目のコンテンツの長さが無限大というところです。Flutter のレンダリングはネイティブと比べても遜色のないパフォーマンスがありますが、それでも 1 万個の投稿を一気に表示するのは骨が折れる作業です。
先述したようにそもそも表示するべきものはほんの一部なので、見えてない部分も含めてあらかじめ全てのコンテンツを描画しておくのはメモリと CPU の無駄遣いです。
Flutter はスクロールできるレイアウトをどうやって実現しているか
さて、ここまではスクロールするレイアウトに関する一般的なことをお話しましたが、具体的に Flutter がどうやってこれを実現しているかについて見ていきましょう。 Flutter にはスクロールに関する Widget がたくさんありますが、中でも重要なものは以下の 3 つです。
Scrollable
Sliver
ViewPort
ようやく今日の本題であるSliver
が出てきましたが、まずはScrollable
から見ていきましょう。
この Widget は主にユーザーのジェスチャーとviewPort
の管理を担当しています。簡単に言ってしまうとユーザーのジェスチャーを検知してコンテンツのどのあたりを表示するかを決める Widget です。この Widget はあくまでviewPort
の位置を決めるだけでそれ以外のことは何もしません。
Scrollable
によって決まったスクロール位置はViewPort
Widget に渡され、そこで実際のレンダリングが行われます。
このViewPort
Widget は、Scrollable
から現在のスクロール位置が与えられる他に、なにをどう表示するかを指定することができます。
return Viewport(
offset: offset,
slivers: slivers,
// ...
);
このために渡されるのが、今回の主役であるところのSliverWidget
です。つまりSliver
はスクロールするレイアウトの中で、特定のスクロール位置に対して「なにを、どうレイアウトするか」という部分に関する役割を担っている概念です。
ここで一つ補足として、実は Flutter にSliver
やSliverWidget
という名前のクラスはありません。実装パターンとして概念的にsliver protocol
というキーワードが登場するだけです。この掴み所のなさが Sliver の難しさの一因かもしれませんね。
今日は便宜的にSliver
として使われる Widget をSliverWidget
と呼びたいと思います。
では、Sliver の役割がわかったところで、実際にどんな Sliver があるか見ていきましょう。
Flutter にはいくつもSliverWidget
がありますが、もっともポピュラーなものはSliverList
です。使い方は以下の通りです。
CusomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(
[
const Text('Hello!'),
const Text('Sliver'),
const Text('List'),
]
),
),
],
),
CustomScrollView
という Widget については後述するので、ここではSliverList
の中身に注目しましょう。
delegate
というパラメーターにSliverChildListDelegate
というクラスが指定されています。
delegate
とは日本語に訳すと「委譲」で、さらに噛み砕いて言うと自分の役割などを他の人に任せることです。Swift や Kotlin でもよく使われる概念なのでネイティブアプリ開発経験のある方なら少し馴染みがあるかもしれません。
ではSliverList
はなにを委譲しているのでしょうか。先ほどSliver
の役割として、「なにを、どうレイアウトするか」を決める。という説明をしましたが、このうち「なにをレイアウトするか」の部分を委譲してます。
delegate
は大きく分けて二つでSliverChildListDelegate
とSliverChildBuilderDelegate
です。SliverChildListDelegate
は先ほどのコードを見ての通りですが、SliverChildBuilderDelegate
はリストの要素数と、要素ごと Widget を生成するにbuilder
関数を渡す仕組みになっています。こうすることで、必要になるタイミングまで Widget が生成されるのを遅らせることができるようになります。これはタイムラインのような要素が大量にあるリストを作るときに必須の機能です。
CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Text('This is $index th item!'),
childCount: 1000,
),
),
],
),
「なにをレイアウトするか」を別のクラスに委譲することによって、各アイテムのレイアウトを作成するコードを変更することなくSliverList
からSliverGrid
に切り替えたり、逆に要素数によって使うdelegate
を切り替えるといったことができるようになります。
Flutter では以上の概念の組み合わせによってほとんどのスクロール Widget が作られています。
ListView
, GridView
, PageView
, TabBarView
などがそうです。
ListView
はSliverList
一つだけを Sliver として持つViewport
によって作られていて、これをSliverGrid
にするとGridView
になります。
それぞれのデフォルトコンストラクタではSliverChildListDelegate
が使われていますが、これをSliverChildBuilderDelegate
に変更したものが、ListView.builder
とGridView.builder
です。
PageView
はSliverFillViewport
というViewport
をいっぱいに使うSliver
からできています。TabBarView
は内部的にはPageView
そのものです。
たった一つ例外として、SingleChildScrollView
だけはSliver
を使わずに、Scrollable
から得たviewport
を自前でレイアウトしています。ListView
の代わりにSingleChildScrollView
とColumn
を使えそうな感じがしますが、実はこの Widget だけ全くの別物として作られているんですね。
Sliver の応用
ここまで述べたようにSliver
は普段使っている Widget の中で広く使われていますが、ほとんどの場合代表的な使い道ごとにラッパー Widget が存在するため、アプリケーションコードに出てくることはあまりありません。
しかし、複数の Sliver を組み合わせたりデフォルトで用意されていないレイアウトを作るために、直接Sliver
を指定できる Widget があります。それがCustomScrollView
です。
ListView.custom
というコンストラクタは Sliver を使うこともできますが、こちらは一つしか Sliver を使うことができません。それに対してCustomScrollView
は複数の Sliver を組み合わせて使うことができます。
まずは、基本的なパターンとしてSliverGrid
とSliverList
の組み合わせを試してみましょう。