skip to Main Content

I am trying to implement an infinite scroll list view, My data loads to liveView but everytime new data comes, listview scrolls to top, I want to avoid scroll to top when new data comes in

import 'package:bloc_infinite_scroll/bloc/user_bloc.dart';
import 'package:bloc_infinite_scroll/bloc/user_event.dart';
import 'package:bloc_infinite_scroll/bloc/user_state.dart';
import 'package:bloc_infinite_scroll/data/models/user_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ScrollController _scrollController = ScrollController();
  List<UserData> users = [];

  @override
  void initState() {
    super.initState();
    context.read<UserBloc>().add(FetchUserEvent());
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        context.read<UserBloc>().add(FetchUserEvent());
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll'),
      ),
      body: BlocBuilder<UserBloc, UserState>(
        builder: (context, state) {

          if (state is UserInitialState) {
            return const Center(
              child: Text('UserInitialState'),
            );
          }
      
          if (state is UserLoadingState) {
            return const Center(
              child: Text('UserLoadingState'),
            );
          }
      
          if (state is UserLoadedState) {
            return Center(
              child: ListView.builder(
                controller: _scrollController,
                itemCount: state.users.length + 1,
                itemBuilder: (context, index) {
                  if (index >= state.users.length) {
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  }
      
                  return ListTile(
                    title: Text(state.users[index].name.toString()),
                  );
                },
              ),
            );
          }
      
          return const Text('Users list');
        },
      ),
    );
  }
}

here is my user_bloc.dart file

import 'package:bloc_infinite_scroll/bloc/user_event.dart';
import 'package:bloc_infinite_scroll/bloc/user_state.dart';
import 'package:bloc_infinite_scroll/data/models/user_model.dart';
import 'package:bloc_infinite_scroll/data/repositories/user_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class UserBloc extends Bloc<UserEvent, UserState> {
  UserRepository userRepository;

  UserBloc(this.userRepository) : super(UserInitialState()) {
    on<FetchUserEvent>((event, emit) async {
      emit(UserLoadingState());

      final List<UserData> users = await userRepository.getUsers();

      if(state is UserLoadingState){
        emit(UserLoadedState(users: users));
      }

      if(state is UserLoadedState){
        emit(UserLoadedState(users: [...(state as UserLoadedState).users,...users]));  
      }

    });
  }
}

2

Answers


  1. It is likely due to during FetchUserEvent the state was temporarily UserLoadingState and the scroll position is lost, maybe you can try
    make isLoading a property of UserState. And still show the list (instead of Text('UserLoadingState')) if the it is loading and has data.

    Edit: My previous answer would cause multiple fetch so I removed the code.

    Login or Signup to reply.
  2. The issue lies in the emission of the UserLoadingState state in bloc event handler, which causes the BlocBuilder to rebuild the ListView, therefore losing the scroll position. To fix the issue, you need to either replace the loading state with something else, for example, a boolean flag or another state class that doesn’t cause the widget tree to rebuild.

    You also have small issues in the provided code sample:

    1. In the bloc event handler, state is not checked to be UserLoadingState. This can cause multiple calls to UserRepository.getUsers in a single tree rebuild;
    2. In the widget tree, subscription to ScrollController and reliance on position.maxScrollExtent will work incorrectly if the first page of users ends up taking less than vertical viewport space. The scroll view will not have an overflow available for you to see in position.pixels and the next page will not be requested, i.e. no scrolling available = no ability to load new pages on scroll. Instead, you can check the item index position in the list builder callback and request data loads from there;
    3. For infinite lists of data, CustomScrollView and SliverList are more performant in general.

    Here is a code sample with your original question and additional issues fixed. Note that I am using the freezed code generation package to generate the UserState class with copyWith method. Alternatively, you can use copy_with_extension_gen to generate the copyWith method.

    user_state.dart:

    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'user_state.freezed.dart';
    
    @freezed
    class UserState with _$UserState {
      const factory UserState({
        required List<String> users,
        @Default(false) bool isLoading,
        @Default(false) bool isInitial,
      }) = _UserState;
    }
    

    user_event.dart:

    sealed class UserEvent {
      const UserEvent();
    }
    
    final class FetchUserEvent extends UserEvent {
      const FetchUserEvent();
    }
    

    user_bloc.dart:

    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:fluttertest/user_event.dart';
    import 'package:fluttertest/user_state.dart';
    
    class UserBloc extends Bloc<UserEvent, UserState> {
      UserBloc()
          : super(const UserState(
              users: [],
              isInitial: true,
            )) {
        on<FetchUserEvent>(
          (event, emit) async {
            if (state.isLoading) {
              return;
            }
    
            emit(state.copyWith(isLoading: true));
    
            final users = await _generateUsers();
    
            emit(
              state.copyWith(
                users: [...state.users, ...users],
                isLoading: false,
                isInitial: false,
              ),
            );
          },
        );
      }
    
      int _counter = 1;
    
      Future<List<String>> _generateUsers() async {
        await Future<void>.delayed(const Duration(seconds: 1));
    
        final data = List.generate(10, (i) => 'User #${i + _counter}');
        _counter += 10;
        return data;
      }
    }
    

    main.dart:

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:fluttertest/user_bloc.dart';
    import 'package:fluttertest/user_event.dart';
    import 'package:fluttertest/user_state.dart';
    
    void main() => runApp(const MaterialApp(home: MyPage()));
    
    class MyPage extends StatefulWidget {
      const MyPage({super.key});
    
      @override
      State<MyPage> createState() => _MyPageState();
    }
    
    class _MyPageState extends State<MyPage> {
      late final UserBloc _userBloc;
      final _scrollController = ScrollController();
    
      @override
      void initState() {
        super.initState();
    
        _userBloc = UserBloc()..add(const FetchUserEvent());
      }
    
      @override
      void dispose() {
        _userBloc.close();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Infinite Scroll'),
          ),
          body: BlocBuilder<UserBloc, UserState>(
            bloc: _userBloc,
            builder: (context, state) {
              if (state.isInitial) {
                return const Center(
                  child: Text('UserInitialState'),
                );
              } else if (state.users.isEmpty) {
                return const Center(
                  child: Text('Users list'),
                );
              } else {
                return CustomScrollView(
                  physics: const BouncingScrollPhysics(
                    parent: AlwaysScrollableScrollPhysics(),
                  ),
                  controller: _scrollController,
                  slivers: [
                    SliverList.builder(
                      itemCount: state.users.length,
                      itemBuilder: (context, index) {
                        if (index == state.users.length - 1) {
                          Future<void>.delayed(
                            Duration.zero,
                            () => _userBloc.add(const FetchUserEvent()),
                          );
                        }
    
                        return ListTile(
                          title: Text(state.users[index]),
                        );
                      },
                    ),
                    if (state.isLoading)
                      const SliverToBoxAdapter(
                        child: SizedBox(
                          height: 64,
                          child: Center(
                            child: CircularProgressIndicator(),
                          ),
                        ),
                      ),
                  ],
                );
              }
            },
          ),
        );
      }
    }
    

    Here’s a gif of the code sample: demo.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search