From e0d418bad710edde2cacdf998d6c127b1626faa9 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 6 Mar 2023 12:03:33 -0800 Subject: [PATCH 1/4] store [nfc]: Fix some formatting mangled by Android Studio On copy-paste (or cut-and-paste), Android Studio tries to automatically adjust indentation, and sometimes it makes a mess of it. That happened when this code was moved from widgets/app.dart. --- lib/widgets/store.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index 86bb2452a9..f5a4894963 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -116,8 +116,8 @@ class PerAccountStoreWidget extends InheritedNotifier { PerAccountStore get store => notifier!; static PerAccountStore of(BuildContext context) { - final widget = - context.dependOnInheritedWidgetOfExactType(); + final widget = context + .dependOnInheritedWidgetOfExactType(); assert(widget != null, 'No PerAccountStoreWidget ancestor'); return widget!.store; } From 4406b3a2a1bbf5e999c067bfb8e3961445155bfd Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 6 Mar 2023 11:51:50 -0800 Subject: [PATCH 2/4] store [nfc]: Rename things to expose just one PerAccountStoreWidget type Rather than having the rest of the app refer to PerAccountRoot for one aspect of this API and PerAccountStoreWidget for another. --- lib/model/store.dart | 4 ++-- lib/widgets/app.dart | 2 +- lib/widgets/store.dart | 35 +++++++++++++++++++---------------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/model/store.dart b/lib/model/store.dart index 96d3080106..b3ebeeb4c0 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -48,9 +48,9 @@ class GlobalStore extends ChangeNotifier { // Future updateAccount... // TODO add a registry of [PerAccountStore]s, like the latter's of [MessageListView] - // That will allow us to have many [PerAccountRoot] widgets for a given + // That will allow us to have many [PerAccountStoreWidget]s for a given // account, e.g. at the top of each page; and to access server data from - // outside any [PerAccountRoot], e.g. for handling a notification. + // outside any [PerAccountStoreWidget], e.g. for handling a notification. } /// Store for the user's data for a given Zulip account. diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 17336cffbe..52900e6ca6 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -21,7 +21,7 @@ class ZulipApp extends StatelessWidget { // https://m3.material.io/theme-builder#/custom colorScheme: ColorScheme.fromSeed(seedColor: kZulipBrandColor)); return DataRoot( - child: PerAccountRoot( + child: PerAccountStoreWidget( // Just one account for now. accountId: GlobalStore.fixtureAccountId, child: MaterialApp( diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index f5a4894963..1a0c420901 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -53,18 +53,25 @@ class GlobalStoreWidget extends InheritedNotifier { store != oldWidget.store; } -class PerAccountRoot extends StatefulWidget { - const PerAccountRoot( +class PerAccountStoreWidget extends StatefulWidget { + const PerAccountStoreWidget( {super.key, required this.accountId, required this.child}); final int accountId; final Widget child; + static PerAccountStore of(BuildContext context) { + final widget = context + .dependOnInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>(); + assert(widget != null, 'No PerAccountStoreWidget ancestor'); + return widget!.store; + } + @override - State createState() => _PerAccountRootState(); + State createState() => _PerAccountStoreWidgetState(); } -class _PerAccountRootState extends State { +class _PerAccountStoreWidgetState extends State { PerAccountStore? store; @override @@ -104,26 +111,22 @@ class _PerAccountRootState extends State { Widget build(BuildContext context) { // TODO: factor out the use of LoadingPage to be configured by the widget, like [widget.child] is if (store == null) return const LoadingPage(); - return PerAccountStoreWidget(store: store!, child: widget.child); + return _PerAccountStoreInheritedWidget(store: store!, child: widget.child); } } -class PerAccountStoreWidget extends InheritedNotifier { - const PerAccountStoreWidget( - {super.key, required PerAccountStore store, required super.child}) +// This is separate from [PerAccountStoreWidget] only because we need a +// [StatefulWidget] to get hold of the store, and an [InheritedWidget] to +// provide it to descendants, and one widget can't be both of those. +class _PerAccountStoreInheritedWidget extends InheritedNotifier { + const _PerAccountStoreInheritedWidget( + {required PerAccountStore store, required super.child}) : super(notifier: store); PerAccountStore get store => notifier!; - static PerAccountStore of(BuildContext context) { - final widget = context - .dependOnInheritedWidgetOfExactType(); - assert(widget != null, 'No PerAccountStoreWidget ancestor'); - return widget!.store; - } - @override - bool updateShouldNotify(covariant PerAccountStoreWidget oldWidget) => + bool updateShouldNotify(covariant _PerAccountStoreInheritedWidget oldWidget) => store != oldWidget.store; } From a8d202fd3c2ff63922864a9e384a729baa52109c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 6 Mar 2023 11:54:33 -0800 Subject: [PATCH 3/4] store [nfc]: Rename things to expose just one GlobalStoreWidget type Just like we did for per-account data in the previous commit. --- lib/widgets/app.dart | 2 +- lib/widgets/store.dart | 35 +++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 52900e6ca6..d698e1fb67 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -20,7 +20,7 @@ class ZulipApp extends StatelessWidget { // Or try this tool to see the whole palette: // https://m3.material.io/theme-builder#/custom colorScheme: ColorScheme.fromSeed(seedColor: kZulipBrandColor)); - return DataRoot( + return GlobalStoreWidget( child: PerAccountStoreWidget( // Just one account for now. accountId: GlobalStore.fixtureAccountId, diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index 1a0c420901..6cd435f0eb 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -2,16 +2,23 @@ import 'package:flutter/material.dart'; import '../model/store.dart'; -class DataRoot extends StatefulWidget { - const DataRoot({super.key, required this.child}); +class GlobalStoreWidget extends StatefulWidget { + const GlobalStoreWidget({super.key, required this.child}); final Widget child; + static GlobalStore of(BuildContext context) { + final widget = context + .dependOnInheritedWidgetOfExactType<_GlobalStoreInheritedWidget>(); + assert(widget != null, 'No GlobalStoreWidget ancestor'); + return widget!.store; + } + @override - State createState() => _DataRootState(); + State createState() => _GlobalStoreWidgetState(); } -class _DataRootState extends State { +class _GlobalStoreWidgetState extends State { GlobalStore? store; @override @@ -30,26 +37,22 @@ class _DataRootState extends State { final store = this.store; // TODO: factor out the use of LoadingPage to be configured by the widget, like [widget.child] is if (store == null) return const LoadingPage(); - return GlobalStoreWidget(store: store, child: widget.child); + return _GlobalStoreInheritedWidget(store: store, child: widget.child); } } -class GlobalStoreWidget extends InheritedNotifier { - const GlobalStoreWidget( - {super.key, required GlobalStore store, required super.child}) +// This is separate from [GlobalStoreWidget] only because we need +// a [StatefulWidget] to get hold of the store, and an [InheritedWidget] to +// provide it to descendants, and one widget can't be both of those. +class _GlobalStoreInheritedWidget extends InheritedNotifier { + const _GlobalStoreInheritedWidget( + {required GlobalStore store, required super.child}) : super(notifier: store); GlobalStore get store => notifier!; - static GlobalStore of(BuildContext context) { - final widget = - context.dependOnInheritedWidgetOfExactType(); - assert(widget != null, 'No GlobalStoreWidget ancestor'); - return widget!.store; - } - @override - bool updateShouldNotify(covariant GlobalStoreWidget oldWidget) => + bool updateShouldNotify(covariant _GlobalStoreInheritedWidget oldWidget) => store != oldWidget.store; } From 8a5fb4d6b86d9f23fdf93f6168e83df192eca77b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 13 Mar 2023 17:28:47 -0700 Subject: [PATCH 4/4] store [nfc]: Document store widgets and their `of` static methods --- lib/widgets/store.dart | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index 6cd435f0eb..7b557f71f2 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -2,11 +2,41 @@ import 'package:flutter/material.dart'; import '../model/store.dart'; +/// Provides access to the app's data. +/// +/// There should be one of this widget, near the root of the tree. +/// +/// See also: +/// * [GlobalStoreWidget.of], to get access to the data. +/// * [PerAccountStoreWidget], for the user's data associated with a +/// particular Zulip account. class GlobalStoreWidget extends StatefulWidget { const GlobalStoreWidget({super.key, required this.child}); final Widget child; + /// The app's global data store. + /// + /// The given build context will be registered as a dependency of the + /// store. This means that when the data in the store changes, + /// the element at that build context will be rebuilt. + /// + /// This method is typically called near the top of a build method or a + /// [State.didChangeDependencies] method, like so: + /// ``` + /// @override + /// Widget build(BuildContext context) { + /// final globalStore = GlobalStoreWidget.of(context); + /// ``` + /// + /// This method should not be called from a [State.initState] method; + /// use [State.didChangeDependencies] instead. For discussion, see + /// [BuildContext.dependOnInheritedWidgetOfExactType]. + /// + /// See also: + /// * [InheritedNotifier], which provides the "dependency" mechanism. + /// * [PerAccountStoreWidget.of], for the user's data associated with a + /// particular Zulip account. static GlobalStore of(BuildContext context) { final widget = context .dependOnInheritedWidgetOfExactType<_GlobalStoreInheritedWidget>(); @@ -56,6 +86,22 @@ class _GlobalStoreInheritedWidget extends InheritedNotifier { store != oldWidget.store; } +/// Provides access to the user's data for a particular Zulip account. +/// +/// Widgets that need information that comes from the Zulip server, or need to +/// interact with the Zulip server, should use [PerAccountStoreWidget.of] to get +/// the [PerAccountStore] for the relevant account. +/// +/// A page that is all about a single Zulip account (which includes most of +/// the pages in the app) should have one of this widget, near the root of +/// the page's tree. Where the UI shows information from several accounts, +/// this widget can be used to specify the account that each subtree should +/// interact with. +/// +/// See also: +/// * [PerAccountStoreWidget.of], to get access to the data. +/// * [GlobalStoreWidget], for the app's data beyond that of a +/// particular account. class PerAccountStoreWidget extends StatefulWidget { const PerAccountStoreWidget( {super.key, required this.accountId, required this.child}); @@ -63,6 +109,32 @@ class PerAccountStoreWidget extends StatefulWidget { final int accountId; final Widget child; + /// The user's data for the relevant Zulip account for this widget. + /// + /// The data is taken from the closest [PerAccountStoreWidget] that encloses + /// the given context. Throws an error if there is no enclosing + /// [PerAccountStoreWidget]. + /// + /// The given build context will be registered as a dependency of the + /// returned store. This means that when the data in the store changes, + /// the element at that build context will be rebuilt. + /// + /// This method is typically called near the top of a build method or a + /// [State.didChangeDependencies] method, like so: + /// ``` + /// @override + /// Widget build(BuildContext context) { + /// final store = PerAccountStoreWidget.of(context); + /// ``` + /// + /// This method should not be called from a [State.initState] method; + /// use [State.didChangeDependencies] instead. For discussion, see + /// [BuildContext.dependOnInheritedWidgetOfExactType]. + /// + /// See also: + /// * [InheritedNotifier], which provides the "dependency" mechanism. + /// * [GlobalStoreWidget.of], for the app's data beyond that of a + /// particular account. static PerAccountStore of(BuildContext context) { final widget = context .dependOnInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>();