cacheExtent of GridView nested in ListView is ignored

149

shrinkWrap makes GridView/ListView decides height itself.

Without shrinkWrap, ListView expands to main axis infinitely. So its height is constrained by parent box constraint, and fills available space. In that case render space is definite. ListView only build childs on the visible space.

With shrinkWrap, ListView try to shrink, as short as possible unless it can contain all childs, so its height is sum of child's height (roughly). In this case ListView doesn't see parent constraint so ListView doesn't know how many childs it should build, so it builds all elements.

Now let's see your example. With replacing log to print, we get the following.

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

Although I can't see the second InnerList, ListView built the second child. Now with commenting out ListView.builder's shrinkWrap

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

If you scroll down, log will be same with the first.

GridView is same. But you can't comment out GridView's shrinkWrap due to layout error. It is because ListView gives infinite height to its children (GridView). If GridView expands, its height will be infinity.

Of course using SizedBox will solve this partially, but maybe it is not what you want.

If you really want on demand load here, you may use sliver.

Add: cacheExtent doesn't mean item count. It is pixel count.

Share:
149
cm101
Author by

cm101

Updated on January 03, 2023

Comments

  • cm101
    cm101 over 1 year

    I am trying to build a gridview nested in a listview with flutter, based on the attached minimal viable example, while trying to leverage the inner gridviews cacheExtent feature, which shall prevent items to be built all at once. In the full application i do kind of heavy stuff, so i want as few items to be loaded at the same time as possible, but unfortunately it looks like cacheExtent is ignored for the inner list, when nesting multiple list. For the surrounding list cacheExtent works as expected.

    Does anyone have a solution for this and can explain me, while it won't work for the inner list? I am not only looking for a copy&paste solution, i am really trying to understand whats going on in flutter here, since i guess it is caused by any fundamental layouting policy which i don't know yet.

    Environment:

    [✓] 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)
    

    Example:

    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);
                })
          ],
        );
      }
    }
    

    // Update

    The accepted answer pointed me into the right direction, leading my search to a video explaining why it won't work - the simple answer is: shrinkWrap forces lists to evaluate all childs, to determine their height. It is even shown on a very similar example, using shrinkWrap and physics properties of the list. The solution with Slivers now looks similar like follows, even if i am still kind of skeptic constructing the outer list used by CostumScrollView in a loop.

    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
                )
        );
      }
    }