how to make this lazyload scrolling working with provider

Issue

it take about 7 days trying make a working example for lazyload listview with provider in flutter with real world example and it’s still not working because i think something is missing

As a note : the first load , works good and when i scroll it’s print (scroll) but nothing happened it’s still in the same page
if i try to return _todolist variable in the _onScrollUpdated it not change page correctly and after three times i see this error

E/flutter ( 7713): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: type ‘String’ is not a subtype of type
‘List’ E/flutter ( 7713): #0 TodoService.fetchTodos
(package:flutter_todo_provider/services/todo_service.dart:32:21)

json example
https://jsonformatter.org/52c83e

todos_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_todo_provider/helpers/http_exception.dart';

import 'package:provider/provider.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/services/todo_service.dart';

class TodosScreen extends StatefulWidget {
  @override
  _TodosScreenState createState() => _TodosScreenState();
}

class _TodosScreenState extends State<TodosScreen> {
  ScrollController _controller;
  List<dynamic> _todoList;
  bool _isLoading ;
  @override
  void initState() {
    super.initState();
    _controller = ScrollController();
    _controller.addListener(_onScrollUpdated);
  }
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(Configuration.AppName),
      ),
      body: FutureBuilder(
          future: _fetchListItems(),
          builder: (context, snapshot){
            if(snapshot.hasData){
              return _listItems(snapshot.data);
            }
            return _buildProgressIndicator();
          }
      ),
    );
  }
  _fetchListItems() async {
    try {
      await Provider.of<TodoService>(context, listen: false).loadNextPage();
      _todoList = Provider.of<TodoService>(context, listen: false).items;
    } on HttpException catch (e) {
      EasyLoading.showError(e.message);
    }
    return  _todoList ;

  }


  Widget _listItems(data){
    _isLoading =  Provider.of<TodoService>(context, listen: false).isLoading ;
    return ListView.builder(
      controller: _controller,
      itemCount: data.length  ,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(data[index].title),
          subtitle:Text(data[index].content),
          trailing: Icon(Icons.print),
        );
      },
    );
  }

  Future<void> _onScrollUpdated() async {
    print("Scroll11");
    var maxScroll = _controller.position.maxScrollExtent;
    var currentPosition = _controller.position.pixels;
    if (currentPosition == maxScroll ) {
      try {
        await Provider.of<TodoService>(context, listen: false).loadNextPage();
        _todoList = Provider.of<TodoService>(context, listen: false).items;

// return _todoList ; if use this line i see the error

      } on HttpException catch (e) {
        EasyLoading.showError(e.message);
      }
    }
  }

  Widget _buildProgressIndicator() {
    _isLoading =  Provider.of<TodoService>(context, listen: false).isLoading ;
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: new Opacity(
          opacity: _isLoading ? 1.0 : 00,
          child: new CircularProgressIndicator(),
        ),
      ),
    );
  }
}

todo_service.dart

 import 'dart:io';
    import 'package:dio/dio.dart';
    import 'package:flutter/foundation.dart';
    import 'package:flutter_todo_provider/.env.dart';
    import 'package:flutter_todo_provider/models/todo.dart';
    
    class TodoService with ChangeNotifier {
      bool isLoading = false;
      bool isFetching = false;
      int currentPage = 1;
      int totalRows = 10;
      List<Todo> items = [];
    
    
      loadNextPage() async {
        await fetchTodos(currentPage);
        currentPage++;
        notifyListeners();
      }
    
      Future fetchTodos(int currentPage) async {
    
        try {
          //404
          var options = Options(headers: {
            HttpHeaders.authorizationHeader: 'Basic ${Configuration.authToken}'
          });
          Map<String, dynamic> qParams = {
            'current_page': currentPage,
          };
          Response  response = await Dio().get('${Configuration.ApiUrl}/todos/my_todos',   options: options, queryParameters: qParams);
          List<dynamic> responseBode =  response.data["data"];
          responseBode.forEach(( dynamic json) {
            items.add(Todo.fromJson(json));
          });
          notifyListeners();
    
        } on DioError catch (e) {
          print("Error Message" + e.response.statusMessage);
          return items=[];
        }
    
    
      }
    

}

Solution

Here is the code:

class TodoScreen extends StatefulWidget {
  // Your service goes here
  // (the class extending ChangeNotifier)
  @override
  _TodoScreenState createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final TodoService todoService = TodoService();

  ScrollController _controller;

  @override
  void initState() {
    super.initState();
    _controller = ScrollController();
    _controller.addListener(_onScrollUpdated);
    loadNextPage();
  }

  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Configuration.AppName'),
      ),
      body: ChangeNotifierProvider.value(
        value: todoService,
        child: Consumer<TodoService>(builder: (_, ctl, __) {
          if (todoService.isLoading) {
            return _buildProgressIndicator();
          } else {
            return _listItems(todoService.items);
          }
        }),
      ),
    );
  }

  Widget _listItems(data) {
    return ListView.builder(
      controller: _controller,
      itemCount: data.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(data[index].title),
          subtitle: Text(data[index].content),
          trailing: Icon(Icons.print),
        );
      },
    );
  }

  Widget _buildProgressIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: CircularProgressIndicator(),
      ),
    );
  }

  Future<void> _onScrollUpdated() async {
    var maxScroll = _controller.position.maxScrollExtent;
    var currentPosition = _controller.position.pixels;
    if (currentPosition == maxScroll) {
      todoService.loadNextPage();
    }
  }
}

Note that i didn’t make changes to your service. The notifyListeners will do all the job for us.

When you are using Provider, the idea is to keep all your data inside the controller or service (the class that extends ChangeNitifier) and just use the variables with notifyListeners to change the behavior of your screen.

The screen needs to be listening for changes, for this we use the pair ChangeNotifierProvider.value with Consumer(builder: (_, ctl, __) {}).
Use ChangeNotifierProvider in some upper level of the widget tree and use Consumer only where you need the widget to be updated. You can even use more than one Consumer, they all just need to be under ChangeNotifierProvider.

Answered By – Rod

Answer Checked By – Mary Flores (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.