From 29fa80d6759f3df6512c423589f2087a43c94a15 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 25 Feb 2024 02:23:03 +0100 Subject: [PATCH] Added bookmarks lazy loading --- lib/models/data/bookmarks.dart | 4 +- .../bookmarks/model/bookmarks.model.dart | 13 +- .../provider/bookmarks.provider.dart | 62 ++++++- .../provider/bookmarks.provider.g.dart | 172 ++++++++++++++++-- lib/screens/bookmarks/ui/bookmarks.dart | 115 +++++++----- 5 files changed, 297 insertions(+), 69 deletions(-) diff --git a/lib/models/data/bookmarks.dart b/lib/models/data/bookmarks.dart index 6441dea..54d84fd 100644 --- a/lib/models/data/bookmarks.dart +++ b/lib/models/data/bookmarks.dart @@ -1,7 +1,7 @@ class BookmarksResponse { final int? count; - final int? next; - final int? previous; + final String? next; + final String? previous; final List? results; BookmarksResponse({ diff --git a/lib/screens/bookmarks/model/bookmarks.model.dart b/lib/screens/bookmarks/model/bookmarks.model.dart index 7776cd1..a44692d 100644 --- a/lib/screens/bookmarks/model/bookmarks.model.dart +++ b/lib/screens/bookmarks/model/bookmarks.model.dart @@ -1,9 +1,20 @@ +import 'package:linkdy/constants/enums.dart'; +import 'package:linkdy/models/data/bookmarks.dart'; + class BookmarksModel { int currentPage; int limit; + List bookmarks; + LoadStatus inialLoadStatus; + bool loadingMore; + int maxNumber; BookmarksModel({ this.currentPage = 0, - this.limit = 100, + this.limit = 20, + required this.bookmarks, + this.inialLoadStatus = LoadStatus.loading, + this.loadingMore = false, + this.maxNumber = 0, }); } diff --git a/lib/screens/bookmarks/provider/bookmarks.provider.dart b/lib/screens/bookmarks/provider/bookmarks.provider.dart index ce777e3..c60dae1 100644 --- a/lib/screens/bookmarks/provider/bookmarks.provider.dart +++ b/lib/screens/bookmarks/provider/bookmarks.provider.dart @@ -1,24 +1,58 @@ -import 'package:linkdy/models/data/bookmarks.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/bookmarks/model/bookmarks.model.dart'; -import 'package:linkdy/models/api_response.dart'; import 'package:linkdy/providers/api_client_provider.dart'; +import 'package:linkdy/constants/enums.dart'; part 'bookmarks.provider.g.dart'; @riverpod -FutureOr> bookmarksRequest(BookmarksRequestRef ref) async { - final result = await ref.watch(apiClientProvider)!.fetchBookmarks(); - return result; +FutureOr bookmarksRequest(BookmarksRequestRef ref, int limit) async { + final result = await ref.watch(apiClientProvider)!.fetchBookmarks( + limit: limit, + offset: 0, + ); + + if (result.successful == true) { + ref.read(bookmarksProvider).bookmarks = result.content!.results!; + ref.read(bookmarksProvider).maxNumber = result.content!.count!; + ref.read(bookmarksProvider).inialLoadStatus = LoadStatus.loaded; + } else { + ref.read(bookmarksProvider).inialLoadStatus = LoadStatus.error; + } + ref.read(bookmarksProvider.notifier).notifyListeners(); } @riverpod +FutureOr bookmarksRequestLoadMore(BookmarksRequestLoadMoreRef ref) async { + final provider = ref.watch(bookmarksProvider); + + final newOffset = provider.limit * (provider.currentPage + 1); + + final result = await ref.watch(apiClientProvider)!.fetchBookmarks( + limit: provider.limit, + offset: newOffset, + ); + + if (result.successful == true) { + provider.bookmarks = [...provider.bookmarks, ...result.content!.results!]; + provider.maxNumber = result.content!.count!; + provider.currentPage = provider.currentPage + 1; + } + + ref.read(bookmarksProvider.notifier).setLoadingMore(false); +} + +@Riverpod(keepAlive: true) class Bookmarks extends _$Bookmarks { @override BookmarksModel build() { - return BookmarksModel(); + final model = BookmarksModel( + bookmarks: [], + ); + ref.read(bookmarksRequestProvider(model.limit)); + return model; } void setCurrentPage(int page) { @@ -28,4 +62,20 @@ class Bookmarks extends _$Bookmarks { void setLimit(int limit) { state.limit = limit; } + + void setLoadingMore(bool value) { + state.loadingMore = value; + ref.notifyListeners(); + } + + void notifyListeners() { + ref.notifyListeners(); + } + + Future refresh() async { + await ref.read(bookmarksRequestProvider(state.limit).future); + state.currentPage = 0; + state.loadingMore = false; + ref.notifyListeners(); + } } diff --git a/lib/screens/bookmarks/provider/bookmarks.provider.g.dart b/lib/screens/bookmarks/provider/bookmarks.provider.g.dart index a56c33a..c56b7cd 100644 --- a/lib/screens/bookmarks/provider/bookmarks.provider.g.dart +++ b/lib/screens/bookmarks/provider/bookmarks.provider.g.dart @@ -6,29 +6,177 @@ part of 'bookmarks.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$bookmarksRequestHash() => r'c80328ae148c39617997a44ac5c34f27daf25a8a'; +String _$bookmarksRequestHash() => r'716bbb168bef640b11619d7f6a333c53b3c29bdb'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} /// See also [bookmarksRequest]. @ProviderFor(bookmarksRequest) -final bookmarksRequestProvider = - AutoDisposeFutureProvider>.internal( - bookmarksRequest, - name: r'bookmarksRequestProvider', +const bookmarksRequestProvider = BookmarksRequestFamily(); + +/// See also [bookmarksRequest]. +class BookmarksRequestFamily extends Family> { + /// See also [bookmarksRequest]. + const BookmarksRequestFamily(); + + /// See also [bookmarksRequest]. + BookmarksRequestProvider call( + int limit, + ) { + return BookmarksRequestProvider( + limit, + ); + } + + @override + BookmarksRequestProvider getProviderOverride( + covariant BookmarksRequestProvider provider, + ) { + return call( + provider.limit, + ); + } + + 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'bookmarksRequestProvider'; +} + +/// See also [bookmarksRequest]. +class BookmarksRequestProvider extends AutoDisposeFutureProvider { + /// See also [bookmarksRequest]. + BookmarksRequestProvider( + int limit, + ) : this._internal( + (ref) => bookmarksRequest( + ref as BookmarksRequestRef, + limit, + ), + from: bookmarksRequestProvider, + name: r'bookmarksRequestProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$bookmarksRequestHash, + dependencies: BookmarksRequestFamily._dependencies, + allTransitiveDependencies: + BookmarksRequestFamily._allTransitiveDependencies, + limit: limit, + ); + + BookmarksRequestProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.limit, + }) : super.internal(); + + final int limit; + + @override + Override overrideWith( + FutureOr Function(BookmarksRequestRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: BookmarksRequestProvider._internal( + (ref) => create(ref as BookmarksRequestRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + limit: limit, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _BookmarksRequestProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BookmarksRequestProvider && other.limit == limit; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, limit.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin BookmarksRequestRef on AutoDisposeFutureProviderRef { + /// The parameter `limit` of this provider. + int get limit; +} + +class _BookmarksRequestProviderElement + extends AutoDisposeFutureProviderElement with BookmarksRequestRef { + _BookmarksRequestProviderElement(super.provider); + + @override + int get limit => (origin as BookmarksRequestProvider).limit; +} + +String _$bookmarksRequestLoadMoreHash() => + r'523edffa3f18851e1879948e7f8996904aa0aa32'; + +/// See also [bookmarksRequestLoadMore]. +@ProviderFor(bookmarksRequestLoadMore) +final bookmarksRequestLoadMoreProvider = + AutoDisposeFutureProvider.internal( + bookmarksRequestLoadMore, + name: r'bookmarksRequestLoadMoreProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$bookmarksRequestHash, + : _$bookmarksRequestLoadMoreHash, dependencies: null, allTransitiveDependencies: null, ); -typedef BookmarksRequestRef - = AutoDisposeFutureProviderRef>; -String _$bookmarksHash() => r'4a1726830b4e7bbc131d69761aeb4702ca6b7180'; +typedef BookmarksRequestLoadMoreRef = AutoDisposeFutureProviderRef; +String _$bookmarksHash() => r'454f0fcfa9ee776828ba50edf4906ed0a267cb97'; /// See also [Bookmarks]. @ProviderFor(Bookmarks) -final bookmarksProvider = - AutoDisposeNotifierProvider.internal( +final bookmarksProvider = NotifierProvider.internal( Bookmarks.new, name: r'bookmarksProvider', debugGetCreateSourceHash: @@ -37,6 +185,6 @@ final bookmarksProvider = allTransitiveDependencies: null, ); -typedef _$Bookmarks = AutoDisposeNotifier; +typedef _$Bookmarks = Notifier; // 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/lib/screens/bookmarks/ui/bookmarks.dart b/lib/screens/bookmarks/ui/bookmarks.dart index 722e518..28946e9 100644 --- a/lib/screens/bookmarks/ui/bookmarks.dart +++ b/lib/screens/bookmarks/ui/bookmarks.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_item.dart'; @@ -18,7 +19,7 @@ class BookmarksScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final bookmarks = ref.watch(bookmarksRequestProvider); + final bookmarks = ref.watch(bookmarksProvider); final width = MediaQuery.of(context).size.width; @@ -38,6 +39,16 @@ class BookmarksScreen extends ConsumerWidget { ); } + bool scrollListener(ScrollUpdateNotification scrollNotification) { + if (scrollNotification.metrics.extentAfter < 100 && + bookmarks.loadingMore == false && + bookmarks.bookmarks.length < bookmarks.maxNumber) { + ref.read(bookmarksProvider.notifier).setLoadingMore(true); + ref.watch(bookmarksRequestLoadMoreProvider); + } + return false; + } + return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ @@ -66,57 +77,65 @@ class BookmarksScreen extends ConsumerWidget { child: Builder( builder: (context) => RefreshIndicator( displacement: 120, - onRefresh: () => ref.refresh(bookmarksRequestProvider.future), - child: CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (bookmarks.isLoading && !bookmarks.isRefreshing) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), + onRefresh: () => ref.read(bookmarksProvider.notifier).refresh(), + child: NotificationListener( + onNotification: scrollListener, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), - if (bookmarks.value != null && bookmarks.value!.successful == false) - SliverFillRemaining( - child: ErrorScreen( - error: t.bookmarks.cannotLoadBookmarks, + if (bookmarks.inialLoadStatus == LoadStatus.loading) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), ), - ), - if (bookmarks.value != null && - bookmarks.value!.successful == true && - bookmarks.value!.content!.results!.isEmpty) - SliverFillRemaining( - child: NoDataScreen( - message: t.bookmarks.noBookmarksAdded, + if (bookmarks.inialLoadStatus == LoadStatus.error) + SliverFillRemaining( + child: ErrorScreen( + error: t.bookmarks.cannotLoadBookmarks, + ), ), - ), - if (bookmarks.value != null && - bookmarks.value!.successful == true && - bookmarks.value!.content!.results!.isNotEmpty) - SliverList.builder( - itemCount: bookmarks.value!.content!.results!.length + 1, - itemBuilder: (context, index) { - // index == bookmarks.value!.content!.results!.length -> itemCount + 1 - if (index == bookmarks.value!.content!.results!.length) { - // Bottom gap for FAB - return const SizedBox(height: 80); - } - final link = bookmarks.value?.content?.results?[index]; - return Column( - children: [ - BookmarkItem(bookmark: link!), - if (index < bookmarks.value!.content!.results!.length - 1) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Divider( - color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + if (bookmarks.bookmarks.isEmpty) + SliverFillRemaining( + child: NoDataScreen( + message: t.bookmarks.noBookmarksAdded, + ), + ), + if (bookmarks.bookmarks.isNotEmpty) + SliverList.builder( + itemCount: bookmarks.bookmarks.length + 1, + itemBuilder: (context, index) { + // index == bookmarks.value!.content!.results!.length -> itemCount + 1 + if (index == bookmarks.bookmarks.length) { + if (bookmarks.loadingMore) { + return const SizedBox( + height: 80, + child: Center( + child: CircularProgressIndicator(), ), - ), - ], - ); - }, - ), - ], + ); + } + // Bottom gap for FAB + return const SizedBox(height: 80); + } + + final link = bookmarks.bookmarks[index]; + return Column( + children: [ + BookmarkItem(bookmark: link), + if (index < bookmarks.bookmarks.length - 1) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + ), + ], + ); + }, + ), + ], + ), ), ), ),