Skip to content

Commit d1f648c

Browse files
app: Use DeferredBuilderWidget while loading GlobalStore
Moves the actual loading of GlobalStore from GlobalStoreWidget to _ZulipAppState.initState. This will allow to do some other tasks concurrently while the GlobalStore is loading and showing the placeholder while they complete.
1 parent cb9a849 commit d1f648c

File tree

5 files changed

+236
-201
lines changed

5 files changed

+236
-201
lines changed

lib/widgets/app.dart

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter/scheduler.dart';
77
import '../generated/l10n/zulip_localizations.dart';
88
import '../log.dart';
99
import '../model/actions.dart';
10+
import '../model/binding.dart';
1011
import '../model/localizations.dart';
1112
import '../model/store.dart';
1213
import '../notifications/display.dart';
@@ -151,10 +152,13 @@ class ZulipApp extends StatefulWidget {
151152
}
152153

153154
class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
155+
late final Future<GlobalStore> _globalStoreFuture;
156+
154157
@override
155158
void initState() {
156159
super.initState();
157160
WidgetsBinding.instance.addObserver(this);
161+
_globalStoreFuture = ZulipBinding.instance.getGlobalStoreUniquely();
158162
}
159163

160164
@override
@@ -212,44 +216,101 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
212216

213217
@override
214218
Widget build(BuildContext context) {
215-
return GlobalStoreWidget(
216-
child: Builder(builder: (context) {
217-
return MaterialApp(
218-
onGenerateTitle: (BuildContext context) {
219-
return ZulipLocalizations.of(context).zulipAppTitle;
220-
},
221-
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
222-
supportedLocales: ZulipLocalizations.supportedLocales,
223-
// The context has to be taken from the [Builder] because
224-
// [zulipThemeData] requires access to [GlobalStoreWidget] in the tree.
225-
theme: zulipThemeData(context),
226-
227-
navigatorKey: ZulipApp.navigatorKey,
228-
navigatorObservers: [
229-
if (widget.navigatorObservers != null)
230-
...widget.navigatorObservers!,
231-
_PreventEmptyStack(),
232-
],
233-
builder: (BuildContext context, Widget? child) {
234-
if (!ZulipApp.ready.value) {
235-
SchedulerBinding.instance.addPostFrameCallback(
236-
(_) => widget._declareReady());
237-
}
238-
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
239-
return child!;
240-
},
219+
return DeferredBuilderWidget(
220+
future: _globalStoreFuture,
221+
builder: (context, store) {
222+
return GlobalStoreWidget(
223+
store: store,
224+
child: Builder(builder: (context) {
225+
return MaterialApp(
226+
onGenerateTitle: (BuildContext context) {
227+
return ZulipLocalizations.of(context).zulipAppTitle;
228+
},
229+
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
230+
supportedLocales: ZulipLocalizations.supportedLocales,
231+
// The context has to be taken from the [Builder] because
232+
// [zulipThemeData] requires access to [GlobalStoreWidget] in the tree.
233+
theme: zulipThemeData(context),
234+
235+
navigatorKey: ZulipApp.navigatorKey,
236+
navigatorObservers: [
237+
if (widget.navigatorObservers != null)
238+
...widget.navigatorObservers!,
239+
_PreventEmptyStack(),
240+
],
241+
builder: (BuildContext context, Widget? child) {
242+
if (!ZulipApp.ready.value) {
243+
SchedulerBinding.instance.addPostFrameCallback(
244+
(_) => widget._declareReady());
245+
}
246+
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
247+
return child!;
248+
},
249+
250+
// We use onGenerateInitialRoutes for the real work of specifying the
251+
// initial nav state. To do that we need [MaterialApp] to decide to
252+
// build a [Navigator]... which means specifying either `home`, `routes`,
253+
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`.
254+
// It never actually gets called, though: `onGenerateInitialRoutes`
255+
// handles startup, and then we always push whole routes with methods
256+
// like [Navigator.push], never mere names as with [Navigator.pushNamed].
257+
onGenerateRoute: (_) => null,
258+
259+
onGenerateInitialRoutes: _handleGenerateInitialRoutes);
260+
}));
261+
});
262+
}
263+
}
264+
265+
/// A widget that defers the builder until the provided [future] completes.
266+
///
267+
/// It shows a placeholder widget while it waits for the [future]
268+
/// to complete.
269+
class DeferredBuilderWidget<T> extends StatefulWidget {
270+
const DeferredBuilderWidget({
271+
super.key,
272+
required this.future,
273+
required this.builder,
274+
this.placeholderBuilder = _defaultPlaceHolderBuilder,
275+
});
276+
277+
final Future<T> future;
278+
279+
/// The widget to build when [future] completes, with it's result
280+
/// passed as `result`.
281+
final Widget Function(BuildContext context, T result) builder;
282+
283+
/// The placeholder widget to build while waiting for the [future]
284+
/// to complete.
285+
///
286+
/// By default, it will build the [LoadingPlaceholder].
287+
final Widget Function(BuildContext context) placeholderBuilder;
241288

242-
// We use onGenerateInitialRoutes for the real work of specifying the
243-
// initial nav state. To do that we need [MaterialApp] to decide to
244-
// build a [Navigator]... which means specifying either `home`, `routes`,
245-
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`.
246-
// It never actually gets called, though: `onGenerateInitialRoutes`
247-
// handles startup, and then we always push whole routes with methods
248-
// like [Navigator.push], never mere names as with [Navigator.pushNamed].
249-
onGenerateRoute: (_) => null,
250-
251-
onGenerateInitialRoutes: _handleGenerateInitialRoutes);
252-
}));
289+
static Widget _defaultPlaceHolderBuilder(BuildContext context) {
290+
return const LoadingPlaceholder();
291+
}
292+
293+
@override
294+
State<DeferredBuilderWidget<T>> createState() => _DeferredBuilderWidgetState<T>();
295+
}
296+
297+
class _DeferredBuilderWidgetState<T> extends State<DeferredBuilderWidget<T>> {
298+
T? _result;
299+
300+
@override
301+
void initState() {
302+
super.initState();
303+
() async {
304+
_result = await widget.future;
305+
if (mounted) setState(() {});
306+
}();
307+
}
308+
309+
@override
310+
Widget build(BuildContext context) {
311+
final result = _result;
312+
if (result == null) return widget.placeholderBuilder(context);
313+
return widget.builder(context, result);
253314
}
254315
}
255316

lib/widgets/store.dart

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/scheduler.dart';
33

4-
import '../model/binding.dart';
5-
import '../model/database.dart';
64
import '../model/settings.dart';
75
import '../model/store.dart';
86
import 'page.dart';
@@ -15,15 +13,14 @@ import 'page.dart';
1513
/// * [GlobalStoreWidget.of], to get access to the data.
1614
/// * [PerAccountStoreWidget], for the user's data associated with a
1715
/// particular Zulip account.
18-
class GlobalStoreWidget extends StatefulWidget {
19-
const GlobalStoreWidget({
16+
class GlobalStoreWidget extends InheritedNotifier<GlobalStore> {
17+
GlobalStoreWidget({
2018
super.key,
21-
this.placeholder = const LoadingPlaceholder(),
22-
required this.child,
23-
});
24-
25-
final Widget placeholder;
26-
final Widget child;
19+
required GlobalStore store,
20+
required Widget child,
21+
}) : super(notifier: store,
22+
child: _GlobalSettingsStoreInheritedWidget(
23+
store: store.settings, child: child));
2724

2825
/// The app's global data store.
2926
///
@@ -48,7 +45,7 @@ class GlobalStoreWidget extends StatefulWidget {
4845
/// * [PerAccountStoreWidget.of], for the user's data associated with a
4946
/// particular Zulip account.
5047
static GlobalStore of(BuildContext context) {
51-
final widget = context.dependOnInheritedWidgetOfExactType<_GlobalStoreInheritedWidget>();
48+
final widget = context.dependOnInheritedWidgetOfExactType<GlobalStoreWidget>();
5249
assert(widget != null, 'No GlobalStoreWidget ancestor');
5350
return widget!.store;
5451
}
@@ -75,43 +72,6 @@ class GlobalStoreWidget extends StatefulWidget {
7572
return widget!.store;
7673
}
7774

78-
@override
79-
State<GlobalStoreWidget> createState() => _GlobalStoreWidgetState();
80-
}
81-
82-
class _GlobalStoreWidgetState extends State<GlobalStoreWidget> {
83-
GlobalStore? store;
84-
85-
@override
86-
void initState() {
87-
super.initState();
88-
(() async {
89-
final store = await ZulipBinding.instance.getGlobalStoreUniquely();
90-
setState(() {
91-
this.store = store;
92-
});
93-
})();
94-
}
95-
96-
@override
97-
Widget build(BuildContext context) {
98-
final store = this.store;
99-
if (store == null) return widget.placeholder;
100-
return _GlobalStoreInheritedWidget(store: store, child: widget.child);
101-
}
102-
}
103-
104-
// This is separate from [GlobalStoreWidget] only because we need
105-
// a [StatefulWidget] to get hold of the store, and an [InheritedWidget] to
106-
// provide it to descendants, and one widget can't be both of those.
107-
class _GlobalStoreInheritedWidget extends InheritedNotifier<GlobalStore> {
108-
_GlobalStoreInheritedWidget({
109-
required GlobalStore store,
110-
required Widget child,
111-
}) : super(notifier: store,
112-
child: _GlobalSettingsStoreInheritedWidget(
113-
store: store.settings, child: child));
114-
11575
GlobalStore get store => notifier!;
11676
}
11777

test/widgets/content_test.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import 'package:zulip/widgets/content.dart';
1515
import 'package:zulip/widgets/icons.dart';
1616
import 'package:zulip/widgets/message_list.dart';
1717
import 'package:zulip/widgets/page.dart';
18-
import 'package:zulip/widgets/store.dart';
1918
import 'package:zulip/widgets/text.dart';
2019

2120
import '../example_data.dart' as eg;
@@ -1139,9 +1138,9 @@ void main() {
11391138

11401139
final httpClient = prepareBoringImageHttpClient();
11411140

1142-
await tester.pumpWidget(GlobalStoreWidget(
1143-
child: PerAccountStoreWidget(accountId: eg.selfAccount.id,
1144-
child: RealmContentNetworkImage(src))));
1141+
await tester.pumpWidget(TestZulipApp(
1142+
accountId: eg.selfAccount.id,
1143+
child: RealmContentNetworkImage(src)));
11451144
await tester.pump();
11461145
await tester.pump();
11471146

@@ -1181,9 +1180,9 @@ void main() {
11811180
await store.addUser(user);
11821181

11831182
prepareBoringImageHttpClient();
1184-
await tester.pumpWidget(GlobalStoreWidget(
1185-
child: PerAccountStoreWidget(accountId: eg.selfAccount.id,
1186-
child: AvatarImage(userId: user.userId, size: size ?? 30))));
1183+
await tester.pumpWidget(TestZulipApp(
1184+
accountId: eg.selfAccount.id,
1185+
child: AvatarImage(userId: user.userId, size: size ?? 30)));
11871186
await tester.pump();
11881187
await tester.pump();
11891188
tester.widget(find.byType(AvatarImage));

0 commit comments

Comments
 (0)