skip to Main Content

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?

4

Answers


  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
      ),
    )
    
    Login or Signup to reply.
  2. 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),
    );
    
    Login or Signup to reply.
  3. 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.

    Login or Signup to reply.
  4. 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;
                  },
                ),
              ),
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search