Skip to content

Commit af0cc46

Browse files
committed
msglist: Ensure sole ownership of MessageListView
PerAccountStore shouldn't be an owner of the MessageListView elements. Its relationship to MessageListView is similar to that of AutocompleteViewManager to MentionAutocompleteView (zulip#645). With two owners, the MessageListView can be disposed twice: 1. before the frame is rendered, `removeAccount` disposes the `PerAccountStoreWidget`, which disposes the `MessageListView`; `_MessageListState` is not yet disposed; 2. during build, because `store` is set to `null`, `PerAccountStoreWidget` gets rebuilt. `_MessageListState`, a descendent of it, is no longer in the render tree; 3. during finalization, `_MessageListState` tries to dispose the `MessageListView`. This removes regression tests added for zulip#810, because `MessageStoreImpl.dispose` no longer exists. `MessageListView` does not get disposed unless there is a `_MessageListState` owner. Signed-off-by: Zixuan James Li <[email protected]>
1 parent def4988 commit af0cc46

File tree

6 files changed

+28
-42
lines changed

6 files changed

+28
-42
lines changed

lib/model/message.dart

+6-8
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,12 @@ class MessageStoreImpl with MessageStore {
6060
}
6161
}
6262

63-
void dispose() {
64-
// When a MessageListView is disposed, it removes itself from the Set
65-
// `MessageStoreImpl._messageListViews`. Instead of iterating on that Set,
66-
// iterate on a copy, to avoid concurrent modifications.
67-
for (final view in _messageListViews.toList()) {
68-
view.dispose();
69-
}
70-
}
63+
// No `dispose` method, because there's nothing for it to do.
64+
// The [MessageListView]s are owned by (i.e., they get [dispose]d by)
65+
// the [_MessageListState], including in the case where the [PerAccountStore]
66+
// is replaced. Discussion:
67+
// https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/.60MentionAutocompleteView.2Edispose.60/near/2083074
68+
// void dispose() { … }
7169

7270
@override
7371
void reconcileMessages(List<Message> messages) {

lib/model/store.dart

-1
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,6 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
557557
assert(!_disposed);
558558
recentDmConversationsView.dispose();
559559
unreads.dispose();
560-
_messages.dispose();
561560
typingStatus.dispose();
562561
typingNotifier.dispose();
563562
updateMachine?.dispose();

lib/widgets/message_list.dart

+1
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
483483

484484
@override
485485
void onNewStore() { // TODO(#464) try to keep using old model until new one gets messages
486+
model?.dispose();
486487
_initModel(PerAccountStoreWidget.of(context));
487488
}
488489

test/model/message_test.dart

-12
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,6 @@ void main() {
7777
checkNotified(count: messageList.fetched ? messages.length : 0);
7878
}
7979

80-
test('disposing multiple registered MessageListView instances', () async {
81-
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
82-
await prepare(narrow: const MentionsNarrow());
83-
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
84-
check(store.debugMessageListViews).length.equals(2);
85-
86-
// When disposing, the [MessageListView]s are expected to unregister
87-
// themselves from the message store.
88-
store.dispose();
89-
check(store.debugMessageListViews).isEmpty();
90-
});
91-
9280
group('reconcileMessages', () {
9381
test('from empty', () async {
9482
await prepare();

test/model/store_test.dart

-21
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import 'package:zulip/api/model/model.dart';
1212
import 'package:zulip/api/route/events.dart';
1313
import 'package:zulip/api/route/messages.dart';
1414
import 'package:zulip/api/route/realm.dart';
15-
import 'package:zulip/model/message_list.dart';
16-
import 'package:zulip/model/narrow.dart';
1715
import 'package:zulip/log.dart';
1816
import 'package:zulip/model/store.dart';
1917
import 'package:zulip/notifications/receive.dart';
@@ -824,25 +822,6 @@ void main() {
824822
checkReload(prepareHandleEventError);
825823
});
826824

827-
test('expired queue disposes registered MessageListView instances', () => awaitFakeAsync((async) async {
828-
// Regression test for: https://github.com/zulip/zulip-flutter/issues/810
829-
await preparePoll();
830-
831-
// Make sure there are [MessageListView]s in the message store.
832-
MessageListView.init(store: store, narrow: const MentionsNarrow());
833-
MessageListView.init(store: store, narrow: const StarredMessagesNarrow());
834-
check(store.debugMessageListViews).length.equals(2);
835-
836-
// Let the server expire the event queue.
837-
prepareExpiredEventQueue();
838-
updateMachine.debugAdvanceLoop();
839-
async.elapse(Duration.zero);
840-
841-
// The old store's [MessageListView]s have been disposed.
842-
// (And no exception was thrown; that was #810.)
843-
check(store.debugMessageListViews).isEmpty();
844-
}));
845-
846825
group('report error', () {
847826
String? lastReportedError;
848827
String? takeLastReportedError() {

test/widgets/message_list_test.dart

+21
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:zulip/api/model/initial_snapshot.dart';
1212
import 'package:zulip/api/model/model.dart';
1313
import 'package:zulip/api/model/narrow.dart';
1414
import 'package:zulip/api/route/messages.dart';
15+
import 'package:zulip/model/actions.dart';
1516
import 'package:zulip/model/localizations.dart';
1617
import 'package:zulip/model/narrow.dart';
1718
import 'package:zulip/model/store.dart';
@@ -130,6 +131,26 @@ void main() {
130131
final state = MessageListPage.ancestorOf(tester.element(find.text("a message")));
131132
check(state.composeBoxController).isNull();
132133
});
134+
135+
testWidgets('dispose MessageListView when logged out', (tester) async {
136+
addTearDown(testBinding.reset);
137+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
138+
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
139+
(store.connection as FakeApiConnection).prepare(json: eg.newestGetMessagesResult(
140+
foundOldest: true, messages: [eg.streamMessage()]).toJson());
141+
await tester.pumpWidget(TestZulipApp(
142+
accountId: eg.selfAccount.id,
143+
skipAssertAccountExists: true,
144+
child: MessageListPage(initNarrow: const CombinedFeedNarrow())));
145+
await tester.pump();
146+
await tester.pump();
147+
check(store.debugMessageListViews).single;
148+
149+
final future = logOutAccount(testBinding.globalStore, eg.selfAccount.id);
150+
await tester.pump(TestGlobalStore.removeAccountDuration);
151+
await future;
152+
check(store.debugMessageListViews).isEmpty();
153+
});
133154
});
134155

135156
group('app bar', () {

0 commit comments

Comments
 (0)