Start Debugging

修正: A RenderViewport expected a child of type RenderSliver but received a child of type RenderBox (Flutter の CustomScrollView)

CustomScrollView の slivers リストは sliver しか受け付けません。box ウィジェットは SliverToBoxAdapter でラップするか、ListView/Padding/Column を SliverList や SliverPadding に置き換えてください。

A RenderViewport expected a child of type RenderSliver but received a child of type RenderParagraph(あるいは RenderFlexRenderErrorBox、その他任意の RenderBox)は、CustomScrollViewslivers リストの中に通常のウィジェットを直接置いたことを意味します。slivers の中身はすべて sliver でなければなりません。もっとも手早い修正は、問題となっている box ウィジェットを SliverToBoxAdapter でラップすることです。リストの場合のより良い修正は、ListViewSliverList.builder に、PaddingSliverPadding に置き換えることです。Flutter 3.x(3.44)、Dart 3.x で検証しました。

このエラーが起きる文脈

Flutter はこれをレイアウト時、つまり何も描画する前にスローします。“received a child of type” の後に続く具体的な型は、リストに何を置いたかによって変わります。Text なら RenderParagraphColumnRow なら RenderFlex、リスト内の builder が例外をスローした場合は RenderErrorBox になります。しかし形はいつも同じです。

FlutterError (A RenderViewport expected a child of type RenderSliver but received a
child of type RenderParagraph.
RenderObjects expect specific types of children because they coordinate with their
children during layout and paint. For example, a RenderSliver cannot be the child of
a RenderBox because a RenderSliver does not understand the RenderBox layout protocol.

The RenderViewport that expected a RenderSliver child was created by:
  Viewport ← IgnorePointer ← Semantics ← Listener ← _GestureSemantics ←
    Scrollable ← PrimaryScrollController ← CustomScrollView ← ...

The RenderParagraph that did not match the expected child type was created by:
  Text ← ...)

役に立つのは 2 つの “created by” ブロックです。最初のブロックは sliver を期待したスクロールウィジェットの名前を示します(ほぼ確実にあなたの CustomScrollView です)。2 つ目のブロックは sliver でなかった正確なウィジェットの名前を示します。まず 2 つ目のブロックを読んでください。変更すべき行をそのまま指し示しています。

なぜ viewport は box の子を拒否するのか

Flutter にはレイアウトプロトコルが 1 つではなく 2 つあります。ContainerTextRowColumn のような通常のウィジェットは box としてレイアウトされます。親が幅と高さの制約を下に渡し、子が具体的な Size を返して完了です。それらのレンダーオブジェクトは RenderBox のサブクラス(RenderParagraphRenderFlex など)です。

sliver はスクロールのために作られた、より豊かな別のプロトコルを使います。sliver は単にサイズを報告するだけではありません。レイアウト中に、現在どれだけがスクロールで画面外に出ているか、viewport にどれだけ余地が残っているか、スクロールオフセット、軸の方向などを記述した SliverConstraints を受け取ります。そして、どれだけの空間を描画したか、スクロール軸上でどれだけ消費したか、ヒットテストの範囲、可視化を望むかどうかを記述した SliverGeometry を返します。この往復のやり取りこそが、SliverAppBar がスクロールにつれて縮み、SliverList が現在画面上にある行だけをビルドできる理由です。そのレンダーオブジェクトは RenderSliver のサブクラスです。

RenderViewportCustomScrollView の背後にあるレンダーオブジェクト)は、子に対して sliver プロトコルしか話しません。各子に SliverConstraints を渡し、SliverGeometry が返ってくることを期待します。そこに RenderBox を渡すと、その box は SliverConstraints をどう扱えばよいか分からず、viewport が呼び出そうとしているメソッドを実装していません。レイアウトの奥深くで分かりにくい null とともにクラッシュするのではなく、フレームワークは事前に子の型をチェックしてこのアサーションをスローします。メッセージがルールを明言しています。“a RenderSliver cannot be the child of a RenderBox because a RenderSliver does not understand the RenderBox layout protocol”(RenderSliver は RenderBox のレイアウトプロトコルを理解しないので、RenderBox の子にはなれない)。逆方向についても同じことが言え、それがまさにあなたが遭遇したミスマッチです。

つまりこれは、RenderBox was not laid out のような微妙な制約バグや RenderFlex オーバーフロー とは違います。これは型のミスマッチです。sliver があるべき場所に box ウィジェットが立っているのです。

最小の再現

slivers リストに sliver でないウィジェットがあれば、どれでもこれを引き起こします。ここに最小のバージョンを示します。sliver があるべき場所に裸の Text を置いたものです。

// Flutter 3.x (tested 3.44), Dart 3.x
class Feed extends StatelessWidget {
  const Feed({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const Text('Recent activity'), // RenderParagraph, not a sliver
        SliverList.builder(
          itemCount: 20,
          itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
        ),
      ],
    );
  }
}

SliverList.builder は問題ありません。問題は Text です。これは RenderParagraph になり、viewport は RenderSliver を期待していたので、レイアウトがスローします。ColumnPaddingCenterListView、あるいはカスタムページウィジェット全体を slivers に落とし込んでも同じことが起こります。sliver でなければ、viewport はそれを拒否します。

修正 1: 単一の box ウィジェットを SliverToBoxAdapter でラップする

見出し、バナー、カード、ボタン行のような単発の box ウィジェットには、SliverToBoxAdapter でラップします。このウィジェットは sliver であり、その役目はただ 1 つの RenderBox の子をホストし、2 つのプロトコルの間を翻訳することです。box を測定し、viewport に適切な SliverGeometry を報告します。

// Flutter 3.x (tested 3.44)
CustomScrollView(
  slivers: [
    const SliverToBoxAdapter(
      child: Text('Recent activity'),
    ),
    SliverList.builder(
      itemCount: 20,
      itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
    ),
  ],
)

これは直接的な修正であり、box のコンテンツが本当に単一の固定サイズの塊であるときに正しい修正です。ヘッダー、スペーサー、サマリーカードが 1 つのスクロールビューの中でリストの上に置かれる必要があるときに、まず手を伸ばす sliver です。

知っておくべきことが 1 つあります。SliverToBoxAdapter は子を積極的にビルドし、画面上にあるかどうかにかかわらず生かし続けます。box には遅延という概念がないためです。ヘッダーには問題ありません。しかし長いリストには不適切であり、それが修正 2 です。

修正 2: リストにはラップした ListView ではなく SliverList/SliverGrid を使う

もっとも多い間違いは、ListViewslivers に落とし込み、このエラーが出たときに ListViewSliverToBoxAdapter でラップすることです。それはアサーションを黙らせますが、形が間違っています。スクロール可能なものの中にスクロール可能なものがあることになり、内側の ListView はアダプターから無制限の高さを受け取ります。これは ListView を Column の中にネストする のと同じ失敗の系統です。shrinkWrap で無理に動くようにしても、遅延ビルドを捨てることになります。すべての行が最初にまとめて構築されてしまいます。

CustomScrollView の要点は、そのセクションが 1 つの viewport を共有する sliver であることです。ですから、box 化された ListView ではなく sliver リストを使ってください。

// Flutter 3.x (tested 3.44)
CustomScrollView(
  slivers: [
    const SliverToBoxAdapter(child: Text('Recent activity')),

    // Lazy: only builds rows near the viewport. Direct replacement for ListView.builder.
    SliverList.builder(
      itemCount: items.length,
      itemBuilder: (context, i) => ListTile(title: Text(items[i])),
    ),

    // Grid section in the same scroll view.
    SliverGrid.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
      itemCount: photos.length,
      itemBuilder: (context, i) => Image.network(photos[i]),
    ),
  ],
)

SliverList.builderSliverList.separatedSliverFixedExtentListSliverGrid.builder は、ListViewGridView の builder に相当する sliver です。長いリストを安価にする遅延ビルドを保ち、そのまま slivers にはまります。リストとグリッドを 1 つの連続したスクロールで流したい場合は、これがそのためのレイアウトです。完全なパターンについては slivers で ListView と GridView を混在させる を参照してください。

修正 3: Padding の代わりに SliverPadding、box の代わりに SliverFillRemaining

box と sliver のルールはラッパーウィジェットにも及びます。sliver に余白を付けるために Padding でラップすると、PaddingRenderBox なので viewport はそれを拒否します。sliver 対応のバージョンは SliverPadding で、これは sliver の子に余白を付けつつ自身は sliver であり続けます。

// Flutter 3.x (tested 3.44)
CustomScrollView(
  slivers: [
    SliverPadding(
      padding: const EdgeInsets.all(16),
      sliver: SliverList.builder(       // note: 'sliver:', not 'child:'
        itemCount: items.length,
        itemBuilder: (context, i) => Text(items[i]),
      ),
    ),
  ],
)

パラメーター名に注意してください。SliverPaddingchild: ではなく sliver: を取ります。その子自身が sliver でなければならないためです。同じ考え方が、他のいくつかのよくあるニーズをカバーします。

考え方はこうです。通常使う box ウィジェットごとに、それをラップする(SliverToBoxAdapterSliverFillRemaining)か、その sliver 版を見つける(SliverListSliverGridSliverPaddingSliverAppBar)かのどちらかにします。

落とし穴とそっくりさん

slivers 内の builder がスローすると、この同じエラーが表示されます。 受け取った型が RenderErrorBox の場合、子は ErrorWidget でした。slivers の中にある StreamBuilderFutureBuilder の内部で何かがビルド中にスローし、Flutter が赤いエラー box(RenderBox)を代わりに置き、viewport がそれを拒否したのです。修正は 2 段階です。builder がどのパスでも sliver を返すようにし、エラーケースを処理します。slivers 内の StreamBuilder は、エラーとローディングの分岐を含め、その builder から sliver を返さなければなりません。

// Flutter 3.x (tested 3.44)
StreamBuilder<List<String>>(
  stream: feed,
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return const SliverToBoxAdapter(child: Text('Could not load feed'));
    }
    if (!snapshot.hasData) {
      return const SliverToBoxAdapter(child: Center(child: CircularProgressIndicator()));
    }
    final items = snapshot.data!;
    return SliverList.builder(
      itemCount: items.length,
      itemBuilder: (context, i) => ListTile(title: Text(items[i])),
    );
  },
)

いずれかの分岐から裸の TextCircularProgressIndicator を返すと、元のエラーに逆戻りします。(ついでに言うと、FutureBuilder がリビルドのたびに future を再実行しているなら、それは別のバグで、修正する価値があります。FutureBuilder が Future を再作成しないようにする方法 を参照してください。)

逆方向のミスマッチはほとんど同じように読めます。 box があるべき場所に sliver を置くと(たとえば Column の中の SliverList)、“A RenderObjectWithChildMixin expected a child of type RenderBox but received a child of type RenderSliverList” または “expected a RenderBox but received a RenderSliverPadding” が出ます。同じルールの逆方向です。sliver は viewport(CustomScrollView、または NestedScrollView の slivers 領域)の中にしか住めず、ColumnCenterPadding の中に直接置くことはできません。sliver を box の親が受け入れるものに戻すには、通常はそうしません。sliver が CustomScrollView の中に入るように構造を組み直します。

SliverToBoxAdapter は長いリストを隠す場所ではありません。 動作するので誘惑されますが、遅延性を台無しにします。アダプターは子のサブツリー全体を即座にビルドします。5,000 行の ListView(または 5,000 個の子を持つ Column)を 1 つに包むと、最初のフレームで 5,000 個すべてをビルドすることになり、レイアウト時間が急増してタイムライン上でジャンクとして現れます。ヘッダーや単一のカードには使い、スクロールするものには SliverList.builder を使ってください。

ホットリロードがこれから回復できないことがあります。 アサーションがレイアウト中に発火するため、コードを修正した後のホットリロードでレンダーツリーが行き詰まったままになることがときどきあります。問題の行を明らかに修正した後もエラーが残る場合は、ホットリロード(r)ではなくホットリスタート(R)を行ってください。

関連記事

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る