Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
nikita488 opened this issue Sep 5, 2024 · 8 comments
Assignees

Comments

@nikita488
Copy link

nikita488 commented Sep 5, 2024

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

2024-09-06_09-10-02.mp4

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
Copy link
Member

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.

standby({
BuildContext? sliverContext,
bool isRemove = false,
int changeCount = 1,
ChatScrollObserverHandleMode mode = ChatScrollObserverHandleMode.normal,
ChatScrollObserverRefIndexType refIndexType =
ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent,
@Deprecated(
'It will be removed in version 2, please use [refItemIndex] instead')
int refItemRelativeIndex = 0,
@Deprecated(
'It will be removed in version 2, please use [refItemIndexAfterUpdate] instead')
int refItemRelativeIndexAfterUpdate = 0,
int refItemIndex = 0,
int refItemIndexAfterUpdate = 0,
}) async {
innerMode = mode;
this.isRemove = isRemove;
this.changeCount = changeCount;
observeSwitchShrinkWrap();
final firstItemModel = observerController.observeFirstItem(
sliverContext: sliverContext,
);
if (firstItemModel == null) return;

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants