From 9d5bbdb19d3beaecf2378dc55aba297be712d8be Mon Sep 17 00:00:00 2001 From: K9i-0 Date: Sun, 2 Jun 2024 21:41:10 +0900 Subject: [PATCH 1/3] add: enableRefreshIndicator and enableErrorSnackBar to PagingHelperViewTheme --- .vscode/launch.json | 17 +-- example/lib/main2.dart | 2 +- example/lib/main2.g.dart | 2 +- example/lib/main3.dart | 113 ++++++++++++++++++ example/lib/main3.g.dart | 30 +++++ example/lib/ui/first_page_error_screen.g.dart | 2 +- .../lib/ui/second_page_error_screen.g.dart | 2 +- example/pubspec.lock | 26 +++- example/pubspec.yaml | 1 + lib/src/paging_helper_view.dart | 82 ++++++++----- lib/src/paging_helper_view_theme.dart | 11 ++ 11 files changed, 237 insertions(+), 51 deletions(-) create mode 100644 example/lib/main3.dart create mode 100644 example/lib/main3.g.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index c7d977e..28f4f4a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -42,27 +42,18 @@ "flutterMode": "release" }, { - "name": "ui custom example", + "name": "basic ui custom example", "cwd": "example", "program": "lib/main2.dart", "request": "launch", "type": "dart" }, { - "name": "ui custom example (profile mode)", + "name": "advanced ui custom example", "cwd": "example", - "program": "lib/main2.dart", + "program": "lib/main3.dart", "request": "launch", - "type": "dart", - "flutterMode": "profile" + "type": "dart" }, - { - "name": "ui custom example (release mode)", - "cwd": "example", - "program": "lib/main2.dart", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } ] } \ No newline at end of file diff --git a/example/lib/main2.dart b/example/lib/main2.dart index 7954459..78e6142 100644 --- a/example/lib/main2.dart +++ b/example/lib/main2.dart @@ -87,7 +87,7 @@ class SampleScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Sample Screen'), + title: const Text('Basic UI Customization'), ), body: PagingHelperView( provider: sampleNotifierProvider, diff --git a/example/lib/main2.g.dart b/example/lib/main2.g.dart index f782e3b..5ab8721 100644 --- a/example/lib/main2.g.dart +++ b/example/lib/main2.g.dart @@ -6,7 +6,7 @@ part of 'main2.dart'; // RiverpodGenerator // ************************************************************************** -String _$sampleNotifierHash() => r'9b79e30e0e85e61102691f95a9d23525de90cb98'; +String _$sampleNotifierHash() => r'baca230cf54023ea30431c4ab714753e6b53d155'; /// A Riverpod provider that mixes in [CursorPagingNotifierMixin]. /// This provider handles the pagination logic for fetching [SampleItem] data using cursor-based pagination. diff --git a/example/lib/main3.dart b/example/lib/main3.dart new file mode 100644 index 0000000..d3cc5b9 --- /dev/null +++ b/example/lib/main3.dart @@ -0,0 +1,113 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:example/data/sample_item.dart'; +import 'package:example/repository/sample_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; +import 'package:riverpod_paging_utils/theme_extension.dart'; + +part 'main3.g.dart'; + +void main() { + runApp( + const ProviderScope( + child: MainApp(), + ), + ); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + extensions: [ + PagingHelperViewTheme( + // disable error snackbar + enableErrorSnackBar: false, + // disable pull-to-refresh + enableRefreshIndicator: false, + ), + ], + ), + home: const SampleScreen(), + ); + } +} + +/// A Riverpod provider that mixes in [CursorPagingNotifierMixin]. +/// This provider handles the pagination logic for fetching [SampleItem] data using cursor-based pagination. +@riverpod +class SampleNotifier extends _$SampleNotifier + with CursorPagingNotifierMixin { + /// Builds the initial state of the provider by fetching data with a null cursor. + @override + Future> build() => fetch(cursor: null); + + /// Fetches paginated data from the [SampleRepository] based on the provided [cursor]. + /// Returns a [CursorPagingData] object containing the fetched items, a flag indicating whether more data is available, + /// and the next cursor for fetching the next page. + @override + Future> fetch({ + required String? cursor, + }) async { + // Simulate a delay of 2 seconds to demonstrate the loading view. + await Future.delayed(const Duration(seconds: 2)); + final repository = ref.read(sampleRepositoryProvider); + final (items, nextCursor) = await repository.getByCursor(cursor); + final hasMore = nextCursor != null && nextCursor.isNotEmpty; + + return CursorPagingData( + items: items, + hasMore: hasMore, + nextCursor: nextCursor, + ); + } +} + +/// A sample page that demonstrates the usage of [PagingHelperView] with the [SampleNotifier] provider. +class SampleScreen extends ConsumerWidget { + const SampleScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + title: const Text('Advanced UI Customization'), + ), + body: PagingHelperView( + provider: sampleNotifierProvider, + futureRefreshable: sampleNotifierProvider.future, + notifierRefreshable: sampleNotifierProvider.notifier, + contentBuilder: (data, endItemView) { + // Use EasyRefresh alternative to RefreshIndicator + return EasyRefresh( + onRefresh: () { + ref.invalidate(sampleNotifierProvider); + return ref.read(sampleNotifierProvider.future); + }, + child: ListView.builder( + itemCount: data.items.length + (endItemView != null ? 1 : 0), + itemBuilder: (context, index) { + // If the end item view is provided and the index is the last item, + // return the end item view. + if (endItemView != null && index == data.items.length) { + return endItemView; + } + + // Otherwise, build a list tile for each sample item. + return ListTile( + title: Text(data.items[index].name), + subtitle: Text(data.items[index].id), + ); + }, + ), + ); + }, + ), + ); + } +} diff --git a/example/lib/main3.g.dart b/example/lib/main3.g.dart new file mode 100644 index 0000000..2b05cab --- /dev/null +++ b/example/lib/main3.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main3.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$sampleNotifierHash() => r'baca230cf54023ea30431c4ab714753e6b53d155'; + +/// A Riverpod provider that mixes in [CursorPagingNotifierMixin]. +/// This provider handles the pagination logic for fetching [SampleItem] data using cursor-based pagination. +/// +/// Copied from [SampleNotifier]. +@ProviderFor(SampleNotifier) +final sampleNotifierProvider = AutoDisposeAsyncNotifierProvider>.internal( + SampleNotifier.new, + name: r'sampleNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$sampleNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SampleNotifier + = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/example/lib/ui/first_page_error_screen.g.dart b/example/lib/ui/first_page_error_screen.g.dart index 391a3df..6229814 100644 --- a/example/lib/ui/first_page_error_screen.g.dart +++ b/example/lib/ui/first_page_error_screen.g.dart @@ -7,7 +7,7 @@ part of 'first_page_error_screen.dart'; // ************************************************************************** String _$firstPageErrorNotifierHash() => - r'8ffc25c6d2f137ddfdaad383499e15a12ab9f795'; + r'fc07f03e251eb290c695881aeb33861af41a90bd'; /// See also [FirstPageErrorNotifier]. @ProviderFor(FirstPageErrorNotifier) diff --git a/example/lib/ui/second_page_error_screen.g.dart b/example/lib/ui/second_page_error_screen.g.dart index a368634..1e6746a 100644 --- a/example/lib/ui/second_page_error_screen.g.dart +++ b/example/lib/ui/second_page_error_screen.g.dart @@ -7,7 +7,7 @@ part of 'second_page_error_screen.dart'; // ************************************************************************** String _$secondPageErrorNotifierHash() => - r'2d1b42f79de72f136eb14230dd113ab2d8aab885'; + r'c63e485985d4e2319bc6de3af432f55188cf61d8'; /// See also [SecondPageErrorNotifier]. @ProviderFor(SecondPageErrorNotifier) diff --git a/example/pubspec.lock b/example/pubspec.lock index 3cbc19b..0b487db 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + easy_refresh: + dependency: "direct main" + description: + name: easy_refresh + sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" + url: "https://pub.dev" + source: hosted + version: "3.4.0" fake_async: dependency: transitive description: @@ -451,6 +459,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" pool: dependency: transitive description: @@ -737,4 +761,4 @@ packages: version: "2.1.0" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.1.0-0" + flutter: ">=3.7.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cc0e2b5..c2501d9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: ">=3.0.0" dependencies: + easy_refresh: ^3.4.0 flutter: sdk: flutter flutter_riverpod: ^2.5.1 diff --git a/lib/src/paging_helper_view.dart b/lib/src/paging_helper_view.dart index 8e07639..d0231f5 100644 --- a/lib/src/paging_helper_view.dart +++ b/lib/src/paging_helper_view.dart @@ -39,20 +39,24 @@ class PagingHelperView, I> extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Display errors using SnackBar - ref.listen(provider, (_, state) { - if (!state.isLoading && state.hasError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - state.error!.toString(), + final theme = Theme.of(context).extension(); + final enableErrorSnackBar = theme?.enableErrorSnackBar ?? true; + + if (enableErrorSnackBar) { + // Display errors using SnackBar + ref.listen(provider, (_, state) { + if (!state.isLoading && state.hasError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.error!.toString(), + ), ), - ), - ); - } - }); + ); + } + }); + } - final theme = Theme.of(context).extension(); final loadingBuilder = theme?.loadingViewBuilder ?? (context) => const Center( child: CircularProgressIndicator(), @@ -78,28 +82,40 @@ class PagingHelperView, I> extends ConsumerWidget { required isLoading, required error, }) { - return RefreshIndicator( - onRefresh: () => ref.refresh(futureRefreshable), - child: contentBuilder( - data, - switch ((data.hasMore, hasError, isLoading)) { - // Display a widget to detect when the last element is reached - // if there are more pages and no errors - (true, false, _) => _EndVisibilityDetectorLoadingItemView( - onScrollEnd: () => - ref.read(notifierRefreshable).loadNext(), - ), - (true, true, false) when showSecondPageError => - _EndErrorItemView( - error: error, - onRetryButtonPressed: () => - ref.read(notifierRefreshable).loadNext(), - ), - (true, true, true) => const _EndLoadingItemView(), - _ => null, - }, - ), + final content = contentBuilder( + data, + switch ((data.hasMore, hasError, isLoading)) { + // Display a widget to detect when the last element is reached + // if there are more pages and no errors + (true, false, _) => _EndVisibilityDetectorLoadingItemView( + onScrollEnd: () => ref.read(notifierRefreshable).loadNext(), + ), + (true, true, false) when showSecondPageError => + _EndErrorItemView( + error: error, + onRetryButtonPressed: () => + ref.read(notifierRefreshable).loadNext(), + ), + (true, true, true) => const _EndLoadingItemView(), + _ => null, + }, ); + + final enableRefreshIndicator = + theme?.enableRefreshIndicator ?? true; + + if (enableRefreshIndicator) { + return RefreshIndicator( + onRefresh: () { + // ignore: unused_result + ref.refresh(futureRefreshable); + return ref.read(futureRefreshable); + }, + child: content, + ); + } else { + return content; + } }, // Loading state for the first page loading: () => loadingBuilder(context), diff --git a/lib/src/paging_helper_view_theme.dart b/lib/src/paging_helper_view_theme.dart index 29accc2..e0066f3 100644 --- a/lib/src/paging_helper_view_theme.dart +++ b/lib/src/paging_helper_view_theme.dart @@ -21,17 +21,23 @@ typedef EndErrorWidgetBuilder = Widget Function( /// [errorViewBuilder] is used to build the error view. /// [endLoadingViewBuilder] is used to build the ui of endItemView. /// [endErrorViewBuilder] is used to build the ui of endItemView when an error occurs. +/// [enableRefreshIndicator] is used to enable or disable the pull-to-refresh functionality. +/// [enableErrorSnackBar] is used to enable or disable the error message using SnackBar. class PagingHelperViewTheme extends ThemeExtension { PagingHelperViewTheme({ this.loadingViewBuilder, this.errorViewBuilder, this.endLoadingViewBuilder, this.endErrorViewBuilder, + this.enableRefreshIndicator, + this.enableErrorSnackBar, }); final WidgetBuilder? loadingViewBuilder; final ErrorWidgetBuilder? errorViewBuilder; final WidgetBuilder? endLoadingViewBuilder; final EndErrorWidgetBuilder? endErrorViewBuilder; + final bool? enableRefreshIndicator; + final bool? enableErrorSnackBar; @override ThemeExtension copyWith({ @@ -39,6 +45,8 @@ class PagingHelperViewTheme extends ThemeExtension { ErrorWidgetBuilder? errorViewBuilder, WidgetBuilder? endLoadingViewBuilder, EndErrorWidgetBuilder? endErrorViewBuilder, + bool? enableRefreshIndicator, + bool? enableErrorSnackBar, }) { return PagingHelperViewTheme( loadingViewBuilder: loadingViewBuilder ?? loadingViewBuilder, @@ -46,6 +54,9 @@ class PagingHelperViewTheme extends ThemeExtension { endLoadingViewBuilder: endLoadingViewBuilder ?? this.endLoadingViewBuilder, endErrorViewBuilder: endErrorViewBuilder ?? this.endErrorViewBuilder, + enableRefreshIndicator: + enableRefreshIndicator ?? this.enableRefreshIndicator, + enableErrorSnackBar: enableErrorSnackBar ?? this.enableErrorSnackBar, ); } From e042f4030c56cbc956d144104183131b689dd553 Mon Sep 17 00:00:00 2001 From: K9i-0 Date: Sun, 2 Jun 2024 21:48:51 +0900 Subject: [PATCH 2/3] add: update readme --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/README.md b/README.md index 88efa38..0ed2d16 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ class SampleScreen extends StatelessWidget { ## UI Customization +### Basic Customization + You can easily customize the appearance of loading and error states using `ThemeExtension`. riverpod_paging_utils_sample @@ -139,3 +141,83 @@ class MainApp extends StatelessWidget { ``` A complete sample implementation can be found in the [example/lib/main2.dart](https://github.com/K9i-0/riverpod_paging_utils/blob/main/example/lib/main2.dart) file. + +### Advanced Customization + +Customizing the appearance of error SnackBars or RefreshIndicators requires a bit more setup. + +#### 1. Theme Configuration + +First, adjust your `PagingHelperViewTheme` like this: + +```dart +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + extensions: [ + PagingHelperViewTheme( + // disable error snackbar + enableErrorSnackBar: false, + // disable pull-to-refresh + enableRefreshIndicator: false, + ), + ], + ), + home: const SampleScreen(), + ); + } +} +``` + +#### 2. Integrating Custom Refresh (e.g., with easy_refresh) + +If you're using a package like [easy_refresh](https://pub.dev/packages/easy_refresh) to provide a RefreshIndicator, modify your screen code as follows: + +```dart +class SampleScreen extends ConsumerWidget { + const SampleScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + title: const Text('Advanced UI Customization'), + ), + body: PagingHelperView( + provider: sampleNotifierProvider, + futureRefreshable: sampleNotifierProvider.future, + notifierRefreshable: sampleNotifierProvider.notifier, + contentBuilder: (data, endItemView) { + // Use EasyRefresh alternative to RefreshIndicator + return EasyRefresh( + onRefresh: () { + ref.invalidate(sampleNotifierProvider); + return ref.read(sampleNotifierProvider.future); + }, + child: ListView.builder( + itemCount: data.items.length + (endItemView != null ? 1 : 0), + itemBuilder: (context, index) { + // If the end item view is provided and the index is the last item, + // return the end item view. + if (endItemView != null && index == data.items.length) { + return endItemView; + } + + // Otherwise, build a list tile for each sample item. + return ListTile( + title: Text(data.items[index].name), + subtitle: Text(data.items[index].id), + ); + }, + ), + ); + }, + ), + ); + } +} +``` From e303eac0fcbc6a82e8951bc23d28c89586b4c939 Mon Sep 17 00:00:00 2001 From: K9i-0 Date: Sun, 2 Jun 2024 21:49:37 +0900 Subject: [PATCH 3/3] add: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0ed2d16..8fc91c1 100644 --- a/README.md +++ b/README.md @@ -221,3 +221,5 @@ class SampleScreen extends ConsumerWidget { } } ``` + +A complete sample implementation can be found in the [example/lib/main3.dart](https://github.com/K9i-0/riverpod_paging_utils/blob/main/example/lib/main3.dart) file.