From 119c165bc808cd3abd2c0ba51ac1f8a9682a1d2f Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 25 Feb 2024 04:16:36 +0100 Subject: [PATCH] Added lazy loading bookmarks per tag --- .../model/tag_bookmarks.model.dart | 18 +- .../provider/tag_bookmarks.provider.dart | 83 +++++-- .../provider/tag_bookmarks.provider.g.dart | 223 +++++------------- .../tag_bookmarks/ui/tag_bookmarks.dart | 143 ++++++----- 4 files changed, 232 insertions(+), 235 deletions(-) diff --git a/lib/screens/tag_bookmarks/model/tag_bookmarks.model.dart b/lib/screens/tag_bookmarks/model/tag_bookmarks.model.dart index 3a725bb..de4fed0 100644 --- a/lib/screens/tag_bookmarks/model/tag_bookmarks.model.dart +++ b/lib/screens/tag_bookmarks/model/tag_bookmarks.model.dart @@ -1,9 +1,25 @@ +import 'package:linkdy/constants/enums.dart'; +import 'package:linkdy/models/data/bookmarks.dart'; +import 'package:linkdy/models/data/tags.dart'; + class TagBookmarksModel { + String? tagId; + Tag? tag; int currentPage; int limit; + LoadStatus initialLoadStatus; + List bookmarks; + int maxNumber; + bool loadingMore; TagBookmarksModel({ + this.tagId, + this.tag, this.currentPage = 0, - this.limit = 100, + this.limit = 30, + this.initialLoadStatus = LoadStatus.loading, + required this.bookmarks, + this.maxNumber = 0, + this.loadingMore = false, }); } diff --git a/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.dart b/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.dart index 2bff6cd..80146ab 100644 --- a/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.dart +++ b/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.dart @@ -2,39 +2,68 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/tag_bookmarks/model/tag_bookmarks.model.dart'; +import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/models/data/tags.dart'; -import 'package:linkdy/models/data/bookmarks.dart'; -import 'package:linkdy/models/api_response.dart'; -import 'package:linkdy/models/data/tag_bookmarks.dart'; import 'package:linkdy/providers/api_client_provider.dart'; part 'tag_bookmarks.provider.g.dart'; @riverpod -FutureOr> tagIdBookmarksRequest(TagIdBookmarksRequestRef ref, String tagId) async { - final tagResult = await ref.watch(apiClientProvider)!.fetchTagById(tagId); - if (tagResult.content == null) return const ApiResponse(successful: false); - final bookmarksResult = await ref.watch(apiClientProvider)!.fetchBookmarks(q: tagResult.content!.name); - return ApiResponse( - successful: bookmarksResult.successful, - content: TagBookmarksResponse( - bookmarksResponse: bookmarksResult.content, - tag: tagResult.content, - ), - ); +FutureOr tagBookmarksRequest(TagBookmarksRequestRef ref, Tag? tag, String? tagId, int limit) async { + final tagResult = tag == null ? await ref.watch(apiClientProvider)!.fetchTagById(tagId!) : null; + if (tagResult != null && tagResult.successful == false) { + ref.read(tagBookmarksProvider.notifier).setInitialLoadStatus(LoadStatus.error); + return; + } + + final bookmarksResult = await ref.watch(apiClientProvider)!.fetchBookmarks( + q: tag != null ? tag.name : tagResult!.content!.name, + limit: limit, + offset: 0, + ); + + if (bookmarksResult.successful == true) { + ref.read(tagBookmarksProvider).bookmarks = bookmarksResult.content!.results!; + ref.read(tagBookmarksProvider).maxNumber = bookmarksResult.content!.count!; + if (tag == null) ref.read(tagBookmarksProvider).tag = tagResult!.content!; + ref.read(tagBookmarksProvider).currentPage = 0; + ref.read(tagBookmarksProvider).loadingMore = false; + ref.read(tagBookmarksProvider).initialLoadStatus = LoadStatus.loaded; + } else { + ref.read(tagBookmarksProvider).initialLoadStatus = LoadStatus.error; + } + + ref.read(tagBookmarksProvider.notifier).notifyListeners(); } @riverpod -FutureOr> tagBookmarksRequest(TagBookmarksRequestRef ref, Tag tag) async { - final bookmarksResult = await ref.watch(apiClientProvider)!.fetchBookmarks(q: tag.name); - return bookmarksResult; +FutureOr tagBookmarksRequestLoadMore(TagBookmarksRequestLoadMoreRef ref) async { + final provider = ref.watch(tagBookmarksProvider); + + final newOffset = provider.limit * (provider.currentPage + 1); + + final result = await ref.watch(apiClientProvider)!.fetchBookmarks( + q: provider.tag!.name, + limit: provider.limit, + offset: newOffset, + ); + await Future.delayed(Duration(seconds: 4)); + if (result.successful == true) { + provider.bookmarks = [...provider.bookmarks, ...result.content!.results!]; + provider.maxNumber = result.content!.count!; + provider.currentPage = provider.currentPage + 1; + } + + ref.read(tagBookmarksProvider.notifier).setLoadingMore(false); } @riverpod class TagBookmarks extends _$TagBookmarks { @override TagBookmarksModel build() { - return TagBookmarksModel(); + return TagBookmarksModel( + bookmarks: [], + ); } void setCurrentPage(int page) { @@ -44,4 +73,22 @@ class TagBookmarks extends _$TagBookmarks { void setLimit(int limit) { state.limit = limit; } + + void setInitialLoadStatus(LoadStatus status) { + state.initialLoadStatus = status; + ref.notifyListeners(); + } + + void setLoadingMore(bool status) { + state.loadingMore = status; + ref.notifyListeners(); + } + + Future refresh() async { + await ref.read(tagBookmarksRequestProvider(state.tag, state.tagId, state.limit).future); + } + + void notifyListeners() { + ref.notifyListeners(); + } } diff --git a/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.g.dart b/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.g.dart index 7f014cc..cbcc869 100644 --- a/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.g.dart +++ b/lib/screens/tag_bookmarks/provider/tag_bookmarks.provider.g.dart @@ -6,8 +6,8 @@ part of 'tag_bookmarks.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$tagIdBookmarksRequestHash() => - r'2deadbe50293503ac94cd5d8656d6055c6bdc190'; +String _$tagBookmarksRequestHash() => + r'92613aa119dadb65b7b399b9c779197673689908'; /// Copied from Dart SDK class _SystemHash { @@ -30,158 +30,25 @@ class _SystemHash { } } -/// See also [tagIdBookmarksRequest]. -@ProviderFor(tagIdBookmarksRequest) -const tagIdBookmarksRequestProvider = TagIdBookmarksRequestFamily(); - -/// See also [tagIdBookmarksRequest]. -class TagIdBookmarksRequestFamily - extends Family>> { - /// See also [tagIdBookmarksRequest]. - const TagIdBookmarksRequestFamily(); - - /// See also [tagIdBookmarksRequest]. - TagIdBookmarksRequestProvider call( - String tagId, - ) { - return TagIdBookmarksRequestProvider( - tagId, - ); - } - - @override - TagIdBookmarksRequestProvider getProviderOverride( - covariant TagIdBookmarksRequestProvider provider, - ) { - return call( - provider.tagId, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'tagIdBookmarksRequestProvider'; -} - -/// See also [tagIdBookmarksRequest]. -class TagIdBookmarksRequestProvider - extends AutoDisposeFutureProvider> { - /// See also [tagIdBookmarksRequest]. - TagIdBookmarksRequestProvider( - String tagId, - ) : this._internal( - (ref) => tagIdBookmarksRequest( - ref as TagIdBookmarksRequestRef, - tagId, - ), - from: tagIdBookmarksRequestProvider, - name: r'tagIdBookmarksRequestProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$tagIdBookmarksRequestHash, - dependencies: TagIdBookmarksRequestFamily._dependencies, - allTransitiveDependencies: - TagIdBookmarksRequestFamily._allTransitiveDependencies, - tagId: tagId, - ); - - TagIdBookmarksRequestProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.tagId, - }) : super.internal(); - - final String tagId; - - @override - Override overrideWith( - FutureOr> Function( - TagIdBookmarksRequestRef provider) - create, - ) { - return ProviderOverride( - origin: this, - override: TagIdBookmarksRequestProvider._internal( - (ref) => create(ref as TagIdBookmarksRequestRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tagId: tagId, - ), - ); - } - - @override - AutoDisposeFutureProviderElement> - createElement() { - return _TagIdBookmarksRequestProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TagIdBookmarksRequestProvider && other.tagId == tagId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, tagId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin TagIdBookmarksRequestRef - on AutoDisposeFutureProviderRef> { - /// The parameter `tagId` of this provider. - String get tagId; -} - -class _TagIdBookmarksRequestProviderElement - extends AutoDisposeFutureProviderElement> - with TagIdBookmarksRequestRef { - _TagIdBookmarksRequestProviderElement(super.provider); - - @override - String get tagId => (origin as TagIdBookmarksRequestProvider).tagId; -} - -String _$tagBookmarksRequestHash() => - r'd198a94eafd0a78ef19de4064da4ddfba26ec1d3'; - /// See also [tagBookmarksRequest]. @ProviderFor(tagBookmarksRequest) const tagBookmarksRequestProvider = TagBookmarksRequestFamily(); /// See also [tagBookmarksRequest]. -class TagBookmarksRequestFamily - extends Family>> { +class TagBookmarksRequestFamily extends Family> { /// See also [tagBookmarksRequest]. const TagBookmarksRequestFamily(); /// See also [tagBookmarksRequest]. TagBookmarksRequestProvider call( - Tag tag, + Tag? tag, + String? tagId, + int limit, ) { return TagBookmarksRequestProvider( tag, + tagId, + limit, ); } @@ -191,6 +58,8 @@ class TagBookmarksRequestFamily ) { return call( provider.tag, + provider.tagId, + provider.limit, ); } @@ -210,15 +79,18 @@ class TagBookmarksRequestFamily } /// See also [tagBookmarksRequest]. -class TagBookmarksRequestProvider - extends AutoDisposeFutureProvider> { +class TagBookmarksRequestProvider extends AutoDisposeFutureProvider { /// See also [tagBookmarksRequest]. TagBookmarksRequestProvider( - Tag tag, + Tag? tag, + String? tagId, + int limit, ) : this._internal( (ref) => tagBookmarksRequest( ref as TagBookmarksRequestRef, tag, + tagId, + limit, ), from: tagBookmarksRequestProvider, name: r'tagBookmarksRequestProvider', @@ -230,6 +102,8 @@ class TagBookmarksRequestProvider allTransitiveDependencies: TagBookmarksRequestFamily._allTransitiveDependencies, tag: tag, + tagId: tagId, + limit: limit, ); TagBookmarksRequestProvider._internal( @@ -240,15 +114,17 @@ class TagBookmarksRequestProvider required super.debugGetCreateSourceHash, required super.from, required this.tag, + required this.tagId, + required this.limit, }) : super.internal(); - final Tag tag; + final Tag? tag; + final String? tagId; + final int limit; @override Override overrideWith( - FutureOr> Function( - TagBookmarksRequestRef provider) - create, + FutureOr Function(TagBookmarksRequestRef provider) create, ) { return ProviderOverride( origin: this, @@ -260,46 +136,77 @@ class TagBookmarksRequestProvider allTransitiveDependencies: null, debugGetCreateSourceHash: null, tag: tag, + tagId: tagId, + limit: limit, ), ); } @override - AutoDisposeFutureProviderElement> - createElement() { + AutoDisposeFutureProviderElement createElement() { return _TagBookmarksRequestProviderElement(this); } @override bool operator ==(Object other) { - return other is TagBookmarksRequestProvider && other.tag == tag; + return other is TagBookmarksRequestProvider && + other.tag == tag && + other.tagId == tagId && + other.limit == limit; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, tag.hashCode); + hash = _SystemHash.combine(hash, tagId.hashCode); + hash = _SystemHash.combine(hash, limit.hashCode); return _SystemHash.finish(hash); } } -mixin TagBookmarksRequestRef - on AutoDisposeFutureProviderRef> { +mixin TagBookmarksRequestRef on AutoDisposeFutureProviderRef { /// The parameter `tag` of this provider. - Tag get tag; + Tag? get tag; + + /// The parameter `tagId` of this provider. + String? get tagId; + + /// The parameter `limit` of this provider. + int get limit; } class _TagBookmarksRequestProviderElement - extends AutoDisposeFutureProviderElement> - with TagBookmarksRequestRef { + extends AutoDisposeFutureProviderElement with TagBookmarksRequestRef { _TagBookmarksRequestProviderElement(super.provider); @override - Tag get tag => (origin as TagBookmarksRequestProvider).tag; + Tag? get tag => (origin as TagBookmarksRequestProvider).tag; + @override + String? get tagId => (origin as TagBookmarksRequestProvider).tagId; + @override + int get limit => (origin as TagBookmarksRequestProvider).limit; } -String _$tagBookmarksHash() => r'bcfcc986b8d58ccff54ed6dec3ab2a1a8c33a4f7'; +String _$tagBookmarksRequestLoadMoreHash() => + r'3235c66b13a41c52611b86c2b26ecf9376ca1524'; + +/// See also [tagBookmarksRequestLoadMore]. +@ProviderFor(tagBookmarksRequestLoadMore) +final tagBookmarksRequestLoadMoreProvider = + AutoDisposeFutureProvider.internal( + tagBookmarksRequestLoadMore, + name: r'tagBookmarksRequestLoadMoreProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$tagBookmarksRequestLoadMoreHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef TagBookmarksRequestLoadMoreRef = AutoDisposeFutureProviderRef; +String _$tagBookmarksHash() => r'74e6463830052de2f05c628455d5ccca9faef765'; /// See also [TagBookmarks]. @ProviderFor(TagBookmarks) diff --git a/lib/screens/tag_bookmarks/ui/tag_bookmarks.dart b/lib/screens/tag_bookmarks/ui/tag_bookmarks.dart index 220d966..32684b8 100644 --- a/lib/screens/tag_bookmarks/ui/tag_bookmarks.dart +++ b/lib/screens/tag_bookmarks/ui/tag_bookmarks.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_item.dart'; @@ -7,15 +6,13 @@ import 'package:linkdy/screens/tag_bookmarks/provider/tag_bookmarks.provider.dar import 'package:linkdy/widgets/error_screen.dart'; import 'package:linkdy/widgets/no_data_screen.dart'; -import 'package:linkdy/models/api_response.dart'; -import 'package:linkdy/models/data/bookmarks.dart'; -import 'package:linkdy/models/data/tag_bookmarks.dart'; import 'package:linkdy/models/data/tags.dart'; +import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/providers/router_provider.dart'; import 'package:linkdy/router/paths.dart'; import 'package:linkdy/i18n/strings.g.dart'; -class TagBookmarksScreen extends ConsumerWidget { +class TagBookmarksScreen extends ConsumerStatefulWidget { final String? tagId; final Tag? tag; @@ -26,31 +23,48 @@ class TagBookmarksScreen extends ConsumerWidget { }) : super(key: key); @override - Widget build(BuildContext context, WidgetRef ref) { - // Redirect to initial screen if no tagId or tag - SchedulerBinding.instance.addPostFrameCallback((_) { - if (tag == null && tagId == null) { - final router = ref.watch(routerProvider); - while (router.canPop() == true) { - router.pop(); - } - router.pushReplacement(RoutesPaths.bookmarks); + TagBookmarksScreenState createState() => TagBookmarksScreenState(); +} + +class TagBookmarksScreenState extends ConsumerState { + @override + void initState() { + if (widget.tag == null && widget.tagId == null) { + final router = ref.watch(routerProvider); + while (router.canPop() == true) { + router.pop(); } - }); + router.pushReplacement(RoutesPaths.bookmarks); + return; + } + if (widget.tag != null) { + ref.read(tagBookmarksProvider).tag = widget.tag; + } + if (widget.tagId != null) { + ref.read(tagBookmarksProvider).tagId = widget.tagId; + } + ref.read(tagBookmarksRequestProvider(widget.tag, widget.tagId, ref.read(tagBookmarksProvider).limit)); - if (tag == null && tagId == null) return const Material(); + super.initState(); + } - final bookmarksResponse = - tag != null ? ref.watch(tagBookmarksRequestProvider(tag!)) : ref.watch(tagIdBookmarksRequestProvider(tagId!)); + @override + Widget build(BuildContext context) { + if (widget.tag == null && widget.tagId == null) return const Material(); - final bookmarks = tag != null - ? (bookmarksResponse.value as ApiResponse?)?.content - : (bookmarksResponse.value as ApiResponse?)?.content?.bookmarksResponse; + final provider = ref.watch(tagBookmarksProvider); - final title = tag != null - ? tag!.name! - : ((bookmarksResponse.value as ApiResponse?)?.content?.tag?.name ?? ''); + bool scrollListener(ScrollUpdateNotification scrollNotification) { + if (scrollNotification.metrics.extentAfter < 100 && + provider.loadingMore == false && + provider.bookmarks.length < provider.maxNumber) { + ref.read(tagBookmarksProvider.notifier).setLoadingMore(true); + ref.watch(tagBookmarksRequestLoadMoreProvider); + } + return false; + } + print(provider.maxNumber); return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ @@ -61,52 +75,65 @@ class TagBookmarksScreen extends ConsumerWidget { floating: true, centerTitle: false, forceElevated: innerBoxIsScrolled, - title: Text(title != "" ? "#$title" : ""), + title: Text( + widget.tag != null + ? "#${widget.tag!.name}" + : provider.tag != null + ? "#${provider.tag!.name}" + : '', + ), ), ), ], body: SafeArea( top: false, - bottom: false, child: Builder( builder: (context) => RefreshIndicator( displacement: 120, - onRefresh: () => tag != null - ? ref.watch(tagBookmarksRequestProvider(tag!).future) - : ref.watch(tagIdBookmarksRequestProvider(tagId!).future), - child: CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (bookmarksResponse.isLoading && !bookmarksResponse.isRefreshing) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), + onRefresh: () => ref.read(tagBookmarksProvider.notifier).refresh(), + child: NotificationListener( + onNotification: scrollListener, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), - if (bookmarksResponse.value != null && bookmarksResponse.value!.successful == false) - SliverFillRemaining( - child: ErrorScreen( - error: t.bookmarks.cannotLoadBookmarks, + if (provider.initialLoadStatus == LoadStatus.loading) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), ), - ), - if (bookmarksResponse.value != null && - bookmarksResponse.value!.successful == true && - bookmarks!.results!.isEmpty) - SliverFillRemaining( - child: NoDataScreen( - message: t.tags.tagBookmarks.noBookmarksWithThisTag, + if (provider.initialLoadStatus == LoadStatus.error) + SliverFillRemaining( + child: ErrorScreen( + error: t.bookmarks.cannotLoadBookmarks, + ), ), - ), - if (bookmarksResponse.value != null && - bookmarksResponse.value!.successful == true && - bookmarks!.results!.isNotEmpty) - SliverList.builder( - itemCount: bookmarks.results!.length, - itemBuilder: (context, index) => BookmarkItem( - bookmark: bookmarks.results![index], + if (provider.bookmarks.isEmpty) + SliverFillRemaining( + child: NoDataScreen( + message: t.tags.tagBookmarks.noBookmarksWithThisTag, + ), ), - ), - ], + if (provider.bookmarks.isNotEmpty) + SliverList.builder( + itemCount: + provider.loadingMore == true ? provider.bookmarks.length + 1 : provider.bookmarks.length, + itemBuilder: (context, index) { + if (provider.loadingMore == true && index == provider.bookmarks.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + return BookmarkItem( + bookmark: provider.bookmarks[index], + ); + }, + ), + ], + ), ), ), ),