fluttercandies / flutter_scrollview_observer

A widget for observing data related to the child widgets being displayed in a ScrollView. Maintainer: @LinXunFeng
https://pub.dev/packages/scrollview_observer
MIT License
421 stars 44 forks source link

[How to use] Update shrinkWrap state of ChatScrollObserver after initially fetching list data #92

Closed nikita488 closed 1 week ago

nikita488 commented 2 weeks ago

Platforms

Android, Web

Description

I want to have a list of items that will keep it's scroll position everytime new item is added or old item is removed.

Initially i don't have list items, so when i open page with this list it fetch data via REST method and wait for response to get initial items list.

But it seems that ChatScrollObserver observeSwitchShrinkWrap tries to observe it when creating ChatScrollObserver object in the initState method, which observes it at the end of the frame, but initial list data might be not loaded at this point.

In this case when i use standby method and add item to list later, it didn't initially keep the scroll position (made a slight jump), but then it switches shrinkWrap state and after that scroll position is keeped when adding more items to list.

How to handle this?

My code

No response

Try do it

No response

LinXunFeng commented 2 weeks ago

Please provide a reproducible example.

Why do you need to keep the scroll position when the list goes from no data to data?

In addition, it should be noted that the prerequisite for the function of keeping the scroll position is that there must be an item in the list that can be used as a reference.

nikita488 commented 2 weeks ago

Here is the repo with example: https://github.com/nikita488/scrollview_observer_test I don't need to keep position on list initializing, the problem right now is that when it loads after some time, and then when i scroll up and try to add new item, firstly it jumps, rebuild my list with shrinkWrap setted to false and then after I add more items, it not jumps and works as expected.

LinXunFeng commented 2 weeks ago

I haven't reproduced the problem you mentioned, maybe you can record a video demonstration.

and then when i scroll up and try to add new item, firstly it jumps

I see you have set ..fixedPositionOffset = 8.0. Please confirm whether the offset of the list does not exceed this value when the problem you mentioned occurs.

And I noticed that the initial list data has made the list exceed one screen, but chatObserver.isShrinkWrap is still false, which is incorrect.

You can make the following adjustments (I am not familiar with the use of bloc, you can adjust it more reasonably).

class _MainAppState extends State<MainApp> {
  ...
  @override
  void initState() {
    ...
    super.initState();
+    context.read<MessageListCubit>().chatObserver = chatObserver;
  }
  ...
}
class MessageListCubit extends Cubit<MessageListState> {
  ...
+  ChatScrollObserver? chatObserver;

  Future<void> loadMessages() async {
    ...

+  chatObserver?.standby();
+  //  or call
+  //  chatObserver?.observeSwitchShrinkWrap();
    emit(state.copyWith(
        status: MessageListStatus.success,
        items: _items.values.toList(growable: false),
        lastMessageId: 0));
  }
  ...
}
nikita488 commented 1 week ago

https://github.com/user-attachments/assets/9159abf7-3f22-4fb4-b7c1-74006add5c7d

Here is the video demonstration. When i scroll up (much higher than fixedPositionOffset) and press Add button for the first time, my list jumps, and then after i click Add button more, it stays in place as expected.

Adding standy() or observeSwitchShrinkWrap() to Cubit seems to work fine, but what is better to use in this case? When I call standy method, it has changeCount setted to 1 by default, is it fine in this case?

Also, should I move my ListViewObserver inside BlocBuilder widget, so that it gets rebuild everytime ListView rebuilds too?

LinXunFeng commented 1 week ago

Adding standy() or observeSwitchShrinkWrap() to Cubit seems to work fine, but what is better to use in this case? When I call standy method, it has changeCount setted to 1 by default, is it fine in this case?

Which one to use depends on your purpose. observeSwitchShrinkWrap is only used to switch the shrinkWrap value. The standby includes observeSwitchShrinkWrap and keeping the scroll position function. In your example, the two are similar because standby will return in line 121 of the following code.

https://github.com/fluttercandies/flutter_scrollview_observer/blob/091db3b600799f9dbb910a85cca8c1c99b7fb737/lib/src/utils/src/chat/chat_scroll_observer.dart#L97-L121

Also, should I move my ListViewObserver inside BlocBuilder widget, so that it gets rebuild everytime ListView rebuilds too?

Optional, but if you haven’t moved ListViewObserver inside BlocBuilder widget, you’ll need to call observerController.reattach when the ListView’s BuildContext changes.

nikita488 commented 1 week ago

Gotcha.

I changed my code a bit to show loading indicator on initial build, and on success build a ListView, and now the jumping on initial list build problem is back again. I updated my repo, here I build loading indicator: https://github.com/nikita488/scrollview_observer_test/blob/master/lib/main.dart#L80

Can this be fixed?

LinXunFeng commented 1 week ago

Since your ListView does not always appear, it is recommended that you modify it as follows.

class _MainAppState extends State<MainApp> {
  ...
  late final ChatScrollObserver chatObserver;

+  BuildContext? listViewCtx;

  @override
  void initState() {
    ...
  }

  ...
          Expanded(
            child: ListViewObserver(
              controller: observerController,
+              sliverListContexts: () {
+                return [
+                  if (listViewCtx != null) listViewCtx!,
+                ];
+              },
              child: BlocBuilder<MessageListCubit, MessageListState>(
                  buildWhen: (previous, current) => current.status.isSuccess,
                  builder: (context, state) {
                    final items = state.items;
                    if (state.status == MessageListStatus.initial) {
                      return const Center(child: CircularProgressIndicator());
                    } else {
                      print('Rebuild List: ${chatObserver.isShrinkWrap}');
                      Widget resultWidget = ListView.builder(
                          controller: scrollController,
                          reverse: true,
                          physics: ChatObserverClampingScrollPhysics(
                              observer: chatObserver),
                          shrinkWrap: chatObserver.isShrinkWrap,
                          itemCount: items.length,
                          itemBuilder: (context, index) {
+                            if (listViewCtx != context) {
+                              listViewCtx = context;
+                              observerController.reattach();
+                              print('NEEDS REATTACH');
+                              // Waiting for reattach to complete
+                              WidgetsBinding.instance
+                                  .addPostFrameCallback((timeStamp) {
+                                chatObserver.observeSwitchShrinkWrap();
+                              });
+                            }
-                            if (scrollController.hasClients &&
-                                (observerController.sliverContexts.isEmpty ||
-                                    observerController.sliverContexts.first !=
-                                        context)) {
-                              observerController.reattach();
-                              print('NEEDS REATTACH');
-                            }
                            final itemIndex = index;
                            final reversedIndex = items.length - itemIndex - 1;
                            final item = items[reversedIndex];
                            return ListTile(
                                title: Text(item.text),
                                leading: const Icon(Icons.message));
                          });
                      return resultWidget;
                    }
                  }),
            ),
          ),
  ...
nikita488 commented 1 week ago

Thank you very much for your help and explanation, works like a charm now!

Also thank you for your hard work on this package, will use it more in the near future.