Reputation: 167
Want to start off by saying this is unrelated to what is being discussed here as I use the Bloc pattern.
I have a widget where I create a CustomListView with multiple SliverLists based on the items returned by the StreamBuilder on top of the CustomListView. Each SliverList is infinite in the sense that the childCount is set to null. This is for lazy loading purposes. The problem is that when I push to and pop back from a page, all the items of all the SliverLists are rebuild, which causes a delay, especially when I'm already pretty far down the list.
I thought perhaps this might be solvable with Keys, but this seems to be unrelated to that? I think the issue is that I'm rebuilding the list of SliverLists dynamically in the build method (see build() in _ItemsBrowserState). The solution that I can think of is storing these widgets inside the state, but that just seems like I'm treating the symptom rather than the cause? I feel the same way about using AutomaticKeepAliveClientMixin, but feel free to change my mind on this.
class ItemsBrowser extends StatefulWidget {
final RepositoryBloc repoBloc;
ItemsBrowser({Key key, @required this.repoBloc}) : super(key: key);
@override
_ItemsBrowserState createState() => _ItemsBrowserState();
}
class _ItemsBrowserState extends State<ItemsBrowser> {
ScrollController _scrollController;
ItemBrowsersBloc bloc;
List<ItemBrowserBloc> blocs = [];
int atBloc = 0;
bool _batchLoadListener(ScrollNotification scrollNotification) {
if (!(scrollNotification is ScrollUpdateNotification)) return false;
if (_scrollController.position.extentAfter > 500) return false;
if (atBloc == blocs.length) return false;
if (blocs[atBloc].isLoading.value) return false;
if (blocs[atBloc].wasLastPage) atBloc++;
if (atBloc < blocs.length) blocs[atBloc].loadNextBatch();
return false;
}
@override
void initState() {
super.initState();
bloc = ItemBrowsersBloc(widget.repoBloc);
bloc.collections.listen((collections) {
if (_scrollController.hasClients) _scrollController.jumpTo(0.0);
_disposeItemBlocs();
atBloc = 0;
blocs = [];
for (var i = 0; i < collections.length; i++) {
var itemBloc = ItemBrowserBloc(collections[i], initLoad: i == 0);
blocs.add(itemBloc);
}
});
_scrollController = ScrollController();
}
void _disposeItemBlocs() {
if (blocs != null) {
for (var b in blocs) {
b.dispose();
}
}
}
@override
void dispose() {
super.dispose();
bloc?.dispose();
_disposeItemBlocs();
}
@override
Widget build(BuildContext context) {
print('Building Item Browser');
return StreamBuilder<List<Collection>>(
stream: bloc.collections,
builder: (context, snapshot) {
if (!snapshot.hasData) return Container();
List<Widget> slivers = [];
for (var i = 0; i < snapshot.data.length; i++) {
slivers.add(ItemList(blocs[i], key: UniqueKey()));
slivers.add(_buildLoadingWidget(i));
}
slivers.add(const SliverToBoxAdapter(
child: const SizedBox(
height: 90,
)));
return NotificationListener<ScrollNotification>(
onNotification: _batchLoadListener,
child: CustomScrollView(
controller: _scrollController, slivers: slivers),
);
});
}
Widget _buildLoadingWidget(int index) {
return StreamBuilder(
stream: blocs[index].isLoading,
initialData: true,
builder: (context, snapshot) {
return SliverToBoxAdapter(
child: Container(
child: snapshot.data && !blocs[index].initLoaded
? Text(
'Loading more...',
style: TextStyle(color: Colors.grey.shade400),
)
: null,
),
);
},
);
}
}
class ItemList extends StatelessWidget {
final ItemBrowserBloc bloc;
ItemList(this.bloc, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: bloc.isLoading,
initialData: true,
builder: (context, snapshot) {
var isLoading = snapshot.data;
var isInitialLoad = isLoading && !bloc.initLoaded;
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
// Index: 0 1 2 3
// Return: Header Item Item null
print('INDEX $index');
if (index == 0) return _buildHeader();
if (index > bloc.items.value.length) return null;
// var itemIndex = (index - 1) % bloc.batchSize;
var itemIndex = index - 1;
var item = bloc.items.value[itemIndex];
return InkWell(
key: ValueKey<String>(item.key),
child: ItemTile(item),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => ItemPage(item)));
},
);
}, childCount: isInitialLoad ? 0 : null),
);
});
}
Widget _buildHeader() {
return Container();
}
}
Behaviour: I open the page and see the first list. In the logs I see 'INDEX 0', 'INDEX 1', .... 'INDEX 8' (see build() in ItemList), because Flutter lazily builds only the first 9 items. As I scroll down more items are build. I stop at 'INDEX 30' and tap on a item, which pushes a new page. Now the problem: The page loading takes a sec. The logs show 'INDEX 0' ... 'INDEX 30', i.e. all the items are rebuild, causing a delay. I pop the page, and again all items from 0 to 30 are rebuild, causing a delay.
As expected, If I scroll down to the second SliverList, the entirety of the first SliverList and the lazily build items of the second SliverList are all rebuild on push/pop.
Expected behavior: Only the surrounding items should be rebuild.
Upvotes: 0
Views: 4082
Reputation: 167
Ladies and gentleman, we got him:
slivers.add(ItemList(blocs[i], key: UniqueKey()));
Replacing the UniqueKey with a ValueKey (or removing it) removed the awful delay!
Upvotes: 4