Skip to content

Commit

Permalink
store [nfc]: Introduce DataBinding
Browse files Browse the repository at this point in the history
This indirection will be useful as a customization point
for use in tests.

This provides the core of zulip#30: uses of PerAccountStoreWidget are now
testable, in that subsequent commits in this series will start writing
such tests and won't require any further changes in lib/, only test/.
  • Loading branch information
gnprice committed Jun 12, 2023
1 parent b64f76e commit f661af4
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';

import 'licenses.dart';
import 'log.dart';
import 'model/binding.dart';
import 'widgets/app.dart';

void main() {
Expand All @@ -11,5 +12,6 @@ void main() {
return true;
}());
LicenseRegistry.addLicense(additionalLicenses);
LiveDataBinding.ensureInitialized();
runApp(const ZulipApp());
}
81 changes: 81 additions & 0 deletions lib/model/binding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:flutter/foundation.dart';

import '../widgets/store.dart';
import 'store.dart';

/// A singleton service providing the app's data.
///
/// Only one instance will be constructed in the lifetime of the app,
/// by calling the `ensureInitialized` static method on a subclass.
/// This instance can be accessed as [instance].
///
/// Most code should not interact with the bindings directly.
/// Instead, use the corresponding higher-level APIs that expose the bindings'
/// functionality in a widget-oriented way.
///
/// This piece of architecture is modelled on the "binding" classes in Flutter
/// itself. For discussion, see [BindingBase], [WidgetsFlutterBinding], and
/// [TestWidgetsFlutterBinding].
/// This version is simplified because we don't (yet?) have enough complexity
/// to put into these bindings to need to use mixins to split them up.
abstract class DataBinding {
DataBinding() {
assert(_instance == null);
initInstance();
}

/// The single instance of [DataBinding].
static DataBinding get instance => checkInstance(_instance);
static DataBinding? _instance;

static T checkInstance<T extends DataBinding>(T? instance) {
assert(() {
if (instance == null) {
throw FlutterError.fromParts([
ErrorSummary('Zulip binding has not yet been initialized.'),
ErrorHint(
'In the app, this is done by the `LiveDataBinding.ensureInitialized()` call '
'in the `void main()` method.',
),
]);
}
return true;
}());
return instance!;
}

@protected
@mustCallSuper
void initInstance() {
_instance = this;
}

/// Prepare the app's [GlobalStore], loading the necessary data.
///
/// Generally the app should call this function only once.
///
/// This is part of the implementation of [GlobalStoreWidget].
/// In application code, use [GlobalStoreWidget.of] to get access
/// to a [GlobalStore].
Future<GlobalStore> loadGlobalStore();
}

/// A concrete binding for use in the live application.
///
/// The global store returned by [loadGlobalStore], and consequently by
/// [GlobalStoreWidget.of] in application code, will be a [LiveGlobalStore].
/// It therefore uses a live server and live, persistent local database.
class LiveDataBinding extends DataBinding {
/// Initialize the binding if necessary, and ensure it is a [LiveDataBinding].
static LiveDataBinding ensureInitialized() {
if (DataBinding._instance == null) {
LiveDataBinding();
}
return DataBinding.instance as LiveDataBinding;
}

@override
Future<GlobalStore> loadGlobalStore() {
return LiveGlobalStore.load();
}
}
3 changes: 2 additions & 1 deletion lib/widgets/store.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';

import '../model/binding.dart';
import '../model/store.dart';

/// Provides access to the app's data.
Expand Down Expand Up @@ -54,7 +55,7 @@ class _GlobalStoreWidgetState extends State<GlobalStoreWidget> {
void initState() {
super.initState();
(() async {
final store = await LiveGlobalStore.load();
final store = await DataBinding.instance.loadGlobalStore();
setState(() {
this.store = store;
});
Expand Down

0 comments on commit f661af4

Please sign in to comment.