♻️ Rebuilt fetching state machine

This commit is contained in:
2026-01-01 11:40:28 +08:00
parent eea56a742e
commit 38dffa414f
34 changed files with 665 additions and 430 deletions

View File

@@ -18,7 +18,8 @@ final indexedCloudFileListProvider = AsyncNotifierProvider.autoDispose(
IndexedCloudFileListNotifier.new,
);
class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
class IndexedCloudFileListNotifier
extends AsyncNotifier<PaginationState<FileListItem>>
with AsyncPaginationController<FileListItem> {
String _currentPath = '/';
String? _poolId;
@@ -51,6 +52,19 @@ class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
ref.invalidateSelf();
}
@override
FutureOr<PaginationState<FileListItem>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: null,
hasMore: false,
cursor: null,
);
}
@override
Future<List<FileListItem>> fetch() async {
final client = ref.read(apiClientProvider);
@@ -96,7 +110,8 @@ final unindexedFileListProvider = AsyncNotifierProvider.autoDispose(
UnindexedFileListNotifier.new,
);
class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
class UnindexedFileListNotifier
extends AsyncNotifier<PaginationState<FileListItem>>
with AsyncPaginationController<FileListItem> {
String? _poolId;
bool _recycled = false;
@@ -131,6 +146,19 @@ class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
static const int pageSize = 20;
@override
FutureOr<PaginationState<FileListItem>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<FileListItem>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -2,6 +2,42 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class PaginationState<T> {
final List<T> items;
final bool isLoading;
final bool isReloading;
final int? totalCount;
final bool hasMore;
final String? cursor;
const PaginationState({
required this.items,
required this.isLoading,
required this.isReloading,
required this.totalCount,
required this.hasMore,
required this.cursor,
});
PaginationState<T> copyWith({
List<T>? items,
bool? isLoading,
bool? isReloading,
int? totalCount,
bool? hasMore,
String? cursor,
}) {
return PaginationState<T>(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
isReloading: isReloading ?? this.isReloading,
totalCount: totalCount ?? this.totalCount,
hasMore: hasMore ?? this.hasMore,
cursor: cursor ?? this.cursor,
);
}
}
abstract class PaginationController<T> {
int? get totalCount;
int get fetchedCount;
@@ -27,51 +63,84 @@ abstract class PaginationFiltered<F> {
Future<void> applyFilter(F filter);
}
mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
mixin AsyncPaginationController<T> on AsyncNotifier<PaginationState<T>>
implements PaginationController<T> {
@override
int? totalCount;
@override
int get fetchedCount => isReloading ? 0 : state.value?.length ?? 0;
int get fetchedCount =>
state.value?.isReloading == true ? 0 : state.value?.items.length ?? 0;
@override
bool get fetchedAll =>
!hasMore || (totalCount != null && fetchedCount >= totalCount!);
!(state.value?.hasMore ?? true) ||
((state.value?.totalCount != null &&
fetchedCount >= state.value!.totalCount!));
@override
bool isLoading = false;
bool get isLoading => state.value?.isLoading ?? false;
@override
bool isReloading = false;
bool get isReloading => state.value?.isReloading ?? false;
@override
bool hasMore = true;
bool get hasMore => state.value?.hasMore ?? true;
@override
String? cursor;
String? get cursor => state.value?.cursor;
@override
FutureOr<List<T>> build() async {
cursor = null;
return fetch();
set hasMore(bool value) {
if (state is AsyncData) {
state = AsyncData((state as AsyncData).value.copyWith(hasMore: value));
}
}
@override
set cursor(String? value) {
if (state is AsyncData) {
state = AsyncData((state as AsyncData).value.copyWith(cursor: value));
}
}
@override
FutureOr<PaginationState<T>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<void> refresh() async {
isLoading = true;
isReloading = true;
totalCount = null;
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
state = AsyncData(
state.value!.copyWith(
isLoading: true,
isReloading: true,
totalCount: null,
hasMore: true,
cursor: null,
),
);
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
isReloading = false;
isLoading = false;
state = newState;
final newItems = await fetch();
state = AsyncData(
state.value!.copyWith(
items: newItems,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
),
);
}
@override
@@ -79,16 +148,16 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
if (fetchedAll) return;
if (isLoading) return;
isLoading = true;
state = AsyncLoading<List<T>>();
state = AsyncData(state.value!.copyWith(isLoading: true));
final newState = await AsyncValue.guard<List<T>>(() async {
final elements = await fetch();
return [...?state.value, ...elements];
});
final newItems = await fetch();
isLoading = false;
state = newState;
state = AsyncData(
state.value!.copyWith(
items: [...state.value!.items, ...newItems],
isLoading: false,
),
);
}
}
@@ -97,20 +166,29 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
@override
Future<void> applyFilter(F filter) async {
if (currentFilter == filter) return;
// Reset the data
isReloading = true;
isLoading = true;
totalCount = null;
hasMore = true;
cursor = null;
state = AsyncLoading<List<T>>();
state = AsyncData(
state.value!.copyWith(
isReloading: true,
isLoading: true,
totalCount: null,
hasMore: true,
cursor: null,
),
);
currentFilter = filter;
final newState = await AsyncValue.guard<List<T>>(() async {
return await fetch();
});
isLoading = false;
isReloading = false;
state = newState;
final newItems = await fetch();
state = AsyncData(
state.value!.copyWith(
items: newItems,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
),
);
}
}

View File

@@ -1,4 +1,6 @@
// Post Categories Notifier
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
@@ -8,11 +10,25 @@ import 'package:island/pods/paging.dart';
final postCategoriesProvider =
AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
List<SnPostCategory>
PaginationState<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
class PostCategoriesNotifier
extends AsyncNotifier<PaginationState<SnPostCategory>>
with AsyncPaginationController<SnPostCategory> {
@override
FutureOr<PaginationState<SnPostCategory>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
@@ -30,12 +46,26 @@ class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
// Post Tags Notifier
final postTagsProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
AsyncNotifierProvider.autoDispose<
PostTagsNotifier,
PaginationState<SnPostTag>
>(PostTagsNotifier.new);
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
class PostTagsNotifier extends AsyncNotifier<PaginationState<SnPostTag>>
with AsyncPaginationController<SnPostTag> {
@override
FutureOr<PaginationState<SnPostTag>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
@@ -39,7 +41,7 @@ final postListProvider = AsyncNotifierProvider.autoDispose.family(
PostListNotifier.new,
);
class PostListNotifier extends AsyncNotifier<List<SnPost>>
class PostListNotifier extends AsyncNotifier<PaginationState<SnPost>>
with
AsyncPaginationController<SnPost>,
AsyncPaginationFilter<PostListQuery, SnPost> {
@@ -53,9 +55,17 @@ class PostListNotifier extends AsyncNotifier<List<SnPost>>
late PostListQuery currentFilter;
@override
Future<List<SnPost>> build() async {
FutureOr<PaginationState<SnPost>> build() async {
currentFilter = config.initialFilter;
return fetch();
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart';
@@ -7,12 +9,26 @@ final activityListProvider = AsyncNotifierProvider.autoDispose(
ActivityListNotifier.new,
);
class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
class ActivityListNotifier
extends AsyncNotifier<PaginationState<SnTimelineEvent>>
with
AsyncPaginationController<SnTimelineEvent>,
AsyncPaginationFilter<String?, SnTimelineEvent> {
static const int pageSize = 20;
@override
FutureOr<PaginationState<SnTimelineEvent>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
String? currentFilter;
@@ -54,9 +70,9 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
final currentState = state.value;
if (currentState == null) return;
final updatedItems = [...currentState];
final updatedItems = [...currentState.items];
updatedItems[index] = activity;
state = AsyncData(updatedItems);
state = AsyncData(currentState.copyWith(items: updatedItems));
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
@@ -28,10 +30,23 @@ final socialCreditHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
);
class SocialCreditHistoryNotifier
extends AsyncNotifier<List<SnSocialCreditRecord>>
extends AsyncNotifier<PaginationState<SnSocialCreditRecord>>
with AsyncPaginationController<SnSocialCreditRecord> {
static const int pageSize = 20;
@override
FutureOr<PaginationState<SnSocialCreditRecord>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnSocialCreditRecord>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -14,14 +16,30 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:styled_widget/styled_widget.dart';
final levelingHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
LevelingHistoryNotifier.new,
);
final levelingHistoryNotifierProvider =
AsyncNotifierProvider.autoDispose<
LevelingHistoryNotifier,
PaginationState<SnExperienceRecord>
>(LevelingHistoryNotifier.new);
class LevelingHistoryNotifier extends AsyncNotifier<List<SnExperienceRecord>>
class LevelingHistoryNotifier
extends AsyncNotifier<PaginationState<SnExperienceRecord>>
with AsyncPaginationController<SnExperienceRecord> {
static const int pageSize = 20;
@override
FutureOr<PaginationState<SnExperienceRecord>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnExperienceRecord>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -29,12 +31,28 @@ Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
.toList();
}
final relationshipListNotifierProvider = AsyncNotifierProvider.autoDispose(
RelationshipListNotifier.new,
);
final relationshipListNotifierProvider =
AsyncNotifierProvider.autoDispose<
RelationshipListNotifier,
PaginationState<SnRelationship>
>(RelationshipListNotifier.new);
class RelationshipListNotifier extends AsyncNotifier<List<SnRelationship>>
class RelationshipListNotifier
extends AsyncNotifier<PaginationState<SnRelationship>>
with AsyncPaginationController<SnRelationship> {
@override
FutureOr<PaginationState<SnRelationship>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnRelationship>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -588,7 +588,8 @@ final chatMemberListProvider = AsyncNotifierProvider.autoDispose.family(
ChatMemberListNotifier.new,
);
class ChatMemberListNotifier extends AsyncNotifier<List<SnChatMember>>
class ChatMemberListNotifier
extends AsyncNotifier<PaginationState<SnChatMember>>
with AsyncPaginationController<SnChatMember> {
static const pageSize = 20;

View File

@@ -96,13 +96,27 @@ Future<SnActorStatusResponse> publisherActorStatus(
final publisherMemberListNotifierProvider = AsyncNotifierProvider.family
.autoDispose(PublisherMemberListNotifier.new);
class PublisherMemberListNotifier extends AsyncNotifier<List<SnPublisherMember>>
class PublisherMemberListNotifier
extends AsyncNotifier<PaginationState<SnPublisherMember>>
with AsyncPaginationController<SnPublisherMember> {
static const int pageSize = 20;
final String arg;
PublisherMemberListNotifier(this.arg);
@override
FutureOr<PaginationState<SnPublisherMember>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnPublisherMember>> fetch() async {
final apiClient = ref.read(apiClientProvider);

View File

@@ -21,7 +21,7 @@ final pollListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
PollListNotifier.new,
);
class PollListNotifier extends AsyncNotifier<List<SnPollWithStats>>
class PollListNotifier extends AsyncNotifier<PaginationState<SnPollWithStats>>
with AsyncPaginationController<SnPollWithStats> {
static const int pageSize = 20;

View File

@@ -18,7 +18,7 @@ final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
SiteListNotifier.new,
);
class SiteListNotifier extends AsyncNotifier<List<SnPublicationSite>>
class SiteListNotifier extends AsyncNotifier<PaginationState<SnPublicationSite>>
with AsyncPaginationController<SnPublicationSite> {
static const int pageSize = 20;

View File

@@ -140,7 +140,7 @@ final stickerPacksProvider = AsyncNotifierProvider.family.autoDispose(
StickerPacksNotifier.new,
);
class StickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
class StickerPacksNotifier extends AsyncNotifier<PaginationState<SnStickerPack>>
with AsyncPaginationController<SnStickerPack> {
static const int pageSize = 20;

View File

@@ -412,11 +412,11 @@ class NotificationsCard extends HookConsumerWidget {
loading: () => const SkeletonNotificationTile(),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (notificationList) {
if (notificationList.isEmpty) {
if (notificationList.items.isEmpty) {
return Center(child: Text('noNotificationsYet').tr());
}
// Get the most recent notification (first in the list)
final recentNotification = notificationList.first;
final recentNotification = notificationList.items.first;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -22,7 +22,7 @@ final articlesListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
ArticlesListNotifier.new,
);
class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
class ArticlesListNotifier extends AsyncNotifier<PaginationState<SnWebArticle>>
with AsyncPaginationController<SnWebArticle> {
static const int pageSize = 20;

View File

@@ -26,7 +26,7 @@ final marketplaceWebFeedContentNotifierProvider = AsyncNotifierProvider.family
.autoDispose(MarketplaceWebFeedContentNotifier.new);
class MarketplaceWebFeedContentNotifier
extends AsyncNotifier<List<SnWebArticle>>
extends AsyncNotifier<PaginationState<SnWebArticle>>
with AsyncPaginationController<SnWebArticle> {
static const int pageSize = 20;

View File

@@ -16,7 +16,8 @@ final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider.autoDispose(
MarketplaceWebFeedsNotifier.new,
);
class MarketplaceWebFeedsNotifier extends AsyncNotifier<List<SnWebFeed>>
class MarketplaceWebFeedsNotifier
extends AsyncNotifier<PaginationState<SnWebFeed>>
with
AsyncPaginationController<SnWebFeed>,
AsyncPaginationFilter<String?, SnWebFeed> {

View File

@@ -164,10 +164,24 @@ final notificationListProvider = AsyncNotifierProvider.autoDispose(
NotificationListNotifier.new,
);
class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
class NotificationListNotifier
extends AsyncNotifier<PaginationState<SnNotification>>
with AsyncPaginationController<SnNotification> {
static const int pageSize = 5;
@override
FutureOr<PaginationState<SnNotification>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnNotification>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -160,7 +160,7 @@ class PostSearchScreen extends HookConsumerWidget {
);
},
),
if (searchState.value?.isEmpty == true &&
if (searchState.value?.items.isEmpty == true &&
searchController.text.isNotEmpty &&
!searchState.isLoading)
SliverFillRemaining(
@@ -290,7 +290,7 @@ class PostSearchScreen extends HookConsumerWidget {
);
},
),
if (searchState.value?.isEmpty == true &&
if (searchState.value?.items.isEmpty == true &&
searchController.text.isNotEmpty &&
!searchState.isLoading)
SliverFillRemaining(

View File

@@ -492,11 +492,10 @@ class _RealmActionMenu extends HookConsumerWidget {
}
final realmMemberListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<RealmMemberListNotifier, List<SnRealmMember>, String>(
RealmMemberListNotifier.new,
);
.family(RealmMemberListNotifier.new);
class RealmMemberListNotifier extends AsyncNotifier<List<SnRealmMember>>
class RealmMemberListNotifier
extends AsyncNotifier<PaginationState<SnRealmMember>>
with AsyncPaginationController<SnRealmMember> {
String arg;
RealmMemberListNotifier(this.arg);

View File

@@ -31,7 +31,8 @@ sealed class MarketplaceStickerQuery with _$MarketplaceStickerQuery {
final marketplaceStickerPacksNotifierProvider =
AsyncNotifierProvider.autoDispose(MarketplaceStickerPacksNotifier.new);
class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
class MarketplaceStickerPacksNotifier
extends AsyncNotifier<PaginationState<SnStickerPack>>
with
AsyncPaginationController<SnStickerPack>,
AsyncPaginationFilter<MarketplaceStickerQuery, SnStickerPack> {

View File

@@ -963,7 +963,8 @@ final transactionListProvider = AsyncNotifierProvider.autoDispose(
TransactionListNotifier.new,
);
class TransactionListNotifier extends AsyncNotifier<List<SnTransaction>>
class TransactionListNotifier
extends AsyncNotifier<PaginationState<SnTransaction>>
with AsyncPaginationController<SnTransaction> {
static const int pageSize = 20;
@@ -992,7 +993,7 @@ final walletFundsProvider = AsyncNotifierProvider.autoDispose(
WalletFundsNotifier.new,
);
class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>>
class WalletFundsNotifier extends AsyncNotifier<PaginationState<SnWalletFund>>
with AsyncPaginationController<SnWalletFund> {
static const int pageSize = 20;
@@ -1020,7 +1021,7 @@ final walletFundRecipientsProvider = AsyncNotifierProvider.autoDispose(
);
class WalletFundRecipientsNotifier
extends AsyncNotifier<List<SnWalletFundRecipient>>
extends AsyncNotifier<PaginationState<SnWalletFundRecipient>>
with AsyncPaginationController<SnWalletFundRecipient> {
static const int _pageSize = 20;
@@ -1484,7 +1485,7 @@ class WalletScreen extends HookConsumerWidget {
return funds.when(
data: (fundList) {
if (fundList.isEmpty) {
if (fundList.items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -1514,9 +1515,9 @@ class WalletScreen extends HookConsumerWidget {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: fundList.length,
itemCount: fundList.items.length,
itemBuilder: (context, index) {
final fund = fundList[index];
final fund = fundList.items[index];
final claimedCount = fund.recipients
.where((r) => r.isReceived)
.length;

View File

@@ -17,7 +17,8 @@ final chatCloudFileListNotifierProvider = AsyncNotifierProvider.autoDispose(
ChatCloudFileListNotifier.new,
);
class ChatCloudFileListNotifier extends AsyncNotifier<List<SnCloudFile>>
class ChatCloudFileListNotifier
extends AsyncNotifier<PaginationState<SnCloudFile>>
with AsyncPaginationController<SnCloudFile> {
@override
Future<List<SnCloudFile>> fetch() async {
@@ -31,10 +32,9 @@ class ChatCloudFileListNotifier extends AsyncNotifier<List<SnCloudFile>>
queryParameters: queryParameters,
);
final List<SnCloudFile> items =
(response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList();
final List<SnCloudFile> items = (response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList();
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
return items;
@@ -83,28 +83,24 @@ class ChatLinkAttachment extends HookConsumerWidget {
width: 48,
child: switch (itemType) {
'image' => CloudImageWidget(file: item),
'audio' =>
const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' =>
const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ =>
const Icon(
Symbols.body_system,
fill: 1,
).center(),
'audio' => const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' => const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ => const Icon(
Symbols.body_system,
fill: 1,
).center(),
},
),
),
title:
item.name.isEmpty
? Text('untitled').tr().italic()
: Text(item.name),
title: item.name.isEmpty
? Text('untitled').tr().italic()
: Text(item.name),
onTap: () {
Navigator.pop(context, item);
},
@@ -128,9 +124,8 @@ class ChatLinkAttachment extends HookConsumerWidget {
),
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(16),
InkWell(

View File

@@ -15,7 +15,7 @@ import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:visibility_detector/visibility_detector.dart';
class PaginationList<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider;
final ProviderListenable<AsyncValue<PaginationState<T>>> provider;
final Refreshable<PaginationController<T>> notifier;
final Widget? Function(BuildContext, int, T) itemBuilder;
final Widget? Function(BuildContext, int, T)? seperatorBuilder;
@@ -48,7 +48,8 @@ class PaginationList<T> extends HookConsumerWidget {
// For sliver cases, avoid animation to prevent complex sliver issues
if (isSliver) {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
if ((data.isLoading || data.value?.isLoading == true) &&
data.value?.items.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
@@ -77,9 +78,9 @@ class PaginationList<T> extends HookConsumerWidget {
}
final listView = SuperSliverList.separated(
itemCount: (data.value?.length ?? 0) + 1,
itemCount: (data.value?.items.length ?? 0) + 1,
itemBuilder: (context, idx) {
if (idx == data.value?.length) {
if (idx == data.value?.items.length) {
return PaginationListFooter(
noti: noti,
data: data,
@@ -87,13 +88,13 @@ class PaginationList<T> extends HookConsumerWidget {
skeletonMaxWidth: footerSkeletonMaxWidth,
);
}
final entry = data.value?[idx];
final entry = data.value?.items[idx];
if (entry != null) return itemBuilder(context, idx, entry);
return null;
},
separatorBuilder: (context, index) {
if (seperatorBuilder != null) {
final entry = data.value?[index];
final entry = data.value?.items[index];
if (entry != null) {
return seperatorBuilder!(context, index, entry) ??
const SizedBox();
@@ -114,7 +115,8 @@ class PaginationList<T> extends HookConsumerWidget {
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
if ((data.isLoading || data.value?.isLoading == true) &&
data.value?.items.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
@@ -147,9 +149,9 @@ class PaginationList<T> extends HookConsumerWidget {
final listView = SuperListView.separated(
padding: padding,
itemCount: (data.value?.length ?? 0) + 1,
itemCount: (data.value?.items.length ?? 0) + 1,
itemBuilder: (context, idx) {
if (idx == data.value?.length) {
if (idx == data.value?.items.length) {
return PaginationListFooter(
noti: noti,
data: data,
@@ -157,13 +159,13 @@ class PaginationList<T> extends HookConsumerWidget {
skeletonMaxWidth: footerSkeletonMaxWidth,
);
}
final entry = data.value?[idx];
final entry = data.value?.items[idx];
if (entry != null) return itemBuilder(context, idx, entry);
return null;
},
separatorBuilder: (context, index) {
if (seperatorBuilder != null) {
final entry = data.value?[index];
final entry = data.value?.items[index];
if (entry != null) {
return seperatorBuilder!(context, index, entry) ??
const SizedBox();
@@ -193,7 +195,7 @@ class PaginationList<T> extends HookConsumerWidget {
}
class PaginationWidget<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider;
final ProviderListenable<AsyncValue<PaginationState<T>>> provider;
final Refreshable<PaginationController<T>> notifier;
final Widget Function(List<T>, Widget) contentBuilder;
final bool isRefreshable;
@@ -220,7 +222,8 @@ class PaginationWidget<T> extends HookConsumerWidget {
// For sliver cases, avoid animation to prevent complex sliver issues
if (isSliver) {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
if ((data.isLoading || data.value?.isLoading == true) &&
data.value?.items.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
@@ -254,7 +257,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
);
final content = contentBuilder(data.value ?? [], footer);
final content = contentBuilder(data.value?.items ?? [], footer);
return isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
@@ -263,7 +266,8 @@ class PaginationWidget<T> extends HookConsumerWidget {
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
if ((data.isLoading || data.value?.isLoading == true) &&
data.value?.items.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
@@ -300,7 +304,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
);
final content = contentBuilder(data.value ?? [], footer);
final content = contentBuilder(data.value?.items ?? [], footer);
return SizedBox(
key: const ValueKey('data'),
@@ -319,7 +323,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
class PaginationListFooter<T> extends HookConsumerWidget {
final PaginationController<T> noti;
final AsyncValue<List<T>> data;
final AsyncValue<PaginationState<T>> data;
final Widget? skeletonChild;
final double? skeletonMaxWidth;
final bool isSliver;
@@ -347,7 +351,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
child: skeletonChild ?? _DefaultSkeletonChild(maxWidth: skeletonMaxWidth),
);
final child = hasBeenVisible.value
? data.isLoading
? (data.isLoading || data.value?.isLoading == true)
? placeholder
: Row(
spacing: 8,
@@ -363,7 +367,9 @@ class PaginationListFooter<T> extends HookConsumerWidget {
key: Key("pagination-list-${noti.hashCode}"),
onVisibilityChanged: (VisibilityInfo info) {
hasBeenVisible.value = true;
if (!noti.fetchedAll && !data.isLoading && !data.hasError) {
if (!noti.fetchedAll &&
!(data.isLoading || data.value?.isLoading == true) &&
!data.hasError) {
if (context.mounted) noti.fetchFurther();
}
},

View File

@@ -15,12 +15,11 @@ import 'package:island/widgets/poll/poll_stats_widget.dart';
import 'package:island/widgets/response.dart';
import 'package:styled_widget/styled_widget.dart';
final pollFeedbackNotifierProvider = AsyncNotifierProvider.autoDispose
.family<PollFeedbackNotifier, List<SnPollAnswer>, String>(
PollFeedbackNotifier.new,
);
final pollFeedbackNotifierProvider = AsyncNotifierProvider.autoDispose.family(
PollFeedbackNotifier.new,
);
class PollFeedbackNotifier extends AsyncNotifier<List<SnPollAnswer>>
class PollFeedbackNotifier extends AsyncNotifier<PaginationState<SnPollAnswer>>
with AsyncPaginationController<SnPollAnswer> {
static const int pageSize = 20;
@@ -70,7 +69,8 @@ class PollFeedbackSheet extends HookConsumerWidget {
return Column(
children: [
_PollAnswerTile(answer: answer, poll: data),
if (index < (ref.read(provider).value?.length ?? 0) - 1)
if (index <
(ref.read(provider).value?.items.length ?? 0) - 1)
const Divider(height: 1).padding(vertical: 4),
],
);

View File

@@ -44,144 +44,125 @@ class ComposeFundSheet extends HookConsumerWidget {
children: [
// Link/Select existing fund list
fundsData.when(
data:
(funds) =>
funds.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Symbols.money_bag,
size: 48,
color:
Theme.of(
context,
).colorScheme.outline,
),
const Gap(16),
Text(
'noFundsCreated'.tr(),
style:
Theme.of(
context,
).textTheme.titleMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: funds.length,
itemBuilder: (context, index) {
final fund = funds[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap:
() =>
Navigator.of(context).pop(fund),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.money_bag,
color:
Theme.of(
context,
).colorScheme.primary,
fill: 1,
),
const Gap(8),
Expanded(
child: Text(
'${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}',
style: TextStyle(
fontSize: 18,
fontWeight:
FontWeight.bold,
color:
Theme.of(context)
.colorScheme
.primary,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color:
_getFundStatusColor(
context,
fund.status,
).withOpacity(0.1),
borderRadius:
BorderRadius.circular(
12,
),
),
child: Text(
_getFundStatusText(
fund.status,
),
style: TextStyle(
color:
_getFundStatusColor(
context,
fund.status,
),
fontSize: 12,
fontWeight:
FontWeight.w600,
),
),
),
],
),
if (fund.message != null &&
fund.message!.isNotEmpty) ...[
const Gap(8),
Text(
fund.message!,
style:
Theme.of(
context,
).textTheme.bodyMedium,
),
],
const Gap(8),
Text(
'${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
],
),
),
),
);
},
data: (funds) => funds.items.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Symbols.money_bag,
size: 48,
color: Theme.of(context).colorScheme.outline,
),
loading:
() => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => Center(child: Text('Error: $error')),
const Gap(16),
Text(
'noFundsCreated'.tr(),
style: Theme.of(
context,
).textTheme.titleMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: funds.items.length,
itemBuilder: (context, index) {
final fund = funds.items[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: () => Navigator.of(context).pop(fund),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.money_bag,
color: Theme.of(
context,
).colorScheme.primary,
fill: 1,
),
const Gap(8),
Expanded(
child: Text(
'${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(
context,
).colorScheme.primary,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getFundStatusColor(
context,
fund.status,
).withOpacity(0.1),
borderRadius:
BorderRadius.circular(12),
),
child: Text(
_getFundStatusText(fund.status),
style: TextStyle(
color: _getFundStatusColor(
context,
fund.status,
),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
if (fund.message != null &&
fund.message!.isNotEmpty) ...[
const Gap(8),
Text(
fund.message!,
style: Theme.of(
context,
).textTheme.bodyMedium,
),
],
const Gap(8),
Text(
'${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
},
),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, stack) =>
Center(child: Text('Error: $error')),
),
// Create new fund and return it
@@ -208,127 +189,125 @@ class ComposeFundSheet extends HookConsumerWidget {
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
icon:
isPushing.value
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Symbols.add_circle),
icon: isPushing.value
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Symbols.add_circle),
label: Text('create'.tr()),
onPressed:
isPushing.value
? null
: () async {
errorText.value = null;
onPressed: isPushing.value
? null
: () async {
errorText.value = null;
isPushing.value = true;
// Show modal bottom sheet with fund creation form and await result
final result = await showModalBottomSheet<
Map<String, dynamic>
>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) =>
const CreateFundSheet(),
isPushing.value = true;
// Show modal bottom sheet with fund creation form and await result
final result =
await showModalBottomSheet<
Map<String, dynamic>
>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) =>
const CreateFundSheet(),
);
if (result == null) {
isPushing.value = false;
return;
}
try {
if (!context.mounted) return;
final client = ref.read(
apiClientProvider,
);
showLoadingModal(context);
final resp = await client.post(
'/pass/wallets/funds',
data: result,
options: Options(
headers: {'X-Noop': true},
),
);
if (result == null) {
isPushing.value = false;
final fund = SnWalletFund.fromJson(
resp.data,
);
if (fund.status == 0) {
// Return the fund that was just created (but not yet paid)
if (context.mounted) {
hideLoadingModal(context);
Navigator.of(context).pop(fund);
}
return;
}
try {
if (!context.mounted) return;
final orderResp = await client.post(
'/pass/wallets/funds/${fund.id}/order',
);
final order = SnWalletOrder.fromJson(
orderResp.data,
);
final client = ref.read(
apiClientProvider,
);
if (context.mounted) {
hideLoadingModal(context);
}
// Show payment overlay to complete the payment
if (!context.mounted) return;
final paidOrder =
await PaymentOverlay.show(
context: context,
order: order,
enableBiometric: true,
);
if (paidOrder != null &&
context.mounted) {
showLoadingModal(context);
final resp = await client.post(
'/pass/wallets/funds',
data: result,
options: Options(
headers: {'X-Noop': true},
),
// Wait for server to handle order
await Future.delayed(
const Duration(seconds: 1),
);
ref.invalidate(walletFundsProvider);
final fund = SnWalletFund.fromJson(
resp.data,
// Return the created fund
final updatedResp = await client.get(
'/pass/wallets/funds/${fund.id}',
);
if (fund.status == 0) {
// Return the fund that was just created (but not yet paid)
if (context.mounted) {
hideLoadingModal(context);
Navigator.of(context).pop(fund);
}
return;
}
final orderResp = await client.post(
'/pass/wallets/funds/${fund.id}/order',
);
final order = SnWalletOrder.fromJson(
orderResp.data,
);
if (context.mounted) {
hideLoadingModal(context);
}
// Show payment overlay to complete the payment
if (!context.mounted) return;
final paidOrder =
await PaymentOverlay.show(
context: context,
order: order,
enableBiometric: true,
final updatedFund =
SnWalletFund.fromJson(
updatedResp.data,
);
if (paidOrder != null &&
context.mounted) {
showLoadingModal(context);
// Wait for server to handle order
await Future.delayed(
const Duration(seconds: 1),
);
ref.invalidate(walletFundsProvider);
// Return the created fund
final updatedResp = await client.get(
'/pass/wallets/funds/${fund.id}',
);
final updatedFund =
SnWalletFund.fromJson(
updatedResp.data,
);
if (context.mounted) {
hideLoadingModal(context);
Navigator.of(
context,
).pop(updatedFund);
}
} else {
isPushing.value = false;
}
} catch (err) {
if (context.mounted) {
hideLoadingModal(context);
Navigator.of(
context,
).pop(updatedFund);
}
errorText.value = err.toString();
} else {
isPushing.value = false;
}
},
} catch (err) {
if (context.mounted) {
hideLoadingModal(context);
}
errorText.value = err.toString();
isPushing.value = false;
}
},
),
),
],

View File

@@ -13,12 +13,11 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
final cloudFileListNotifierProvider =
AsyncNotifierProvider.autoDispose<CloudFileListNotifier, List<SnCloudFile>>(
CloudFileListNotifier.new,
);
final cloudFileListNotifierProvider = AsyncNotifierProvider.autoDispose(
CloudFileListNotifier.new,
);
class CloudFileListNotifier extends AsyncNotifier<List<SnCloudFile>>
class CloudFileListNotifier extends AsyncNotifier<PaginationState<SnCloudFile>>
with AsyncPaginationController<SnCloudFile> {
@override
Future<List<SnCloudFile>> fetch() async {
@@ -83,28 +82,24 @@ class ComposeLinkAttachment extends HookConsumerWidget {
width: 48,
child: switch (itemType) {
'image' => CloudImageWidget(file: item),
'audio' =>
const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' =>
const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ =>
const Icon(
Symbols.body_system,
fill: 1,
).center(),
'audio' => const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' => const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ => const Icon(
Symbols.body_system,
fill: 1,
).center(),
},
),
),
title:
item.name.isEmpty
? Text('untitled').tr().italic()
: Text(item.name),
title: item.name.isEmpty
? Text('untitled').tr().italic()
: Text(item.name),
onTap: () {
Navigator.pop(context, item);
},
@@ -128,9 +123,8 @@ class ComposeLinkAttachment extends HookConsumerWidget {
),
),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(16),
InkWell(

View File

@@ -291,7 +291,9 @@ class ComposeSettingsSheet extends HookConsumerWidget {
),
),
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
items: (postCategories.value ?? <SnPostCategory>[]).map((item) {
items: (postCategories.value?.items ?? <SnPostCategory>[]).map((
item,
) {
return DropdownMenuItem(
value: item,
enabled: false,
@@ -337,7 +339,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
value: currentCategories.isEmpty ? null : currentCategories.last,
onChanged: (_) {},
selectedItemBuilder: (context) {
return (postCategories.value ?? []).map((item) {
return (postCategories.value?.items ?? []).map((item) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(

View File

@@ -6,12 +6,11 @@ import 'package:island/pods/paging.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/paging/pagination_list.dart';
final postAwardListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<PostAwardListNotifier, List<SnPostAward>, String>(
PostAwardListNotifier.new,
);
final postAwardListNotifierProvider = AsyncNotifierProvider.autoDispose.family(
PostAwardListNotifier.new,
);
class PostAwardListNotifier extends AsyncNotifier<List<SnPostAward>>
class PostAwardListNotifier extends AsyncNotifier<PaginationState<SnPostAward>>
with AsyncPaginationController<SnPostAward> {
static const int pageSize = 20;
@@ -52,7 +51,7 @@ class PostAwardHistorySheet extends HookConsumerWidget {
return Column(
children: [
PostAwardItem(award: award),
if (index < (ref.read(provider).value?.length ?? 0) - 1)
if (index < (ref.read(provider).value?.items.length ?? 0) - 1)
const Divider(height: 1),
],
);

View File

@@ -31,12 +31,12 @@ sealed class ReactionListQuery with _$ReactionListQuery {
}) = _ReactionListQuery;
}
final reactionListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<ReactionListNotifier, List<SnPostReaction>, ReactionListQuery>(
ReactionListNotifier.new,
);
final reactionListNotifierProvider = AsyncNotifierProvider.autoDispose.family(
ReactionListNotifier.new,
);
class ReactionListNotifier extends AsyncNotifier<List<SnPostReaction>>
class ReactionListNotifier
extends AsyncNotifier<PaginationState<SnPostReaction>>
with AsyncPaginationController<SnPostReaction> {
static const int pageSize = 20;

View File

@@ -11,7 +11,7 @@ final postRepliesProvider = AsyncNotifierProvider.autoDispose.family(
PostRepliesNotifier.new,
);
class PostRepliesNotifier extends AsyncNotifier<List<SnPost>>
class PostRepliesNotifier extends AsyncNotifier<PaginationState<SnPost>>
with AsyncPaginationController<SnPost> {
static const int pageSize = 20;

View File

@@ -42,9 +42,9 @@ class PostShuffleScreen extends HookConsumerWidget {
kBottomControlHeight + MediaQuery.of(context).padding.bottom,
),
child: Builder(
key: ValueKey(postListState.value?.length ?? 0),
key: ValueKey(postListState.value?.items.length ?? 0),
builder: (context) {
final items = postListState.value ?? [];
final items = postListState.value?.items ?? [];
if (items.isNotEmpty) {
return CardSwiper(
controller: cardSwiperController,

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
@@ -8,15 +10,30 @@ import 'package:island/widgets/realm/realm_list_tile.dart';
import 'package:styled_widget/styled_widget.dart';
final realmListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<RealmListNotifier, List<SnRealm>, String?>(RealmListNotifier.new);
.family<RealmListNotifier, PaginationState<SnRealm>, String?>(
RealmListNotifier.new,
);
class RealmListNotifier extends AsyncNotifier<List<SnRealm>>
class RealmListNotifier extends AsyncNotifier<PaginationState<SnRealm>>
with AsyncPaginationController<SnRealm> {
String? arg;
RealmListNotifier(this.arg);
static const int _pageSize = 20;
@override
FutureOr<PaginationState<SnRealm>> build() async {
final items = await fetch();
return PaginationState(
items: items,
isLoading: false,
isReloading: false,
totalCount: totalCount,
hasMore: hasMore,
cursor: cursor,
);
}
@override
Future<List<SnRealm>> fetch() async {
final client = ref.read(apiClientProvider);

View File

@@ -7,13 +7,12 @@ import 'package:island/services/time.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/paging/pagination_list.dart';
final thoughtSequenceListNotifierProvider = AsyncNotifierProvider.autoDispose<
ThoughtSequenceListNotifier,
List<SnThinkingSequence>
>(ThoughtSequenceListNotifier.new);
final thoughtSequenceListNotifierProvider = AsyncNotifierProvider.autoDispose(
ThoughtSequenceListNotifier.new,
);
class ThoughtSequenceListNotifier
extends AsyncNotifier<List<SnThinkingSequence>>
extends AsyncNotifier<PaginationState<SnThinkingSequence>>
with AsyncPaginationController<SnThinkingSequence> {
static const int pageSize = 20;