Skip to content

ApkPy 1.2.1 — Production Feeds

VERSION 1.2.1 / RELEASE CANDIDATE

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.

feeds / 1.2.1verified
01observeviewport
02guardone request
03fetchapp cursor
04appendrange insert
05continuesame offset
Small update, narrow contract No Paging 3, mandatory backend or hidden cursor ownership.

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,
)
on_end_reachedOne guarded page request

Runs when the visible range enters the configured prefetch window.

append_itemsKeep rows and position

Adds the new page instead of replacing the complete collection.

on_refreshStart a new generation

Pull from the top or call refresh() with the same callback.

has_moreFinish explicitly

Close the end path with False; refresh may open it again.

Feed pattern explorer

INSTAGRAM / X / REDDIT

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.

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() or finish_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.