diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index ce8f91fa..115bc381 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -262,7 +262,7 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra } FlutterNativeSplash.remove(); - return ResultBuilder( + return ResultBuilder.behaviorSubject( stream: activeAccountSnapshot.hasData ? _accountsBloc.getCapabilitiesBlocFor(activeAccountSnapshot.data!).capabilities : null, diff --git a/packages/neon/neon/lib/src/pages/account_settings.dart b/packages/neon/neon/lib/src/pages/account_settings.dart index 59274770..b35deda0 100644 --- a/packages/neon/neon/lib/src/pages/account_settings.dart +++ b/packages/neon/neon/lib/src/pages/account_settings.dart @@ -60,7 +60,7 @@ class AccountSettingsPage extends StatelessWidget { ), ], ), - body: ResultBuilder( + body: ResultBuilder.behaviorSubject( stream: _userDetailsBloc.userDetails, builder: (final context, final userDetails) => SettingsList( categories: [ @@ -92,7 +92,7 @@ class AccountSettingsPage extends StatelessWidget { onRetry: _userDetailsBloc.refresh, ), NeonLinearProgressIndicator( - visible: userDetails.loading, + visible: userDetails.isLoading, ), ], ), diff --git a/packages/neon/neon/lib/src/pages/home.dart b/packages/neon/neon/lib/src/pages/home.dart index 9eef37df..eb4daba0 100644 --- a/packages/neon/neon/lib/src/pages/home.dart +++ b/packages/neon/neon/lib/src/pages/home.dart @@ -45,12 +45,12 @@ class _HomePageState extends State { _capabilitiesBloc.capabilities.listen((final result) async { if (result.data != null) { // ignore cached version and prevent duplicate dialogs - if (result.cached) { + if (result.isCached) { return; } _appsBloc.appImplementations.listen((final appsResult) async { // ignore cached version and prevent duplicate dialogs - if (appsResult.data == null || appsResult.cached) { + if (appsResult.data == null || appsResult.isCached) { return; } for (final id in [ @@ -168,11 +168,12 @@ class _HomePageState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder( + Widget build(final BuildContext context) => ResultBuilder.behaviorSubject( stream: _capabilitiesBloc.capabilities, - builder: (final context, final capabilities) => ResultBuilder>( + builder: (final context, final capabilities) => ResultBuilder>.behaviorSubject( stream: _appsBloc.appImplementations, - builder: (final context, final appImplementations) => ResultBuilder( + builder: (final context, final appImplementations) => + ResultBuilder.behaviorSubject( stream: _appsBloc.notificationsAppImplementation, builder: (final context, final notificationsAppImplementation) => StreamBuilder( stream: _appsBloc.activeAppID, @@ -204,7 +205,7 @@ class _HomePageState extends State { ), ), ], - if (appImplementations.error != null) ...[ + if (appImplementations.hasError) ...[ const SizedBox( width: 8, ), @@ -214,7 +215,7 @@ class _HomePageState extends State { onlyIcon: true, ), ], - if (appImplementations.loading) ...[ + if (appImplementations.isLoading) ...[ const SizedBox( width: 8, ), diff --git a/packages/neon/neon/lib/src/utils/request_manager.dart b/packages/neon/neon/lib/src/utils/request_manager.dart index 343bb70d..55f4c9e6 100644 --- a/packages/neon/neon/lib/src/utils/request_manager.dart +++ b/packages/neon/neon/lib/src/utils/request_manager.dart @@ -68,8 +68,8 @@ class RequestManager { Result( subject.value.data, null, - loading: true, - cached: true, + isLoading: true, + isCached: true, ), ); } else { @@ -148,8 +148,8 @@ class RequestManager { Result( cached, error, - loading: loading, - cached: true, + isLoading: loading, + isCached: true, ), ); return true; diff --git a/packages/neon/neon/lib/src/utils/result.dart b/packages/neon/neon/lib/src/utils/result.dart index 3a11a9ee..60dbd146 100644 --- a/packages/neon/neon/lib/src/utils/result.dart +++ b/packages/neon/neon/lib/src/utils/result.dart @@ -5,40 +5,56 @@ class Result { const Result( this.data, this.error, { - required this.loading, - required this.cached, + required this.isLoading, + required this.isCached, }); factory Result.loading() => const Result( null, null, - loading: true, - cached: false, + isLoading: true, + isCached: false, ); factory Result.success(final T data) => Result( data, null, - loading: false, - cached: false, + isLoading: false, + isCached: false, ); factory Result.error(final Object error) => Result( null, error, - loading: false, - cached: false, + isLoading: false, + isCached: false, ); final T? data; final Object? error; - final bool loading; - final bool cached; + final bool isLoading; + final bool isCached; Result transform(final R? Function(T data) call) => Result( data != null ? call(data as T) : null, error, - loading: loading, - cached: cached, + isLoading: isLoading, + isCached: isCached, ); + + Result asLoading() => Result( + data, + error, + isLoading: true, + isCached: isCached, + ); + + bool get hasError => error != null; + + @override + bool operator ==(final Object other) => + other is Result && other.isLoading == isLoading && other.data == data && other.error == error; + + @override + int get hashCode => Object.hash(data, error, isLoading, isCached); } diff --git a/packages/neon/neon/lib/src/widgets/account_tile.dart b/packages/neon/neon/lib/src/widgets/account_tile.dart index 5afa02c8..d6afef74 100644 --- a/packages/neon/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/neon/lib/src/widgets/account_tile.dart @@ -36,7 +36,7 @@ class NeonAccountTile extends StatelessWidget { leading: NeonUserAvatar( account: account, ), - title: ResultBuilder( + title: ResultBuilder.behaviorSubject( stream: userDetailsBloc.userDetails, builder: (final context, final userDetails) => Row( children: [ @@ -51,7 +51,7 @@ class NeonAccountTile extends StatelessWidget { ), ), ], - if (userDetails.loading) ...[ + if (userDetails.isLoading) ...[ const SizedBox( width: 5, ), @@ -61,7 +61,7 @@ class NeonAccountTile extends StatelessWidget { ), ), ], - if (userDetails.error != null) ...[ + if (userDetails.hasError) ...[ const SizedBox( width: 5, ), diff --git a/packages/neon/neon/lib/src/widgets/drawer.dart b/packages/neon/neon/lib/src/widgets/drawer.dart index 8cb8cec0..a5e020ee 100644 --- a/packages/neon/neon/lib/src/widgets/drawer.dart +++ b/packages/neon/neon/lib/src/widgets/drawer.dart @@ -166,7 +166,7 @@ class NeonDrawerHeader extends StatelessWidget { }, ); - return ResultBuilder( + return ResultBuilder.behaviorSubject( stream: capabilitiesBloc.capabilities, builder: (final context, final capabilities) => DrawerHeader( decoration: BoxDecoration( @@ -198,7 +198,7 @@ class NeonDrawerHeader extends StatelessWidget { onRetry: capabilitiesBloc.refresh, ), NeonLinearProgressIndicator( - visible: capabilities.loading, + visible: capabilities.isLoading, ), ], accountSelecor, diff --git a/packages/neon/neon/lib/src/widgets/result_builder.dart b/packages/neon/neon/lib/src/widgets/result_builder.dart index d527287f..ae6954a3 100644 --- a/packages/neon/neon/lib/src/widgets/result_builder.dart +++ b/packages/neon/neon/lib/src/widgets/result_builder.dart @@ -1,28 +1,51 @@ part of '../../neon.dart'; -class ResultBuilder extends StatelessWidget { +typedef ResultWidgetBuilder = Widget Function(BuildContext context, Result snapshot); + +class ResultBuilder extends StreamBuilderBase, Result> { const ResultBuilder({ - required this.stream, required this.builder, + this.initialData, + super.stream, super.key, }); - final Stream?>? stream; + ResultBuilder.behaviorSubject({ + required this.builder, + BehaviorSubject>? super.stream, + super.key, + }) : initialData = stream?.valueOrNull; + + final ResultWidgetBuilder builder; + final Result? initialData; + + @override + Result initial() => initialData?.asLoading() ?? Result.loading(); + + @override + Result afterData(final Result current, final Result data) { + // prevent rebuild when only the cache state cahnges + if (current == data) { + return current; + } + + return data; + } + + @override + Result afterError(final Result current, final Object error, final StackTrace stackTrace) { + if (current.hasError) { + return current; + } - final Widget Function(BuildContext, Result) builder; + return Result( + current.data, + error, + isLoading: false, + isCached: false, + ); + } @override - Widget build(final BuildContext context) => StreamBuilder( - stream: stream, - builder: (final context, final snapshot) { - if (snapshot.hasError) { - return builder(context, Result.error(snapshot.error!)); - } - if (snapshot.hasData) { - return builder(context, snapshot.data!); - } - - return builder(context, Result.loading()); - }, - ); + Widget build(final BuildContext context, final Result currentSummary) => builder(context, currentSummary); } diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index 027d04af..e8d3a205 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -74,7 +74,7 @@ class _UserAvatarState extends State { ), if (widget.showStatus) ...[ ResultBuilder( - stream: _userStatusBloc.statuses.map((final statuses) => statuses[widget.username]), + stream: _userStatusBloc.statuses.mapNotNull((final statuses) => statuses[widget.username]), builder: _userStatusIconBuilder, ), ], @@ -89,12 +89,12 @@ class _UserAvatarState extends State { Widget? child; Decoration? decoration; - if (result.loading) { + if (result.isLoading) { child = CircularProgressIndicator( strokeWidth: 1.5, color: widget.foregroundColor ?? Theme.of(context).colorScheme.onPrimary, ); - } else if (result.error != null) { + } else if (result.hasError) { child = Icon( Icons.error_outline, size: scaledSize, diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 28b972dc..49fbc2df 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -32,7 +32,7 @@ class _FilesBrowserViewState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: widget.bloc.files, builder: (final context, final files) => StreamBuilder>( stream: widget.bloc.path, @@ -136,7 +136,7 @@ class _FilesBrowserViewState extends State { ], ], ], - isLoading: files.loading, + isLoading: files.isLoading, error: files.error, onRefresh: widget.bloc.refresh, builder: (final context, final widget) => widget, diff --git a/packages/neon/neon_news/lib/dialogs/add_feed.dart b/packages/neon/neon_news/lib/dialogs/add_feed.dart index 294b2188..63c0cda7 100644 --- a/packages/neon/neon_news/lib/dialogs/add_feed.dart +++ b/packages/neon/neon_news/lib/dialogs/add_feed.dart @@ -43,7 +43,7 @@ class _NewsAddFeedDialogState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: widget.bloc.folders, builder: (final context, final folders) => NeonDialog( title: Text(AppLocalizations.of(context).feedAdd), @@ -74,7 +74,7 @@ class _NewsAddFeedDialogState extends State { ), Center( child: NeonLinearProgressIndicator( - visible: folders.loading, + visible: folders.isLoading, ), ), if (folders.data != null) ...[ diff --git a/packages/neon/neon_news/lib/widgets/articles_view.dart b/packages/neon/neon_news/lib/widgets/articles_view.dart index 2518caea..49047bfb 100644 --- a/packages/neon/neon_news/lib/widgets/articles_view.dart +++ b/packages/neon/neon_news/lib/widgets/articles_view.dart @@ -25,9 +25,9 @@ class _NewsArticlesViewState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: widget.newsBloc.feeds, - builder: (final context, final feeds) => ResultBuilder>( + builder: (final context, final feeds) => ResultBuilder>.behaviorSubject( stream: widget.bloc.articles, builder: (final context, final articles) => SortBoxBuilder( sortBox: articlesSortBox, @@ -37,7 +37,7 @@ class _NewsArticlesViewState extends State { builder: (final context, final sorted) => NeonListView( scrollKey: 'news-articles', items: feeds.data == null ? null : sorted, - isLoading: articles.loading || feeds.loading, + isLoading: articles.isLoading || feeds.isLoading, error: articles.error ?? feeds.error, onRefresh: () async { await Future.wait([ diff --git a/packages/neon/neon_news/lib/widgets/feeds_view.dart b/packages/neon/neon_news/lib/widgets/feeds_view.dart index 7a6d942d..bff83a61 100644 --- a/packages/neon/neon_news/lib/widgets/feeds_view.dart +++ b/packages/neon/neon_news/lib/widgets/feeds_view.dart @@ -11,9 +11,9 @@ class NewsFeedsView extends StatelessWidget { final int? folderID; @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: bloc.folders, - builder: (final context, final folders) => ResultBuilder>( + builder: (final context, final folders) => ResultBuilder>.behaviorSubject( stream: bloc.feeds, builder: (final context, final feeds) => SortBoxBuilder( sortBox: feedsSortBox, @@ -26,7 +26,7 @@ class NewsFeedsView extends StatelessWidget { scrollKey: 'news-feeds', withFloatingActionButton: true, items: sorted, - isLoading: feeds.loading || folders.loading, + isLoading: feeds.isLoading || folders.isLoading, error: feeds.error ?? folders.error, onRefresh: bloc.refresh, builder: (final context, final feed) => _buildFeed( diff --git a/packages/neon/neon_news/lib/widgets/folders_view.dart b/packages/neon/neon_news/lib/widgets/folders_view.dart index 6839df97..47418cdc 100644 --- a/packages/neon/neon_news/lib/widgets/folders_view.dart +++ b/packages/neon/neon_news/lib/widgets/folders_view.dart @@ -9,9 +9,9 @@ class NewsFoldersView extends StatelessWidget { final NewsBloc bloc; @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: bloc.folders, - builder: (final context, final folders) => ResultBuilder>( + builder: (final context, final folders) => ResultBuilder>.behaviorSubject( stream: bloc.feeds, builder: (final context, final feeds) => SortBoxBuilder( sortBox: foldersSortBox, @@ -30,7 +30,7 @@ class NewsFoldersView extends StatelessWidget { scrollKey: 'news-folders', withFloatingActionButton: true, items: sorted, - isLoading: feeds.loading || folders.loading, + isLoading: feeds.isLoading || folders.isLoading, error: feeds.error ?? folders.error, onRefresh: bloc.refresh, builder: _buildFolder, diff --git a/packages/neon/neon_notes/lib/dialogs/create_note.dart b/packages/neon/neon_notes/lib/dialogs/create_note.dart index 36b1b9ce..715d56e0 100644 --- a/packages/neon/neon_notes/lib/dialogs/create_note.dart +++ b/packages/neon/neon_notes/lib/dialogs/create_note.dart @@ -26,7 +26,7 @@ class _NotesCreateNoteDialogState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: widget.bloc.notes, builder: (final context, final notes) => NeonDialog( title: Text(AppLocalizations.of(context).noteCreate), @@ -56,7 +56,7 @@ class _NotesCreateNoteDialogState extends State { ), Center( child: NeonLinearProgressIndicator( - visible: notes.loading, + visible: notes.isLoading, ), ), if (notes.data != null) ...[ diff --git a/packages/neon/neon_notes/lib/dialogs/select_category.dart b/packages/neon/neon_notes/lib/dialogs/select_category.dart index 57055644..85623fda 100644 --- a/packages/neon/neon_notes/lib/dialogs/select_category.dart +++ b/packages/neon/neon_notes/lib/dialogs/select_category.dart @@ -26,7 +26,7 @@ class _NotesSelectCategoryDialogState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: widget.bloc.notes, builder: (final context, final notes) => NeonDialog( title: Text(AppLocalizations.of(context).category), @@ -44,7 +44,7 @@ class _NotesSelectCategoryDialogState extends State { ), Center( child: NeonLinearProgressIndicator( - visible: notes.loading, + visible: notes.isLoading, ), ), if (notes.data != null) ...[ diff --git a/packages/neon/neon_notes/lib/widgets/categories_view.dart b/packages/neon/neon_notes/lib/widgets/categories_view.dart index bef23e35..a8dfbfc1 100644 --- a/packages/neon/neon_notes/lib/widgets/categories_view.dart +++ b/packages/neon/neon_notes/lib/widgets/categories_view.dart @@ -9,7 +9,7 @@ class NotesCategoriesView extends StatelessWidget { final NotesBloc bloc; @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: bloc.notes, builder: (final context, final notes) => SortBoxBuilder( sortBox: categoriesSortBox, @@ -28,7 +28,7 @@ class NotesCategoriesView extends StatelessWidget { builder: (final context, final sorted) => NeonListView( scrollKey: 'notes-categories', items: sorted, - isLoading: notes.loading, + isLoading: notes.isLoading, error: notes.error, onRefresh: bloc.refresh, builder: _buildCategory, diff --git a/packages/neon/neon_notes/lib/widgets/notes_view.dart b/packages/neon/neon_notes/lib/widgets/notes_view.dart index a014e28b..eaffc784 100644 --- a/packages/neon/neon_notes/lib/widgets/notes_view.dart +++ b/packages/neon/neon_notes/lib/widgets/notes_view.dart @@ -11,7 +11,7 @@ class NotesView extends StatelessWidget { final String? category; @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: bloc.notes, builder: (final context, final notes) => SortBoxBuilder( sortBox: notesSortBox, @@ -34,7 +34,7 @@ class NotesView extends StatelessWidget { ...?sortedFavorites, ...?sortedNonFavorites, ], - isLoading: notes.loading, + isLoading: notes.isLoading, error: notes.error, onRefresh: bloc.refresh, builder: _buildNote, diff --git a/packages/neon/neon_notifications/lib/pages/main.dart b/packages/neon/neon_notifications/lib/pages/main.dart index b1048fcd..bc1a0e00 100644 --- a/packages/neon/neon_notifications/lib/pages/main.dart +++ b/packages/neon/neon_notifications/lib/pages/main.dart @@ -24,7 +24,7 @@ class _NotificationsMainPageState extends State { } @override - Widget build(final BuildContext context) => ResultBuilder>( + Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( stream: bloc.notifications, builder: (final context, final notifications) => Scaffold( resizeToAvoidBottomInset: false, @@ -39,7 +39,7 @@ class _NotificationsMainPageState extends State { scrollKey: 'notifications-notifications', withFloatingActionButton: true, items: notifications.data, - isLoading: notifications.loading, + isLoading: notifications.isLoading, error: notifications.error, onRefresh: bloc.refresh, builder: _buildNotification,