perf(market): paginate Nostr offer discovery, infinite scroll, memory cap
Bound market memory and let large areas page instead of loading everything: - DiscoveryQuery gains an until cursor; OfferTransport gains discoverPage() (one-shot, EOSE-bounded, newest-first) alongside the existing live discover() stream, which now accepts since so it only carries NEW offers going forward. - NostrOfferTransport.discoverPage sorts by created_at desc and derives the next cursor from the oldest event seen (not the oldest type-matched one, so filtering never skips a page). - OffersCubit: discover() fetches the first page + opens a since=now live sub; loadNextPage() pages further back; the in-memory list is capped at 400 offers (each can carry a ~40KB inline photo thumbnail, so unbounded growth in a busy area was a real OOM risk). - market_screen: infinite scroll via a scroll-position trigger + footer spinner, instead of a single unbounded ListView. - Added discoverPage unit tests (commons_core) and cubit pagination tests (first page/cursor, accumulation, cap, cross-page dedup).
This commit is contained in:
parent
3de01bd948
commit
f17ae7751c
9 changed files with 513 additions and 43 deletions
|
|
@ -414,23 +414,44 @@ class MarketBody extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: visible.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final o = visible[i];
|
||||
final mine = o.authorPubkeyHex == selfPubkey;
|
||||
return _OfferCard(
|
||||
offer: o,
|
||||
mine: mine,
|
||||
onTap: () async {
|
||||
await context.push('/market/offer', extra: o);
|
||||
await onOfferClosed?.call();
|
||||
},
|
||||
);
|
||||
: NotificationListener<ScrollNotification>(
|
||||
// Page in older offers as the list nears its end, so a
|
||||
// large area streams in on demand rather than all at
|
||||
// once. The cubit guards against overlapping loads.
|
||||
onNotification: (n) {
|
||||
if (state.canLoadMore &&
|
||||
!state.loadingMore &&
|
||||
n.metrics.axis == Axis.vertical &&
|
||||
n.metrics.extentAfter < 500) {
|
||||
cubit.loadNextPage();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
// A trailing spinner row while the next page loads.
|
||||
itemCount: visible.length + (state.loadingMore ? 1 : 0),
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
if (i >= visible.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final o = visible[i];
|
||||
final mine = o.authorPubkeyHex == selfPubkey;
|
||||
return _OfferCard(
|
||||
offer: o,
|
||||
mine: mine,
|
||||
onTap: () async {
|
||||
await context.push('/market/offer', extra: o);
|
||||
await onOfferClosed?.call();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue