Skip to content

Commit

Permalink
msglist: Maintain _UnreadMarker animation state when MessageListView.…
Browse files Browse the repository at this point in the history
…items changes size
  • Loading branch information
sirpengi committed Oct 30, 2023
1 parent e5d1c2a commit bdba1f6
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 0 deletions.
21 changes: 21 additions & 0 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,27 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
_ => ScrollViewKeyboardDismissBehavior.manual,
},

// To preserve state across rebuilds for individual [MessageItem]
// widgets as the size of [MessageListView.items] changes we need
// to match old widgets by their key to their new position in
// the list.
//
// The keys are of type [ValueKey] with a value of [Message.id]
// and here we use a O(log n) binary search method. This could
// be improved but for now it only triggers for materialized
// widgets. As a simple test, flinging through All Messages in
// CZO on a Pixel 5, this only runs about 10 times per rebuild
// and the timing for each call is <100 microseconds.
//
// Non-message items (e.g., start and end markers) that do not
// have state that needs to be preserved have not been given keys
// and will not trigger this callback.
findChildIndexCallback: (Key key) {
final valueKey = key as ValueKey;
final index = model!.findItemWithMessageId(valueKey.value);
if (index == -1) return null;
return length - 1 - index;
},
controller: scrollController,
itemCount: length,
// Setting reverse: true means the scroll starts at the bottom.
Expand Down
52 changes: 52 additions & 0 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,57 @@ void main() {
..value.equals(0.0)
..status.equals(AnimationStatus.completed);
});

testWidgets('animation state persistence', (WidgetTester tester) async {
// Check that _UnreadMarker maintains its in-progress animation
// as the number of items changes in MessageList. See
// findChildIndexCallback on StickyHeaderListView.builder for
//
final message = eg.streamMessage(flags: []);
await setupMessageListPage(tester, messages: [message]);
check(getAnimation(tester, message.id))
..value.equals(1.0)
..status.equals(AnimationStatus.dismissed);

store.handleEvent(UpdateMessageFlagsAddEvent(
id: 0,
flag: MessageFlag.read,
messages: [message.id],
all: false,
));
await tester.pump(); // process handleEvent
check(getAnimation(tester, message.id))
..value.equals(1.0)
..status.equals(AnimationStatus.forward);

// run animation partially
await tester.pump(const Duration(milliseconds: 30));
check(getAnimation(tester, message.id))
..value.isGreaterThan(0.0)
..value.isLessThan(1.0)
..status.equals(AnimationStatus.forward);

// introduce new message
final newMessage = eg.streamMessage(flags:[MessageFlag.read]);
store.handleEvent(MessageEvent(id: 0, message: newMessage));
await tester.pump(); // process handleEvent
check(find.byType(MessageItem).evaluate()).length.equals(2);
check(getAnimation(tester, message.id))
..value.isGreaterThan(0.0)
..value.isLessThan(1.0)
..status.equals(AnimationStatus.forward);
check(getAnimation(tester, newMessage.id))
..value.equals(0.0)
..status.equals(AnimationStatus.dismissed);

final frames = await tester.pumpAndSettle();
check(frames).isGreaterThan(1);
check(getAnimation(tester, message.id))
..value.equals(0.0)
..status.equals(AnimationStatus.completed);
check(getAnimation(tester, newMessage.id))
..value.equals(0.0)
..status.equals(AnimationStatus.dismissed);
});
});
}

0 comments on commit bdba1f6

Please sign in to comment.