Skip to content

Commit 20e6b9e

Browse files
committed
msglist: Ensure sole ownership of MessageListView
PerAccountStore shouldn't be an owner of the MessageListView objects. Its relationship to MessageListView is similar to that of AutocompleteViewManager to MentionAutocompleteView (zulip#645). With two owners, MessageListView can be disposed twice: 1. before the frame is rendered, `GlobalStore.removeAccount` disposes the `PerAccountStore`, which disposes the `MessageListView` (via `MessageStoreImpl`), notifying listeners of `GlobalStore`. At this point `_MessageListState` is not yet disposed. 2. As a dependent of `GlobalStore`, a rebuilt is triggered for `PerAccountStoreWidget`. This time, the `MessageList` Element is no longer in the element tree 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. See discussion: https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/.60MentionAutocompleteView.2Edispose.60/near/2083074 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 6836c84 commit 20e6b9e

File tree

5 files changed

+30
-19
lines changed

5 files changed

+30
-19
lines changed

lib/model/message.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@ class MessageStoreImpl with MessageStore {
6161
}
6262

6363
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-
}
64+
// No `dispose` method, because there's nothing for it to do.
65+
// The [MessageListView]s are owned by (i.e., they get [dispose]d by)
66+
// the [_MessageListState], including in the case where the [PerAccountStore]
67+
// is replaced. Discussion:
68+
// https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/.60MentionAutocompleteView.2Edispose.60/near/2083074
69+
assert(_messageListViews.isEmpty,
70+
'Unexpected [MessageListView]s;\n'
71+
'all should have been disposed by [_MessageListState]');
7072
}
7173

7274
@override

lib/model/store.dart

Lines changed: 0 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 0 additions & 12 deletions
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/widgets/message_list_test.dart

Lines changed: 21 additions & 0 deletions
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)