Sittiphan Sittisak
Sittiphan Sittisak

Reputation: 885

Flutter: Lazy Loading with low amount of data

I try to use lazy load to show the order of the customer by using the ScrollController. Of course, the new user has a low number of orders and those items are not enough to take up the entire screen. So the ScrollController doesn't work. What I can do?

This code will show a basic lazy load. You can change the _initialItemsLength to a low value like 1 to see this issue. You can try this at api.flutter.dev

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Center(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  late List myList;
  ScrollController _scrollController = ScrollController();
  int _initialItemsLength = 10, _currentMax = 10;

  @override
  void initState() {
    super.initState();
    myList = List.generate(_initialItemsLength, (i) => "Item : ${i + 1}");

    _scrollController.addListener(() {
      print("scrolling: ${_scrollController.position.pixels}");
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  _getMoreData() {
    print("load more: ${myList.length}");
    for (int i = _currentMax; i < _currentMax + 10; i++) {
      myList.add("Item : ${i + 1}");
    }
    _currentMax = _currentMax + 10;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemBuilder: (context, i) {
          if (i == myList.length) {
            return CupertinoActivityIndicator();
          }
          return ListTile(
            title: Text(myList[i]),
          );
        },
        itemCount: myList.length + 1,
      ),
    );
  }
}

First, start _initialItemsLength with 10. The scroller will be available and you will see it in the console. After that, change _initialItemsLength to 1. The console will be blank.

Upvotes: 0

Views: 459

Answers (3)

Sittiphan Sittisak
Sittiphan Sittisak

Reputation: 885

The point is 'Find some parameter that can tell whether scroll is enabled or not. If not just load more until the scroll is enabled. Then use a basic step for a lazy load like the code in my question.'

After I find this parameter on google, I don't find this. But I try to check any parameter as possible. _scrollController.any until I found this.

For someone who faces this issue like me.

You can detect the scroll is enabled by using _scrollController.position.maxScrollExtent == 0 with using some delay before that.

This is my code. You can see it works step by step in the console.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class PageStackoverflow72734370 extends StatefulWidget {
  const PageStackoverflow72734370({Key? key}) : super(key: key);

  @override
  State<PageStackoverflow72734370> createState() => _PageStackoverflow72734370State();
}

class _PageStackoverflow72734370State extends State<PageStackoverflow72734370> {
  late final List myList;
  final ScrollController _scrollController = ScrollController();
  final int _initialItemsLength = 1;
  bool isScrollEnable = false, isLoading = false;

  @override
  void initState() {
    super.initState();

    print("\ninitState work!");
    print("_initialItemsLength: $_initialItemsLength");
    myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
    _scrollController.addListener(() {
      print("\nListener work!");
      print("position: ${_scrollController.position.pixels}");
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) _getData();
    });
    _helper();
  }

  Future _helper() async {
    print("\nhelper work!");
    while (!isScrollEnable) {
      print("\nwhile loop work!");
      await Future.delayed(Duration.zero); //Prevent errors from looping quickly.
      try {
        print("maxScroll: ${_scrollController.position.maxScrollExtent}");
        isScrollEnable = 0 != _scrollController.position.maxScrollExtent;
        print("isScrollEnable: $isScrollEnable");

        if (!isScrollEnable) _getData();
      } catch (e) {
        print(e);
      }
    }
    print("\nwhile loop break!");
  }

  void _getData() {
    print("\n_getData work!");
    if (isLoading) return;
    isLoading = true;

    int i = myList.length;
    int j = myList.length + 1;
    for (i; i < j; i++) {
      myList.add("Item : ${i + 1}");
    }
    print("myList.length: ${myList.length}");
    isLoading = false;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemBuilder: (context, i) {
          if (i == myList.length) {
            return const CupertinoActivityIndicator();
          }
          return ListTile(title: Text(myList[i]));
        },
        itemCount: myList.length + 1,
      ),
    );
  }
}

You can test in my test. You can change the initial and incremental values at ?initial=10&incremental=1.

I know, this case is rare. Most applications show more data widget height than the height of the screen or the data fetching 2 turns that enough for making these data widget height than the height of the screen. But I put these data widgets in the wrap for users that use the desktop app. So, I need it.

Upvotes: 0

Nagual
Nagual

Reputation: 2089

scroll listener will be triggered only if user try to scroll as an option you need to check this condition _scrollController.position.pixels == _scrollController.position.maxScrollExtent after build method executed and each time when user scroll to bottom

just change a bit initState and _getMoreData methods

  @override
  void initState() {
    super.initState();
    myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
    _scrollController.addListener(() => _checkIsMaxScroll());
    WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll());
  }

  void _checkIsMaxScroll() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      _getMoreData();
    }
  }

  _getMoreData() {
    print('load more: ${myList.length}');
    for (int i = _currentMax; i < _currentMax + 10; i++) {
      myList.add('Item : ${i + 1}');
    }
    _currentMax = _currentMax + 10;
    setState(() => WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll()));
  }

Upvotes: 1

Lebecca
Lebecca

Reputation: 2878

You can set your ListView with physics: AlwaysScrollableScrollPhysics(), and thus it will be scrollable even when the items are not too many. This will lead the listener to be triggered.

Key code part:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        physics: AlwaysScrollableScrollPhysics(),
        controller: _scrollController,
        itemBuilder: (context, i) {
          if (i == myList.length) {
            return CupertinoActivityIndicator();
          }
          return ListTile(
            title: Text(myList[i]),
          );
        },
        itemCount: myList.length + 1,
      ),
    );
  }

Upvotes: 0

Related Questions