Deep dive into sliver

この記事は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 つです。

ようやく今日の本題であるSliverが出てきましたが、まずはScrollableから見ていきましょう。

この Widget は主にユーザーのジェスチャーとviewPortの管理を担当しています。簡単に言ってしまうとユーザーのジェスチャーを検知してコンテンツのどのあたりを表示するかを決める Widget です。この Widget はあくまでviewPortの位置を決めるだけでそれ以外のことは何もしません。

Scrollableによって決まったスクロール位置はViewPortWidget に渡され、そこで実際のレンダリングが行われます。

このViewPortWidget は、Scrollableから現在のスクロール位置が与えられる他に、なにをどう表示するかを指定することができます。

    return Viewport(
      offset: offset,
      slivers: slivers,
      // ...
    );

このために渡されるのが、今回の主役であるところのSliverWidgetです。つまりSliverはスクロールするレイアウトの中で、特定のスクロール位置に対して「なにを、どうレイアウトするか」という部分に関する役割を担っている概念です。

ここで一つ補足として、実は Flutter にSliverSliverWidgetという名前のクラスはありません。実装パターンとして概念的に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は大きく分けて二つでSliverChildListDelegateSliverChildBuilderDelegateです。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などがそうです。

ListViewSliverList一つだけを Sliver として持つViewportによって作られていて、これをSliverGridにするとGridViewになります。 それぞれのデフォルトコンストラクタではSliverChildListDelegateが使われていますが、これをSliverChildBuilderDelegateに変更したものが、ListView.builderGridView.builderです。

PageViewSliverFillViewportというViewportをいっぱいに使うSliverからできています。TabBarViewは内部的にはPageViewそのものです。

たった一つ例外として、SingleChildScrollViewだけはSliverを使わずに、Scrollableから得たviewportを自前でレイアウトしています。ListViewの代わりにSingleChildScrollViewColumnを使えそうな感じがしますが、実はこの Widget だけ全くの別物として作られているんですね。

Sliver の応用

ここまで述べたようにSliverは普段使っている Widget の中で広く使われていますが、ほとんどの場合代表的な使い道ごとにラッパー Widget が存在するため、アプリケーションコードに出てくることはあまりありません。

しかし、複数の Sliver を組み合わせたりデフォルトで用意されていないレイアウトを作るために、直接Sliverを指定できる Widget があります。それがCustomScrollViewです。

ListView.customというコンストラクタは Sliver を使うこともできますが、こちらは一つしか Sliver を使うことができません。それに対してCustomScrollViewは複数の Sliver を組み合わせて使うことができます。

まずは、基本的なパターンとしてSliverGridSliverListの組み合わせを試してみましょう。