GridView嵌套在ListView中的cacheExtent被忽略

cacheExtent of GridView nested in ListView is ignored

我正在尝试基于随附的最小可行示例构建一个嵌套在 listview 中的 gridview,同时尝试利用内部 gridviews cacheExtent 功能,这将防止一次性构建所有项目。在完整的应用程序中,我做了一些繁重的事情,所以我希望同时加载尽可能少的项目,但不幸的是,当嵌套多个列表时,内部列表似乎忽略了 cacheExtent。对于周围列表 cacheExtent 按预期工作。

有没有人对此有解决方案并可以向我解释,但它不适用于内部列表?我不仅在寻找复制和粘贴解决方案,而且我真的想了解这里的 flutter 是怎么回事,因为我猜它是由我还不知道的任何基本布局策略引起的。

环境:

[✓] Flutter (Channel stable, 2.10.0, on macOS 12.2.1 21D62 darwin-arm, locale en-DE)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)

示例:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:developer';

void main() async {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
          // works as expected, second instance of InnerList will not be loaded on startup
          cacheExtent: 1,
          shrinkWrap: true,
          itemCount: 2,
          itemBuilder: (context, index) {
            return InnerList(index);
          },
      ),
      appBar: AppBar(
        title: const Text('Home'),
      ),
      drawer: const Drawer(
        child: Text("Foobar"),
      ),
    );
  }
}

class ItemWidget extends StatelessWidget {
  int index;

  ItemWidget(this.index);

  @override
  Widget build(BuildContext context) {
    // indicates all widgets are getting build at once, ignoring cacheExtent
    log("building widget " + index.toString());

    // TODO: implement build
    return SizedBox(
      height: 120,
      width: 120,
      child: Center(child: Text(index.toString())),
    );
  }
}

class InnerList extends StatelessWidget {
  int index;
  InnerList(this.index);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Text("Foobar " + index.toString()),
        GridView.builder(
            shrinkWrap: true,
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 120.0,
              crossAxisSpacing: 10.0,
              mainAxisSpacing: 10.0,
            ),
            itemCount: 100,
            primary: false,
            // this is ignored, all items of type ItemWidget will be loaded at once
            cacheExtent: 1,
            physics: const NeverScrollableScrollPhysics(),
            itemBuilder: (context, index) {
              return ItemWidget(index);
            })
      ],
    );
  }
}

//更新

已接受的答案为我指明了正确的方向,将我的搜索引导至 video 解释了为什么它不起作用 - 简单的答案是:shrinkWrap 强制列表评估所有孩子,以确定他们的身高.它甚至显示在一个非常相似的示例中,使用列表的 shrinkWrap 和物理属性。 Slivers 的解决方案现在看起来类似于以下内容,即使我仍然有点怀疑构建 CostumScrollView 在循环中使用的外部列表。

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:developer';

void main() async {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> innerLists = [];

  @override
  void initState() {
    super.initState();

    // construct lists
    for(var i=0; i<10; i++) {
      innerLists.add(SliverToBoxAdapter(
          child: Text("Foobar " + i.toString())
      ));
      innerLists.add(InnerList(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(slivers: innerLists),
      appBar: AppBar(
        title: const Text('Home'),
      ),
      drawer: const Drawer(
        child: Text("Foobar"),
      )
    );
  }
}

class ItemWidget extends StatelessWidget {
  int index;

  ItemWidget(this.index);

  @override
  Widget build(BuildContext context) {
    
    // TODO: implement build
    return SizedBox(
      height: 120,
      width: 120,
      child: Center(child: Text(index.toString())),
    );
  }
}

class InnerList extends StatelessWidget {
  int index;
  InnerList(this.index);

  @override
  Widget build(BuildContext context) {

    return
        SliverGrid(
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
              maxCrossAxisExtent: 120.0,
              crossAxisSpacing: 10.0,
              mainAxisSpacing: 10.0,
            ),
            delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              return ItemWidget(index);
            },
            childCount: 100
            )
    );
  }
}

shrinkWrap让GridView/ListView自己决定高度。

没有shrinkWrap,ListView无限扩展到主轴。所以它的高度受制于parent box constraint,并且填充可用space。在那种情况下 render space 是确定的。 ListView 仅在可见 space.

上构建 childs

对于shrinkWrap,ListView尽量缩小,越短越好,除非它能包含所有child,所以它的高度是child的高度之和(大概) .在这种情况下,ListView 没有看到 parent 约束,因此 ListView 不知道它应该构建多少 child,所以它构建所有元素。

现在让我们看看你的例子。将 log 替换为 print,我们得到以下内容。

building widget 0
building widget 1
...
building widget 99
building widget 0
building widget 1
...
building widget 99

虽然我看不到第二个InnerList,但是ListView构建了第二个child。现在注释掉 ListView.buildershrinkWrap

building widget 0
building widget 1
...
building widget 99

如果向下滚动,日志将与第一个相同。

GridView 相同。但是由于布局错误,你不能注释掉 GridView 的 shrinkWrap。这是因为 ListView 给它的 children (GridView) 赋予了无限的高度。如果 GridView 扩展,它的高度将是无穷大。

当然使用 SizedBox 会部分解决这个问题,但也许这不是您想要的。

如果你真的想在这里按需加载,你可以使用sliver

添加:cacheExtent 不代表项目数。它是像素数。