Skip to content

Feeds and reactive state API

virtual_collection is the list for many rows: a feed, a catalogue, a message history. On Android it is a RecyclerView, so only the rows on the screen exist and they are reused as you scroll; ten thousand items cost about the same as twenty. state and lifecycle are the two small pieces most feeds need around it.

virtual_collection

virtual_collection(
    items,
    template={...},
    layout="list",
    columns=2,
    item_height=None,
    buffer=2,
    on_end_reached=None,
    on_refresh=None,
    prefetch=4,
    screen=home,
)
Argument Default Meaning
items [] the rows, a list of dictionaries
template which item field fills each slot: title, subtitle, image, meta and badge
row a function that builds each row from components, instead of template
on_click called with the item of the tapped row
layout "list" "list" or "grid"
columns 2 the number of columns of a grid
item_height 88 for a list, 224 for a grid one height for every row (at least 56), or "auto" so that each row takes what it needs
buffer 2 rows kept ready just outside the screen, from 1 to 8
on_end_reached asks for the next page when the end comes near
prefetch 4 how many rows before the end on_end_reached runs, from 0 to 100
on_refresh turns on pull to refresh

A template fills each slot from the item's fields with {field}, and a slot can mix fields and text:

tracks = virtual_collection(
    songs,
    template={
        "image": "{cover}",
        "title": "{name}",
        "subtitle": "{artist} ยท {album}",
        "meta": "{duration}",
    },
    on_click=open_track,
    screen=home,
)

Rows built with row= take the height of what they hold. Rows that swipe and rows that can be dragged into a new order (swipe_right, swipe_left, reorder and on_reorder) are explained in Components and layouts.

Methods

Method Result
set_items(items, has_more=True) replace data and finish refresh
append_items(items, has_more=True) add a page without resetting position
prepend_items(items) add records before the visible anchor
update_item(id, changes, key="id", optimistic=False) patch one keyed row
remove_item(id, key="id", optimistic=False) remove one keyed row
merge_items(items, key="id") update matches and append new keys
commit(mutation_id=None) accept an optimistic snapshot
rollback(mutation_id=None) restore a snapshot
finish_load(has_more=True) release the paging latch
refresh() run the guarded refresh callback

Paging. on_end_reached runs once and then waits. It runs again only after append_items() or finish_load(), so a slow network cannot ask for the same page five times. has_more=False tells the list there is nothing left to load.

Keys. update_item, remove_item and merge_items find rows by a field, "id" unless key= names another. The row changes in place, and the list keeps its position.

Optimistic changes. With optimistic=, a change shows at once and can be undone if the server says no. optimistic=True names the change after the row's id; a string gives it a name of your own.

def like_post(post_id):
    mutation = "like-" + post_id
    feed.update_item(post_id, {"liked": True}, optimistic=mutation)
    https.post(
        "https://api.example.com/posts/" + post_id + "/like",
        on_response=lambda success, body: settle(success, mutation),
    )


def settle(success, mutation):
    if success:
        feed.commit(mutation)
    else:
        feed.rollback(mutation)

state

state(initial, id=None)

A value that updates the components bound to it whenever it changes.

Method Result
get() the current value
set(value) a new value; every binding updates
increment(amount=1), decrement(amount=1) add or take away
toggle() True becomes False and back
bind(component, template="{value}") writes the value into the component, through the template
bind_visibility(component, when=True) shows the component while the value is when, and hides it otherwise
basket = state(0)
empty = state(True)
total = label("", id="total", screen=shop)
hint = label("Your basket is empty", id="hint", screen=shop)

basket.bind(total, "{value} items in the basket")
empty.bind_visibility(hint)


def add_one():
    basket.increment()
    empty.set(False)


button("Add", command=add_one, screen=shop)

Give bind_visibility a state that holds True or False.

lifecycle

lifecycle(screen, on_mount=None, on_resume=None, on_pause=None, on_destroy=None)
Event When it runs
on_mount once, when the screen is built and its components exist
on_resume each time the screen comes to the front, the first time included
on_pause each time it leaves the front: another screen opens, or the app goes to the background
on_destroy once, when the screen is closed

Each one takes a function by its name. Load the first page in on_mount, open connections in on_resume, stop what costs battery in on_pause, and close connections in on_destroy. On Android they are the Activity's own onCreate, onResume, onPause and onDestroy.

See API-backed feed and Production Feeds.