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
)
);
}
}
CodePudding user response:
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.