ApkPy 1.2.1 — Production Feeds¶
Long feeds should feel uneventful.
Production Feeds gives virtual_collection() guarded pagination, prefetching and pull-to-refresh. Pages arrive without moving the reader, duplicate scroll events do not duplicate requests and the application keeps ownership of its backend cursor.
What changed¶
virtual_collection() can now coordinate remote pages without becoming a data
framework:
feed = virtual_collection(
[],
template={
"image": "{avatar}",
"title": "{author}",
"subtitle": "{message}",
"meta": "{time}",
},
on_end_reached=load_more,
on_refresh=reload_feed,
prefetch=4,
screen=home,
)
Runs when the visible range enters the configured prefetch window.
Adds the new page instead of replacing the complete collection.
Pull from the top or call refresh() with the same callback.
Close the end path with False; refresh may open it again.
Feed pattern explorer¶
Prefetch before the reader reaches the final post.
Append the next cursor page and leave every visible row exactly where it was.
- Template
- Avatar, author, body and timestamp.
- Finish
- Pass has_more=False when the API returns no cursor.
The same loading contract works in a grid.
Two-column cards keep their image, price, click callback and full source record while pages are inserted.
- Layout
- layout="grid" with application-selected columns.
- Insertion
- Only the appended native adapter range is notified.
A bounded request protects older history.
Scroll gestures can produce many native events. The loading latch admits one callback until application code completes it.
- Success
- append_items(page) releases the latch.
- Empty
- finish_load(has_more=False) closes the history.
Keep the current page and make retry explicit.
A failed request does not need a fake item or a rebuilt list. Release the latch and let the next gesture or retry button try again.
- Error
- finish_load(has_more=True)
- Retry
- The next boundary event is accepted once.
Complete example¶
from apkpy_lib import (
Screen, https, json_get, storage, toast, virtual_collection,
)
home = Screen(id="home", scroll=False)
storage.set("feed_cursor", "first")
def open_post(item):
toast(item["author"])
def page_loaded(success, body):
if not success:
status.set_value("Could not load this page. Scroll to retry.")
feed.finish_load(has_more=True)
return
next_cursor = json_get(body, "next_cursor")
storage.set("feed_cursor", next_cursor)
feed.append_items(
json_get(body, "items"),
has_more=next_cursor != "",
)
def load_more():
cursor = storage.get("feed_cursor", "first")
https.get(
"https://api.example.com/feed?cursor=" + cursor,
on_response=page_loaded,
)
def refresh_loaded(success, body):
if not success:
feed.finish_load()
return
next_cursor = json_get(body, "next_cursor")
storage.set("feed_cursor", next_cursor)
feed.set_items(
json_get(body, "items"),
has_more=next_cursor != "",
)
def reload_feed():
https.get("https://api.example.com/feed", on_response=refresh_loaded)
feed = virtual_collection(
[],
template={
"image": "{avatar}",
"title": "{author}",
"subtitle": "{message}",
"meta": "{time}",
},
on_click=open_post,
on_end_reached=load_more,
on_refresh=reload_feed,
prefetch=4,
screen=home,
)
Retry without rebuilding
Use feed.finish_load(has_more=True) after a recoverable error. Existing
rows stay visible and the next end event may retry. Use
has_more=False only when the server has confirmed the end.
Method contract¶
| Call | Data | Loading state | has_more |
|---|---|---|---|
append_items(page, has_more=True) |
Appends page |
Finished | Uses argument |
set_items(first_page, has_more=True) |
Replaces all items | Finished, including refresh | Uses argument |
finish_load(has_more=True) |
Unchanged | Finished | Uses argument |
refresh() |
Unchanged until callback finishes | Starts guarded refresh | Temporarily reopened |
Both append_items() and set_items() accept a Python list or a JSON-array
string. This lets an HTTP callback pass json_get(body, "items") directly.
Generated Android¶
The generated Activity uses ordinary RecyclerView APIs:
production_feed.addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override public void onScrolled(
RecyclerView recyclerView, int dx, int dy
) {
_apkpyMaybeLoadProduction_FeedPage();
}
}
);
The guard runs before application code:
if (production_feedLoading || !production_feedHasMore) return;
production_feedLoading = true;
pythonCallback_load_more();
Appending a page updates only the inserted range:
int oldSize = production_feed_items.size();
production_feed_items.addAll(page);
production_feed_adapter.notifyItemRangeInserted(oldSize, page.size());
When on_refresh exists, the RecyclerView is wrapped by
SwipeRefreshLayout. Without on_refresh, neither the wrapper nor the
dependency is generated.
Previewer behaviour¶
- Near-end requests use the same prefetch threshold.
- Append preserves the current list/grid scroll offset.
- The loading state is compact and does not replace the records.
- A pull at the top runs
on_refresh;feed.refresh()calls the same path. - Duplicate requests are ignored until the app calls
append_items(),set_items()orfinish_load(). - Templates, click callbacks and complete JSON records remain intact.
The repository demonstration has two screens: an English social timeline and a two-column object catalogue with a deliberate first failure, retry, refresh and no-more-results state.
What 1.2.1 does not own¶
ApkPy does not choose a server cursor, invent page URLs or persist a remote dataset automatically. This release does not include Paging 3, offline sync, automatic cache invalidation or item-level diffing. The current application dataset remains in memory.
That boundary is intentional: Production Feeds makes the UI path reliable without forcing every application into one backend architecture.