1

I'm using GroupedListView (which is based on ListView.builder) to implement a chat.

I have a ChatBloc bloc that is responsible to handle new messages from the DataLayer. Whenever new messages are added, I'm emitting a new state of elements, and rebuild the whole ListView.

Here is my code:

child: BlocBuilder<ChatBloc, ChatState>(
  builder: (context, state) {
    final userId = context.read<AppBloc>().state.user.userId;
    return GroupedListView<Message, DateTime>(
    controller: scrollController,
    elements: state.messages,
    reverse: true,
    cacheExtent: 9999,
    order: GroupedListOrder.DESC,
    itemComparator: (element1, element2) =>
      element1.timestamp.compareTo(element2.timestamp),
    itemBuilder: (context, element) =>
      element.messageToWidget(userId, state.senders),
    groupBy: (element) => DateTime(
      element.timestamp.year,
      element.timestamp.month,
      element.timestamp.day,
    ),
    groupHeaderBuilder: (element) => Center(
    child: TitleMessage(
    text: dateFormat03.format(element.timestamp),
    ),
   ),
  );
 },
)

The problem is that I don't want to rebuild everything again with every new message that is added to the list.

I want to build new messages only.

How do I do that using Bloc state management?

Update

According to @Harsimran Singh ListView.builder does not recycle its elements on refresh (even due adding a unique Key to each element).

Using ListView.custom with SliverChildBuilderDelegate seems not to solve the issue:

enter image description here

Here is my code:

return ListView.custom(
    physics: const AlwaysScrollableScrollPhysics(),
    childrenDelegate: SliverChildBuilderDelegate(
      (context, index) {
        final message = state.messages.elementAt(index);
        return message.messageToWidget(
          userId: userId,
          senders: state.senders,
          key: Key(message.toString()),
        );
      },
      childCount: state.messages.length,
      findChildIndexCallback: (key) {
        return state.messages
            .indexWhere((e) => Key(e.toString()) == key);
      },
    ),
  );

I debugged the code and the findChildIndexCallback works just fine and returns the correct index. But according to the above video, you can see that the elements are still rendered.

Why is that?

genericUser
  • 4,417
  • 1
  • 28
  • 73
  • I'd use a copy with state for the bloc, add you bloc snippet here. Include the state. – griffins Dec 15 '22 at 16:50
  • I'm using copyWith, but that's not the issue. My problem is that whenever a new state emits from my ChatBloc, with an updated list of elements, my ListView.builder builds everything again, instead of building the new messages only. – genericUser Dec 15 '22 at 16:52
  • You're probably not emitting a new state on new data instead of copying old state. – griffins Dec 15 '22 at 16:55
  • Why do you think that? here is the emit code emit( state.copyWith( status: ChatStatus.success, messages: messages, senders: senders, ), ); – genericUser Dec 15 '22 at 16:57
  • I don't think there is away to get this behaviour with bloc but it's possible with riverpod. – Mohammed Alfateh Dec 19 '22 at 14:07
  • How does it work with riverpod? what's the difference? – genericUser Dec 19 '22 at 14:20
  • `cacheExtent:9999` means you want to build `9999` pixels before of showing item and `9999` pixels after of showing item. more info at https://api.flutter.dev/flutter/widgets/ScrollView/cacheExtent.html – Sajjad Dec 20 '22 at 09:25
  • I think you supposed to reduce `cacheExtent` to (normal mobile height ~ 2778 ) for example `cacheExtent : 2778` – Sajjad Dec 20 '22 at 09:33
  • How does it relate to this discussion? – genericUser Dec 20 '22 at 10:03
  • @genericUser listView.builder by default would build necessary items if you set right `cacheExtent`. when you add a message to list you have to build the list again and listview.builder would decide to build which items again. Then flutter engine will decide which items are `dirty` and need to rebuild and can't reuse. – Sajjad Dec 21 '22 at 05:36
  • so to improve performance of your list I suggest to `reduce cacheExtent ` and use `const` keyword for your Message Shower widget . `const` keyword prevent rebuild a widget if it exist in widget tree. more info at https://stackoverflow.com/questions/53492705/does-using-const-in-the-widget-tree-improve-performance – Sajjad Dec 21 '22 at 05:49
  • Yes but the question is not about how to build my widgets faster, it's about how not to rebuild them at first. – genericUser Dec 21 '22 at 09:44

4 Answers4

1

Actually the problem is not related to bloc at all. In fact ListView.builder will always rebuild all of its children, independently to setting a key for them.

The solution I found is to use a ListView.custom with a SliverChildBuilderDelegate. Then assign a key to its children and retrieving the key from the index works as expected and the child is not rebuilt.

Note that childCount and findChildIndexCallback are parameters of the sliver and not the list itself.

Here is an example:

ListView.custom(
            physics: const AlwaysScrollableScrollPhysics(),
            childrenDelegate: SliverChildBuilderDelegate(
              (context, index) {
                return MessageWidget(
                        key: ValueKey('m-${state.messages[index].messageId}'
                           ),
                        message: state.messages[index],
                      );
              },
              childCount: state.messages.length,
              findChildIndexCallback: (key) {
                final ValueKey<String> valueKey = key as ValueKey<String>;
                final index = state.messages
                    .indexWhere((m) => 'm-${m.messageId}' == valueKey.value);
                return index;
              },
            ),
          ),
Harsimran Singh
  • 269
  • 1
  • 7
  • Didn't try it yet but it sounds good. Why it doesn't work using ListView.builder with key for each element? What is the difference? – genericUser Dec 25 '22 at 17:37
  • 1
    Regardless of key listview.builder will rebuild the childeran. When a child is scrolled out of view, the associated element subtree, states and render objects are destroyed. A new child at the same position in the list will be lazily recreated along with new elements, states and render objects when it is scrolled back. Source: https://api.flutter.dev/flutter/widgets/ListView-class.html (Child elements' lifecycle) – Harsimran Singh Dec 25 '22 at 18:37
  • Hey @Harsimran Singh thanks for your reply. `SliverChildBuilderDelegate` seems not to solve my issue. I think you are at the right path, but it seems that my elements are still rendering. I updated my question based on your answer and added a gif that show the problem is still exists. – genericUser Dec 26 '22 at 10:22
  • Can you please give a message ID instead of a message as a key? The reason that is happening is due to your key. – Harsimran Singh Dec 26 '22 at 10:38
  • The comparing is working just fine. Each message has different unique fields, and I'm using `Equatable` with all fields to compare them. I debug each time my messages render, and the returned index from `findChildIndexCallback` was correct. I also tried using `message.id` or `message.timestamp` (since it's unique) for comparing but it didn't help. Can you please provide a simple example that works? Try to debug it and check if its elements are still rendering on each refresh. Thank you for your help. Appriciate – genericUser Dec 26 '22 at 12:33
  • please check if both `message.timestamp` and `message.id` are the same as before and after `setState()`. If the key is different then it will rerender the children. – Harsimran Singh Dec 26 '22 at 13:49
  • Messages fields are independent of `setState` refreshing. I also mentioned that the findChildIndexCallback works just fine and returns the right index. Which means the Key does not change and the compering works as expected. – genericUser Dec 26 '22 at 15:20
  • Can you please update your answer with working `SliverChildBuilderDelegate` that does not render its children on every `setState`? I think it will help me to understand what I did wrong. – genericUser Dec 26 '22 at 15:22
  • @genericUser I tested your code on my device and it works fine there may be any other issue. can you share anything outside the list view? that may help to debug – Harsimran Singh Dec 26 '22 at 15:55
  • Hello @genericUser if you are still stuck please let me know else please accept the answer :) Thanks. – Harsimran Singh Dec 28 '22 at 17:40
0

You can listen to your blocStream in initState and use animated list instead of your current list so you can insert items in real-time

It can be done by defining a global key like this:

final GlobalKey<AnimatedListState> messagesKey = GlobalKey<AnimatedListState>();

and then insert the value like:

messagesList.add(NEW_MESSAGE);
messagesKey.currentState?.insertItem(messagesList.length - 1);

and if you want to remove an item with animations you can:

  messagesKey.currentState?.removeItem(
  index,
  (context, animation) => buildMessage(index, animation),
);
Parsa Barati
  • 73
  • 1
  • 1
  • 5
0

I think you can define and use chat object like this "context.read().state.user.userId;" instead when using from state. It will update only the widget not the build method.

Humza Ali
  • 86
  • 4
  • `read()` works only once. In this case, since I want to listen for changes and refresh the widget, `select()` should be the right function. But it's irrelevant since BlocBuilder works the same as `select()`. My issue is about the problem of building the whole ListView again after adding an element to the list. That's my issue. – genericUser Dec 21 '22 at 15:08
-1

Don't wrap your ListView with a bloc builder in that case, use BlocListener

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    setState((){
       // Have a copy of messages in your widget and update it as you wish (implement getNewMessages)
       messages = getNewMessages(state.messages)
    })
  },
  child: ListView.builder(
     elements: messages
  ),
)