Skip to main content

flutter-it-feed-datasource

Expert guidance on implementing paginated feeds and infinite scroll in Flutter using FeedDataSource and PagedFeedDataSource patterns. Covers base feed data source, cursor-based pagination, auto-pagination at length-3, proxy lifecycle with reference counting, feed widget implementation, filtered feeds, event bus integration, and creation with createOnce. Use when building paginated lists, infinite scroll, feed views, or managing proxy lifecycle in feeds.

소스 정보

저장소
flutter-it/flutter_it
최근 소스 활동
2026년 9월 19일 20:53
감지된 SKILL.md 언어
영어
스타
20
포크
1

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
flutter-it-feed-datasource
description
Expert guidance on implementing paginated feeds and infinite scroll in Flutter using FeedDataSource and PagedFeedDataSource patterns. Covers base feed data source, cursor-based pagination, auto-pagination at length-3, proxy lifecycle with reference counting, feed widget implementation, filtered feeds, event bus integration, and creation with createOnce. Use when building paginated lists, infinite scroll, feed views, or managing proxy lifecycle in feeds.
metadata
{"author":"flutter-it","version":"1.0"}
# Feed DataSource Expert - Paged Lists & Infinite Scroll **What**: Pattern for paginated, reactive list/feed widgets using ValueNotifiers and Commands. Integrates with proxy pattern for entity lifecycle management. ## CRITICAL RULES - Auto-pagination triggers at `items.length - 3` (not at the last item) - `updateDataCommand` for initial/refresh loads, `requestNextPageCommand` for pagination - separate commands - When refreshing with proxies: release OLD proxies AFTER replacing with new ones (delay release for animations) - `itemCount` is a ValueNotifier - watch it to rebuild the list widget - Feed data sources are typically created with `createOnce` in widgets, NOT registered in get_it - `getItemAtIndex(index)` both returns the item AND triggers auto-pagination ## Base FeedDataSource Non-paged feed for finite data sets: ```dart abstract class FeedDataSource<TItem> { FeedDataSource({List<TItem>? initialItems}) : items = initialItems ?? []; final List<TItem> items; final _itemCount = CustomValueNotifier<int>(0); ValueListenable<int> get itemCount => _itemCount; bool updateWasCalled = false; late final updateDataCommand = Command.createAsyncNoParamNoResult( () async { await updateFeedData(); updateWasCalled = true; refreshItemCount(); }, errorFilter: const LocalOnlyErrorFilter(), ); ValueListenable<bool> get isFetchingNextPage => updateDataCommand.isRunning; ValueListenable<CommandError?> get commandErrors => updateDataCommand.errors; /// Subclasses implement - fetch data and populate items list Future<void> updateFeedData(); /// Subclasses implement - compare items for deduplication bool itemsAreEqual(TItem item1, TItem item2); TItem getItemAtIndex(int index) { assert(index >= 0 && index < items.length); return items[index]; } void refreshItemCount() { _itemCount.value = items.length; } void addItemAtStart(TItem item) { items.insert(0, item); refreshItemCount(); } void removeObject(TItem itemToRemove) { items.removeWhere((item) => itemsAreEqual(item, itemToRemove)); refreshItemCount(); } void reset() { items.clear(); updateWasCalled = false; refreshItemCount(); } void dispose() { _itemCount.dispose(); } } ``` ## PagedFeedDataSource Extends FeedDataSource with cursor-based pagination: ```dart abstract class PagedFeedDataSource<TItem> extends FeedDataSource<TItem> { String? nextPageUrl; bool? datasetExpired; bool get hasNextPage => nextPageUrl != null && datasetExpired != true; late final requestNextPageCommand = Command.createAsyncNoParamNoResult( () async { await requestNextPage(); refreshItemCount(); }, errorFilter: const LocalOnlyErrorFilter(), ); /// Subclasses implement - fetch next page and append to items Future<void> requestNextPage(); /// Call after parsing API response to store next page URL void extractNextPageParams(String? url) { nextPageUrl = url; } /// Auto-pagination: triggers when scrolling near the end @override TItem getItemAtIndex(int index) { if (index >= items.length - 3 && commandErrors.value == null && hasNextPage && !requestNextPageCommand.isRunning.value) { requestNextPageCommand.run(); } return super.getItemAtIndex(index); } // Merged loading/error state from both commands late final ValueNotifier<bool> _isFetchingNextPage = ValueNotifier(false); @override ValueListenable<bool> get isFetchingNextPage => _isFetchingNextPage; // Listen to both commands and merge their isRunning states // _isFetchingNextPage.value = updateDataCommand.isRunning.value || // requestNextPageCommand.isRunning.value; @override void reset() { nextPageUrl = null; datasetExpired = null; super.reset(); } } ``` ## Concrete Implementation with Proxies ```dart class PostsFeedSource extends PagedFeedDataSource<PostProxy> { PostsFeedSource(this.feedType); final PostFeedType feedType; @override bool itemsAreEqual(PostProxy a, PostProxy b) => a.id == b.id; @override Future<void> updateFeedData() async { final api = PostApi(di<ApiClient>()); final response = await api.getPosts(type: feedType); if (response == null) return; // Release old proxies (delay for exit animations) final oldItems = List<PostProxy>.from(items); items.clear(); // Create new proxies via manager (increments ref count) final proxies = di<PostsManager>().createProxies(response.data); items.addAll(proxies); extractNextPageParams(response.links?.next); // Release old proxies after animations complete Future.delayed(const Duration(milliseconds: 1000), () { di<PostsManager>().releaseProxies(oldItems); }); } @override Future<void> requestNextPage() async { if (nextPageUrl == null) return; final response = await callNextPageWithUrl<PostListResponse>(nextPageUrl!); if (response == null) return; final proxies = di<PostsManager>().createProxies(response.data); items.addAll(proxies); extractNextPageParams(response.links?.next); } // Override to manage reference counting on individual operations @override void addItemAtStart(PostProxy item) { item.incrementReferenceCount(); super.addItemAtStart(item); } @override void removeObject(PostProxy item) { super.removeObject(item); di<PostsManager>().releaseProxy(item); } } ``` ## Feed Widget ```dart class FeedView<TItem> extends WatchingWidget { const FeedView({ required this.feedSource, required this.itemBuilder, this.emptyListWidget, }); final FeedDataSource<TItem> feedSource; final Widget Function(BuildContext, TItem) itemBuilder; final Widget? emptyListWidget; @override Widget build(BuildContext context) { final itemCount = watch(feedSource.itemCount).value; final isFetching = watch(feedSource.isFetchingNextPage).value; // Trigger initial load callOnce((_) => feedSource.updateDataCommand.run()); // Error handler registerHandler( target: feedSource.commandErrors, handler: (context, error, _) { showErrorSnackbar(context, error.error); }, ); // Error state with retry if (feedSource.commandErrors.value != null && itemCount == 0) { return ErrorWidget( onRetry: () => feedSource.updateDataCommand.run(), ); } // Initial loading if (!feedSource.updateWasCalled && isFetching) { return Center(child: CircularProgressIndicator()); } // Empty state if (itemCount == 0 && feedSource.updateWasCalled) { return emptyListWidget ?? Text('No items'); } // List with pull-to-refresh return RefreshIndicator( onRefresh: () => feedSource.updateDataCommand.runAsync(), child: ListView.builder( itemCount: itemCount + (isFetching ? 1 : 0), itemBuilder: (context, index) { if (index >= itemCount) { return Center(child: CircularProgressIndicator()); } // getItemAtIndex auto-triggers pagination near end final item = feedSource.getItemAtIndex(index); return itemBuilder(context, item); }, ), ); } } ``` ## Creation Pattern ```dart // Create with createOnce in the widget that owns the feed class PostsFeedPage extends WatchingWidget { @override Widget build(BuildContext context) { final feedSource = createOnce( () => PostsFeedSource(PostFeedType.latest), dispose: (source) => source.dispose(), ); return FeedView<PostProxy>( feedSource: feedSource, itemBuilder: (context, post) => PostCard(post: post), emptyListWidget: Text('No posts yet'), ); } } ``` ## Filtered Feeds Same data, different views via filter functions: ```dart class ChatsListSource extends PagedFeedDataSource<ChatProxy> { ChatFilterType _filter = ChatFilterType.ALL; String _query = ''; void setTypeFilter(ChatFilterType filter) { _filter = filter; updateDataCommand.run(); // Re-fetch with new filter } void setSearchQuery(String query) { _query = query; updateDataCommand.run(); } } ``` ## Event Bus Integration Feeds can react to events from other parts of the app: ```dart // In FeedDataSource constructor di<EventBus>().on<FeedEvent>().listen((event) { if (event.feedsToApply.contains(feedId)) { switch (event.action) { case FeedEventActions.update: updateDataCommand.run(); case FeedEventActions.addItem: addItemAtStart(event.data as TItem); case FeedEventActions.removeItem: removeObject(event.data as TItem); } } }); // Trigger from anywhere in the app di<EventBus>().fire(FeedEvent( action: FeedEventActions.addItem, data: newPostProxy, feedsToApply: [FeedIds.latestPostsFeed, FeedIds.followingPostsFeed], )); ``` ## Anti-Patterns ```dart // ❌ Releasing proxies immediately on refresh (breaks exit animations) items.clear(); di<Manager>().releaseProxies(oldItems); // Widgets still animating! items.addAll(newProxies); // ✅ Delay release for animations final oldItems = List.from(items); items.clear(); items.addAll(newProxies); Future.delayed(Duration(milliseconds: 1000), () { di<Manager>().releaseProxies(oldItems); }); // ❌ Registering feed in get_it as singleton di.registerSingleton<PostsFeed>(PostsFeedSource()); // ✅ Create with createOnce in the widget that owns it final feed = createOnce(() => PostsFeedSource()); // ❌ Manually checking scroll position for pagination scrollController.addListener(() { if (scrollController.position.pixels >= ...) loadMore(); }); // ✅ Auto-pagination via getItemAtIndex triggers at length - 3 // ❌ Single command for both initial load and pagination // ✅ Separate commands: updateDataCommand + requestNextPageCommand // Allows independent loading/error states and restrictions ```
GitHub에서 보기