Skip to content

UI stutter on rendering large list due to the use of Column widget #290

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

Open
lhengl opened this issue Dec 11, 2024 · 6 comments
Open

UI stutter on rendering large list due to the use of Column widget #290

lhengl opened this issue Dec 11, 2024 · 6 comments

Comments

@lhengl
Copy link

lhengl commented Dec 11, 2024

Describe the bug
The UI appear to be stuttering when there is a large enough data set being loaded into the UI. This is because the UI favours the use of columns over a more efficient ListView or builder widgets that renders on the fly.

To Reproduce
Steps to reproduce the behavior:

  1. Create a ChatView and inject large list of messages each time - either in the initiaMessageList or during loadMoreData

I've created a demo app to demonstrate this: https://github.com/lhengl/chatview_errors

  1. Tap ScrollController Error list tile
  2. Change the states to see the stutter

Expected behavior
There should be no stutter

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):
N/A

Smartphone (please complete the following information):
N/A

Additional context
N/A

@officialismailshah
Copy link

did you resolve this issue @lhengl

@lhengl
Copy link
Author

lhengl commented Jan 9, 2025

did you resolve this issue @lhengl

No, I haven't resolved this issue yet. Also, for some reason, I can't fork to attempt a fix... it just won't create a fork ok github for me.

@officialismailshah
Copy link

did you resolve this issue @lhengl

No, I haven't resolved this issue yet. Also, for some reason, I can't fork to attempt a fix... it just won't create a fork ok github for me.

sure here you can fork this repo I have forked it just now
https://github.com/officialismailshah/flutter_chatview

@lhengl
Copy link
Author

lhengl commented May 14, 2025

I'm trying to fix this issue because the stuttering is unbearable, so I had a look at this issue today and it's little bit confusing how this seem to work. It looks like the inner most ChatGroupedListWidget does use a ListView.builder(). However the parent widget wraps it inside a column, which in turn is wrapped by a SingleChildScrollView. This is an anti pattern, because I don't think that a SingleScrollView is meant to wrap a more efficient ListView.builder.

Until someone can confirm why this decision was made, I'm reluctant to make any changes because it might break something. But from what I can see moving it to a CustomScrollView with SliverList will likely make it much more efficient. Might have to lift the StreamBuilder higher up on the widget tree as well.

This is kind of a big change but a critical optimisation. Does anyone with a much more expertise on this package that can comment?

Here's the said code:

@override
  Widget build(BuildContext context) {
    final suggestionsListConfig =
        suggestionsConfig?.listConfig ?? const SuggestionListConfig();
    return SingleChildScrollView(
      reverse: true,
      // When reaction popup is being appeared at that user should not scroll.
      physics: showPopUp ? const NeverScrollableScrollPhysics() : null,
      controller: widget.scrollController,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          GestureDetector(
            onHorizontalDragUpdate: (details) =>
                isEnableSwipeToSeeTime && !showPopUp
                    ? _onHorizontalDrag(details)
                    : null,
            onHorizontalDragEnd: (details) =>
                isEnableSwipeToSeeTime && !showPopUp
                    ? _animationController?.reverse()
                    : null,
            onTap: widget.onChatListTap,
            child: _animationController != null
                ? AnimatedBuilder(
                    animation: _animationController!,
                    builder: (context, child) {
                      return _chatStreamBuilder;
                    },
                  )
                : _chatStreamBuilder,
          ),
          if (chatController != null)
            ValueListenableBuilder(
              valueListenable: chatController!.typingIndicatorNotifier,
              builder: (context, value, child) => TypingIndicator(
                typeIndicatorConfig: chatListConfig.typeIndicatorConfig,
                chatBubbleConfig:
                    chatListConfig.chatBubbleConfig?.inComingChatBubbleConfig,
                showIndicator: value,
              ),
            ),
          if (chatController != null)
            Flexible(
              child: Align(
                alignment: suggestionsListConfig.axisAlignment.alignment,
                child: const SuggestionList(),
              ),
            ),

          // Adds bottom space to the message list, ensuring it is displayed
          // above the message text field.
          SizedBox(
            height: chatTextFieldHeight,
          ),
        ],
      ),
    );
  }
 Widget get _chatStreamBuilder {
    DateTime lastMatchedDate = DateTime.now();
    return StreamBuilder<List<Message>>(
      stream: chatController?.messageStreamController.stream,
      builder: (context, snapshot) {
        if (!snapshot.connectionState.isActive) {
          return Center(
            child: chatBackgroundConfig.loadingWidget ??
                const CircularProgressIndicator(),
          );
        } else {
          final messages = chatBackgroundConfig.sortEnable
              ? sortMessage(snapshot.data!)
              : snapshot.data!;

          final enableSeparator =
              featureActiveConfig?.enableChatSeparator ?? false;

          Map<int, DateTime> messageSeparator = {};

          if (enableSeparator) {
            /// Get separator when date differ for two messages
            (messageSeparator, lastMatchedDate) = _getMessageSeparator(
              messages,
              lastMatchedDate,
            );
          }

          /// [count] that indicates how many separators
          /// needs to be display in chat
          var count = 0;

          return ListView.builder(
            key: widget.key,
            physics: const NeverScrollableScrollPhysics(),
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            itemCount: (enableSeparator
                ? messages.length + messageSeparator.length
                : messages.length),
            itemBuilder: (context, index) {
              /// By removing [count] from [index] will get actual index
              /// to display message in chat
              var newIndex = index - count;

              /// Check [messageSeparator] contains group separator for [index]
              if (enableSeparator && messageSeparator.containsKey(index)) {
                /// Increase counter each time
                /// after separating messages with separator
                count++;
                return _groupSeparator(
                  messageSeparator[index]!,
                );
              }

              return ValueListenableBuilder<String?>(
                valueListenable: _replyId,
                builder: (context, state, child) {
                  final message = messages[newIndex];
                  final enableScrollToRepliedMsg = chatListConfig
                          .repliedMessageConfig
                          ?.repliedMsgAutoScrollConfig
                          .enableScrollToRepliedMsg ??
                      false;
                  return ChatBubbleWidget(
                    key: message.key,
                    message: message,
                    slideAnimation: _slideAnimation,
                    onLongPress: (yCoordinate, xCoordinate) =>
                        widget.onChatBubbleLongPress(
                      yCoordinate,
                      xCoordinate,
                      message,
                    ),
                    onSwipe: widget.assignReplyMessage,
                    shouldHighlight: state == message.id,
                    onReplyTap: enableScrollToRepliedMsg
                        ? (replyId) => _onReplyTap(replyId, snapshot.data)
                        : null,
                  );
                },
              );
            },
          );
        }
      },
    );
  }

@lhengl
Copy link
Author

lhengl commented May 14, 2025

An update on this. After I changed the SingleChildScrollView to a CustomScrollView, I noticed a massive improvement in the speed and quality of the ChatView. For example loading hundreds and thousands of messages now are instant. Before it would have caused about a one second stutter delay. This is especially noticeable when opening the chat for the first time through routing, you'll see the animation of the routing is stuttering, indicating too much work is being done.

However, the logic in how the list is ordered is now incorrect. Before, the SingleChildScrollView's reverse property do not need to reverse the list in order to work because it laid out all its child view already. So the order of the list remain in tact. Now that we want to move to a build on the go list of sliver children, the ordering will need to be reversed in order to build the item one by one.

At the moment reversing the list seems to somewhat work, but:

  1. The date separators are now incorrectly placed in the list
  2. Scrolling may cause a RangeError. E.g.: RangeError (length): Invalid value: Not in inclusive range 0..11: -1
  3. And the latest message indicator seems to be applied to all message temporarily as we scroll.

I need help with this. Again, anyone with some expertise on the package can help would be great, because this is such a critical update to optimising the package. It's extremely inefficient to lay out the entire list in a single child scroll view.

Here's the forked branch for this: fix/ui-stutter

@lhengl
Copy link
Author

lhengl commented May 16, 2025

I've managed to fix the issues! I can now load hundreds and thousands of messages without any stuttering or memory inefficiency. I'm gonna play around with it a bit more to ensure it doesn't break anything. Feel free to test the fork here: fix/ui-stutter-caused-by-single-child-scrollview

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