Skip to content

Commit 793dbc7

Browse files
committed
store [nfc]: Separate "backend" class from store itself
This commit makes GlobalSettingsStore more self-contained, taking over from GlobalStore the responsibility for updating settings in the database at the same time as they get updated in the in-app cache [GlobalSettingsStore._data]. To do that, it needs to call doUpdateGlobalSettings, the abstract method that's implemented separately to use a live database in the real app vs. a vacuous fake backend in tests. But that means that method can no longer live on GlobalStore: for GlobalStore needs a GlobalSettingsStore, but then GlobalSettingsStore would need a GlobalStore, forming a cycle. In particular when calling the GlobalSettingsStore constructor from within the initializer list of the GlobalStore constructor, we can't pass `this` as an argument because `this` hasn't yet been constructed. (If we were determined to make such a cycle work, we could do so by having a late or nullable field. But let's take the cycle as a signal that the design can be cleaned up.) So, make a new class GlobalStoreBackend as the home for this abstract method. A GlobalStore now "has a" GlobalStoreBackend, rather than "is a" GlobalStoreBackend as it effectively was before. As a result we can readily construct a "backend" instance first, and pass that to the GlobalSettingsStore constructor, before going on to finish constructing the overall GlobalStore. Probably most or all of the other functionality that LiveGlobalStore implements should get moved to the new "backend" class too, but we'll leave that for possible follow-up changes.
1 parent c107fe8 commit 793dbc7

File tree

3 files changed

+65
-24
lines changed

3 files changed

+65
-24
lines changed

lib/model/settings.dart

+10-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
33
import '../generated/l10n/zulip_localizations.dart';
44
import 'binding.dart';
55
import 'database.dart';
6+
import 'store.dart';
67

78
/// The user's choice of visual theme for the app.
89
///
@@ -49,7 +50,12 @@ enum BrowserPreference {
4950
/// From UI code, use [GlobalStoreWidget.settingsOf] to get hold of
5051
/// an appropriate instance of this class.
5152
class GlobalSettingsStore extends ChangeNotifier {
52-
GlobalSettingsStore({required GlobalSettingsData data}) : _data = data;
53+
GlobalSettingsStore({
54+
required GlobalStoreBackend backend,
55+
required GlobalSettingsData data,
56+
}) : _backend = backend, _data = data;
57+
58+
final GlobalStoreBackend _backend;
5359

5460
/// A cache of the [GlobalSettingsData] singleton in the underlying data store.
5561
GlobalSettingsData _data;
@@ -104,9 +110,9 @@ class GlobalSettingsStore extends ChangeNotifier {
104110
}
105111
}
106112

107-
/// (Should only be called by [GlobalStore].)
108-
void update(GlobalSettingsCompanion data) {
109-
// TODO move responsibility for updating the DB to this class too
113+
/// Update the global settings in the store.
114+
Future<void> update(GlobalSettingsCompanion data) async {
115+
await _backend.doUpdateGlobalSettings(data);
110116
_data = _data.copyWithCompanion(data);
111117
notifyListeners();
112118
}

lib/model/store.dart

+48-17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ import 'user.dart';
3737
export 'package:drift/drift.dart' show Value;
3838
export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsException;
3939

40+
/// An underlying data store that can support a [GlobalStore],
41+
/// possibly storing the data to persist between runs of the app.
42+
///
43+
/// In the real app, the implementation used is [LiveGlobalStoreBackend],
44+
/// which stores data persistently in a database on the user's device.
45+
/// This interface enables tests to use a different implementation.
46+
abstract class GlobalStoreBackend {
47+
/// Update the global settings in the underlying data store.
48+
///
49+
/// This should only be called from [GlobalSettingsStore].
50+
Future<void> doUpdateGlobalSettings(GlobalSettingsCompanion data);
51+
52+
// TODO move here the similar methods for accounts;
53+
// perhaps the rest of the GlobalStore abstract methods, too.
54+
}
55+
4056
/// Store for all the user's data.
4157
///
4258
/// From UI code, use [GlobalStoreWidget.of] to get hold of an appropriate
@@ -55,10 +71,11 @@ export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsExce
5571
/// we use outside of tests.
5672
abstract class GlobalStore extends ChangeNotifier {
5773
GlobalStore({
74+
required GlobalStoreBackend backend,
5875
required GlobalSettingsData globalSettings,
5976
required Iterable<Account> accounts,
6077
})
61-
: settings = GlobalSettingsStore(data: globalSettings),
78+
: settings = GlobalSettingsStore(backend: backend, data: globalSettings),
6279
_accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a)));
6380

6481
/// The store for the user's account-independent settings.
@@ -70,16 +87,11 @@ abstract class GlobalStore extends ChangeNotifier {
7087
final GlobalSettingsStore settings;
7188

7289
/// Update the global settings in the store.
90+
// TODO inline this out
7391
Future<void> updateGlobalSettings(GlobalSettingsCompanion data) async {
74-
await doUpdateGlobalSettings(data);
75-
settings.update(data);
92+
await settings.update(data);
7693
}
7794

78-
/// Update the global settings in the underlying data store.
79-
///
80-
/// This should only be called from [updateGlobalSettings].
81-
Future<void> doUpdateGlobalSettings(GlobalSettingsCompanion data);
82-
8395
/// A cache of the [Accounts] table in the underlying data store.
8496
final Map<int, Account> _accounts;
8597

@@ -832,6 +844,23 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) {
832844
}
833845
}
834846

847+
/// A [GlobalStoreBackend] that uses a live, persistent local database.
848+
///
849+
/// Used as part of a [LiveGlobalStore].
850+
/// The underlying data store is an [AppDatabase] corresponding to a
851+
/// SQLite database file in the app's persistent storage on the device.
852+
class LiveGlobalStoreBackend implements GlobalStoreBackend {
853+
LiveGlobalStoreBackend._({required AppDatabase db}) : _db = db;
854+
855+
final AppDatabase _db;
856+
857+
@override
858+
Future<void> doUpdateGlobalSettings(GlobalSettingsCompanion data) async {
859+
final rowsAffected = await _db.update(_db.globalSettings).write(data);
860+
assert(rowsAffected == 1);
861+
}
862+
}
863+
835864
/// A [GlobalStore] that uses a live server and live, persistent local database.
836865
///
837866
/// The underlying data store is an [AppDatabase] corresponding to a SQLite
@@ -841,10 +870,11 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) {
841870
/// and will have an associated [UpdateMachine].
842871
class LiveGlobalStore extends GlobalStore {
843872
LiveGlobalStore._({
844-
required AppDatabase db,
873+
required LiveGlobalStoreBackend backend,
845874
required super.globalSettings,
846875
required super.accounts,
847-
}) : _db = db;
876+
}) : _backend = backend,
877+
super(backend: backend);
848878

849879
@override
850880
ApiConnection apiConnection({
@@ -861,7 +891,8 @@ class LiveGlobalStore extends GlobalStore {
861891
final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile()));
862892
final globalSettings = await db.getGlobalSettings();
863893
final accounts = await db.select(db.accounts).get();
864-
return LiveGlobalStore._(db: db,
894+
return LiveGlobalStore._(
895+
backend: LiveGlobalStoreBackend._(db: db),
865896
globalSettings: globalSettings,
866897
accounts: accounts);
867898
}
@@ -885,13 +916,13 @@ class LiveGlobalStore extends GlobalStore {
885916
return File(p.join(dir.path, 'zulip.db'));
886917
}
887918

888-
final AppDatabase _db;
919+
final LiveGlobalStoreBackend _backend;
889920

890-
@override
891-
Future<void> doUpdateGlobalSettings(GlobalSettingsCompanion data) async {
892-
final rowsAffected = await _db.update(_db.globalSettings).write(data);
893-
assert(rowsAffected == 1);
894-
}
921+
// The methods that use this should probably all move to [GlobalStoreBackend]
922+
// and [LiveGlobalStoreBackend] anyway (see comment on the former);
923+
// so let the latter be the canonical home of the [AppDatabase].
924+
// This getter just simplifies the transition.
925+
AppDatabase get _db => _backend._db;
895926

896927
@override
897928
Future<PerAccountStore> doLoadPerAccount(int accountId) async {

test/model/test_store.dart

+7-3
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ mixin _ApiConnectionsMixin on GlobalStore {
6262
}
6363
}
6464

65-
mixin _DatabaseMixin on GlobalStore {
65+
class _TestGlobalStoreBackend implements GlobalStoreBackend {
6666
@override
6767
Future<void> doUpdateGlobalSettings(GlobalSettingsCompanion data) async {
6868
// Nothing to do.
6969
}
70+
}
7071

72+
mixin _DatabaseMixin on GlobalStore {
7173
int _nextAccountId = 1;
7274

7375
@override
@@ -138,7 +140,8 @@ class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMi
138140
TestGlobalStore({
139141
GlobalSettingsData? globalSettings,
140142
required super.accounts,
141-
}) : super(globalSettings: globalSettings ?? GlobalSettingsData());
143+
}) : super(backend: _TestGlobalStoreBackend(),
144+
globalSettings: globalSettings ?? GlobalSettingsData());
142145

143146
final Map<int, InitialSnapshot> _initialSnapshots = {};
144147

@@ -202,7 +205,8 @@ class UpdateMachineTestGlobalStore extends GlobalStore with _ApiConnectionsMixin
202205
UpdateMachineTestGlobalStore({
203206
GlobalSettingsData? globalSettings,
204207
required super.accounts,
205-
}) : super(globalSettings: globalSettings ?? GlobalSettingsData());
208+
}) : super(backend: _TestGlobalStoreBackend(),
209+
globalSettings: globalSettings ?? GlobalSettingsData());
206210

207211
// [doLoadPerAccount] depends on the cache to prepare the API responses.
208212
// Calling [clearCachedApiConnections] is permitted, though.

0 commit comments

Comments
 (0)