1

I'm trying to implement a lazy loading listview in my Flutter app. Basically when user scrolls to bottom, we fetch more data from the server. Let's say there are totally 100 items and I fetch 10 of them at the beginning.

The problem is, some devices such as tablets are large enough to display all the 10 items at once. So Flutter makes the listview unscrollable. Therefore we are not able to reach the bottom and fetching is not triggered.

What is the best approach to trigger fetching automatically on such use cases?

void _scrollListener() {
    // Check if we are at the bottom of the list and loading isn't in progress
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_isLoading) {
      _loadMoreItems();
    }
  }

  Future<void> _loadMoreItems() async {
    setState(() {
      _isLoading = true;
    });

    // Simulate a network request (wait 2 seconds)
    await Future.delayed(Duration(seconds: 2));

    // Fetch more items
    final newItems = List.generate(10, (index) => _items.length + index);

    setState(() {
      _isLoading = false;
      _items.addAll(newItems);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Lazy Loading ListView')),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _items.length + 1, // +1 for the loading indicator
        itemBuilder: (context, index) {
          if (index == _items.length) {
            // Show loading indicator at the bottom when more items are being fetched
            return _isLoading
                ? Center(child: CircularProgressIndicator())
                : SizedBox.shrink();
          }
          return ListTile(title: Text('Item ${_items[index]}'));
        },
      ),
    );
  }

This screen is large enough to display all 10 items so fetching does not trigger

3
  • 2
    Did you consider loading more items on that larger screen. So instead of using a hard-coded 10, calculate that page based on the actual available space (from the layout constraints) and your (if needed: estimated) item height. Commented Feb 27 at 1:02
  • 1
    If you look at YouTube or Stack Overflow, they probably design their interfaces for larger screens and aim to load a reasonable number of items per request. I’m not sure about your case, but sometimes fetching more items in a single request is better than making multiple requests. On smaller devices, users will need to scroll anyway Commented Feb 27 at 3:56
  • @FrankvanPuffelen yes, actually I was trying to ask how to determine the best value for page size instead of using a hard-coded value like 10. I do not know the height of the items displayed as they are rendered dynamically based on the content they have. I'll check out layout constraints and assign a value depending on that as you said. Thanks Commented Feb 27 at 6:47

3 Answers 3

2

i have updated code please try this if it works.

import 'package:flutter/material.dart';

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

  @override
  State<LazyLoadingList> createState() => _LazyLoadingListState();
}

class _LazyLoadingListState extends State<LazyLoadingList> {
  List<int> items = List.generate(20, (index) => index); // Initial list
  final ScrollController _scrollController = ScrollController();
  bool isLoading = false;
  int totalItems = 100; // Simulated total items available

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    if (_scrollController.position.pixels >=
            _scrollController.position.maxScrollExtent - 200 &&
        !isLoading &&
        items.length < totalItems) {
      _loadMoreItems();
    }
  }

  Future<void> _loadMoreItems() async {
    if (!mounted) return;

    setState(() {
      isLoading = true;
    });

    // Simulate network delay
    await Future.delayed(const Duration(seconds: 1));

    if (!mounted) return;

    setState(() {
      final newItems = List.generate(
        20,
        (index) => items.length + index,
      );
      items.addAll(newItems);
      isLoading = false;
    });
  }

  @override
  void dispose() {
    _scrollController.removeListener(_onScroll);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Lazy Loading List'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: items.length + (isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == items.length && isLoading) {
            return const Center(
              child: Padding(
                padding: EdgeInsets.all(8.0),
                child: CircularProgressIndicator(),
              ),
            );
          }
          return ListTile(
            title: Text('Item ${items[index]}'),
          );
        },
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: LazyLoadingList()));
}
Sign up to request clarification or add additional context in comments.

2 Comments

While your update works fine on the given scenario, with total item count known, it does not fully solve my problem because I do not have the information of the total count every time. I'm mostly looking for a solution on how to fix the said triggering problem. But thanks anyways
You can remove "totalItems" and it will still work, if you want to use "totalItems" you can update once you get the API response to update the "totalItems" count.
2

You can use layout builder and depends on screen size. I used like this. You can make configuration for yours, like get 10 items for small screen, 20 for big screen sizes.

  Widget _buildScreenLayout(BuildContext context) => Container(
        height: context.height,
        margin: const EdgeInsets.all(10),
        child: LayoutBuilder(
          builder: (context, constraints) {
            final isSizeSmall = MediaQuery.sizeOf(context).width < 600;
            if (isSizeSmall) {
              return _buildLayout();
            } else {
              return _buildBigScreenLayout();
            }
          },
        ),
      );

Comments

0

I would recommend EasyRefresh to you

  EasyRefresh(
    onRefresh: () async {
      ....
    },
    onLoad: () async {
      ....
    },
    child: ListView(),
  );

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.