Skip to content

CarouselView.builder: Lazy Loading for CarouselView #170692

@exoad

Description

@exoad

Use case

Commonly, widgets like PageView and ListView lazily load items that are not in view. However, CarouselView does not. This can make things like fetching images from the internet or having to do heavy computations to get the widgets feel sluggish and can be taxing on the system if there are a lot of elements in the carousel.

However, it can be easier to use a PageView.builder for some cases, but this method enforces page snapping and additionally, the developer would need to program in the material effects of the CarouselView themselves which can be not trivial.

Description

The current implementation of CarouselView does not have a rigid form of lazy loading process akin to something like ListView.builder which allows for items in the list to load when they are deemed visible by Flutter.

Here is a demonstration of the difference between CarouselView and ListView.builder at handling 30 FutureBuilders.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(debugShowCheckedModeBanner: false, home: MainApp()));
}

final class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  bool showListView = false;
  Axis dir = Axis.vertical;

  late List<Future<void>> _carouselFutures;

  static const int totalItems = 30;
  static const Duration futureDelayMS = Duration(milliseconds: 3500);

  @override
  void initState() {
    super.initState();
    _carouselFutures = List<Future<void>>.generate(
      totalItems,
      (int index) =>
          Future<void>.delayed(futureDelayMS, () => print("(CarouselView) $index finished!")),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: const SizedBox.shrink(),
        flexibleSpace: SafeArea(
          child: Row(
            spacing: 8,
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              ElevatedButton(
                onPressed: () => setState(() {
                  showListView = !showListView;
                  if (!showListView) {
                    _carouselFutures = List<Future<void>>.generate(
                      totalItems,
                      (int index) => Future<void>.delayed(
                        futureDelayMS,
                        () => print("(CarouselView) $index finished!"),
                      ),
                    );
                  }
                }),
                child: Text("Show ${showListView ? "CarouselView" : "ListView"}"),
              ),
              ElevatedButton(
                onPressed: () =>
                    setState(() => dir = dir == Axis.horizontal ? Axis.vertical : Axis.horizontal),
                child: Text("Set ${dir == Axis.horizontal ? "Vertical" : "Horizontal"}"),
              ),
            ],
          ),
        ),
      ),
      body: SafeArea(
        left: false,
        right: false,
        child: Material(
          child: Column(
            children: <Widget>[
              Expanded(
                child: !showListView
                    ?
                      // carousel view here
                      CarouselView.weighted(
                        scrollDirection: dir,
                        flexWeights: const <int>[1, 2, 1],
                        itemSnapping: true,
                        shape: Border.all(color: Colors.transparent),
                        children: List<Widget>.generate(totalItems, (int index) {
                          return SizedBox(
                            height: MediaQuery.sizeOf(context).height,
                            width: MediaQuery.sizeOf(context).width * 0.95,
                            child: FutureBuilder<void>(
                              future: _carouselFutures[index],
                              builder: (BuildContext context, AsyncSnapshot<void> asyncSnapshot) {
                                if (asyncSnapshot.hasData ||
                                    asyncSnapshot.connectionState == ConnectionState.done) {
                                  return Container(
                                    decoration: BoxDecoration(
                                      color: index % 2 == 0 ? Colors.red : Colors.green,
                                      borderRadius: BorderRadius.circular(30),
                                    ),
                                    child: Center(
                                      child: Text(
                                        index.toString(),
                                        textAlign: TextAlign.center,
                                        textWidthBasis: TextWidthBasis.longestLine,
                                        style: const TextStyle(
                                          fontSize: 64,
                                          color: Colors.black,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                    ),
                                  );
                                } else if (asyncSnapshot.hasError) {
                                  return const Text(
                                    "Failed to load!",
                                    style: TextStyle(color: Colors.red),
                                  );
                                }
                                return const CircularProgressIndicator(color: Colors.amber);
                              },
                            ),
                          );
                        }),
                      )
                    :
                      // listview method
                      ListView.builder(
                        scrollDirection: dir,
                        itemCount: totalItems,
                        itemBuilder: (BuildContext context, int index) {
                          return SizedBox(
                            height: MediaQuery.sizeOf(context).height,
                            width: MediaQuery.sizeOf(context).width * 0.95,
                            child: FutureBuilder<void>(
                              future: Future<void>.delayed(
                                futureDelayMS,
                                () => print("(ListView) $index finished!"),
                              ),
                              builder: (BuildContext context, AsyncSnapshot<void> asyncSnapshot) {
                                if (asyncSnapshot.connectionState == ConnectionState.done) {
                                  return Container(
                                    decoration: BoxDecoration(
                                      color: index % 2 == 0 ? Colors.red : Colors.green,
                                      borderRadius: BorderRadius.circular(30),
                                    ),
                                    child: Center(
                                      child: Text(
                                        index.toString(),
                                        style: const TextStyle(
                                          fontSize: 64,
                                          color: Colors.black,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                    ),
                                  );
                                } else if (asyncSnapshot.hasError) {
                                  return const Text(
                                    "Failed to load!",
                                    style: TextStyle(color: Colors.red),
                                  );
                                }
                                return const CircularProgressIndicator(color: Colors.amber);
                              },
                            ),
                          );
                        },
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Proposal

Using the code sample from above, it would be better to have some kind of way to build the items on demand with like a CarouselView.builder and CarouselView.buildWeighted that can be supplied a common Widget Function(BuildContext, int) to render the widget at that index.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterf: material designflutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.team-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions