Skip to content

Essential API reference

This page is the fastest route from an API name to working code. It documents the supported public surface exported by apkpy_lib; generated Java helper classes are implementation details.

For the 1.9.0 NFC API, use the exact device signatures and complete NFC examples. It is not available in published 1.8.0; callbacks take (ok, value), and writes replace existing tag content.

The same 1.9.0 adds Contacts: choose a phone/email without broad access, list/get with read permission, or open a native create/edit form. Exact signatures and People Desk source include callback contracts and errors. Editor return does not confirm a saved contact.

Conventions

from apkpy_lib import Screen, Theme, button, label, run

home = Screen(id="home", scroll=True)
label("Hello", id="title", screen=home)
button("Continue", id="continue", command=lambda: None, screen=home)

style = """
home { padding: 24px; gap: 12px; }
title { font-size: 28px; font-weight: bold; }
continue { border-radius: 14px; }
"""

run(start_screen=home, theme=Theme(mode="dark"))
  • Pass screen= to attach a top-level component to a screen.
  • Pass parent= to put a component inside a container or card.
  • An id is both the stable component name and its CSS-like selector.
  • Network, uploads and typed database work run outside the UI thread. Their callbacks return to the Previewer/Android UI thread.
  • The Previewer exercises layout and application flow. Permissions, Firebase, codecs, background restrictions and GPS still need an Android test.

App, screens and navigation

API Signature Returns
Screen Screen(id, background_image=None, scroll=False) screen definition
run run(start_screen=None, theme=None) starts the app/Previewer
bottom_nav bottom_nav(screens, labels=None, icons=None) BottomNav
on_click_navigate on_click_navigate(screen, data=None) callback suitable for command=
app_bar app_bar(title, leading=None, actions=None, id=None, screen=None) app-bar definition
sliver_app_bar sliver_app_bar(title, image, expanded_height=240, pinned=True, leading=None, actions=None, id=None, screen=None) collapsible app bar
action action(icon, command=None, label=None, id=None) app-bar action
back back() closes this screen; the one that opened it comes back as it was. command=back works as it is (1.12.0)

Navigation data is read on the destination screen:

details = Screen(id="details", scroll=True)
button(
    "Open note",
    command=on_click_navigate(details, {"note_id": 42}),
    screen=home,
)

selected_id = details.get_param("note_id", 0)

Components

API Exact public signature
label label(text, id=None, screen=None, parent=None, variant=None)
button button(text, id=None, command=None, screen=None, parent=None, variant=None, icon=None, describe=None)
inputs inputs(placeholder="", id=None, type="text", screen=None, parent=None, on_change=None, help=None, error=None, required=False)
image image(src, id=None, screen=None, parent=None, *, placeholder=None, fallback=None, cache=True, fade_in=False, blur=0, tint=None, aspect_ratio=None, describe=None, command=None, zoom=False)
video video(src, id=None, screen=None, parent=None, *, poster=None, autoplay=False, controls=True, loop=False, muted=False, preload=True, aspect_ratio="16:9", fit="contain", on_ready=None, on_progress=None, on_end=None, on_error=None)
avatar avatar(src, size=48, status=None, id=None, screen=None, parent=None, *, placeholder=None, fallback=None, cache=True, fade_in=True, blur=0, tint=None)
container container(id=None, screen=None, parent=None)
card card(title=None, subtitle=None, image=None, content=None, actions=None, id=None, variant="elevated", screen=None, parent=None)
list_view list_view(items=None, id=None, screen=None, parent=None, on_click=None, rich=False)
chips chips(options, selected=None, multiple=False, on_change=None, icons=None, check=True, required=False, id=None, screen=None, parent=None)
segmented segmented(options, selected=None, multiple=False, on_change=None, icons=None, check=True, required=False, id=None, screen=None, parent=None)
chart chart(items=None, kind="bar", x="label", y="value", id=None, screen=None, parent=None, on_click=None, fill=False, value=0, max=100, center=None)
pager pager(id=None, screen=None, parent=None)
tab tab(text="", icon=None, id=None, parent=None, describe=None)
view_image view_image(src, describe=None)
validate validate(*fields)

Common returned-component methods are get_value(), set_value(value), show() and hide(). Images expose set_src(). Videos expose play(), pause(), stop(), seek(seconds), set_source(), set_speed() and set_muted().

Input types include text, password, search, number, date, time, textarea, select, switch, checkbox, radio and range. Options and range values are passed through set_items() or set_value() as shown in the component guides.

The rows from chips to validate are new in 1.12.0, as are help=, error=, required= and zoom=. Their contracts:

API Contract
on_change(value) on inputs the text of a text field; "true"/"false" from a switch or a checkbox; a slider's number as text. Not called for the value the screen opens with
validate(a, b) / field.validate() shows the message of every empty required field at once, focuses the first, returns True when all passed
field.set_error(text) shows your own message under a text field; set_error("") clears it, and so does typing
chips / segmented on_change(value) the selected word ("" when a chip is cleared), or a list with multiple=True. get_value() gives the same; set_value() does not call it
chart on_click(item) the item of the bar, point or slice that was tapped. set_items(items) redraws; set_value(v) moves a ring
pager.select(index) slides to that tab, counting from 0
image(..., zoom=True) a tap opens the picture full screen with pinch zoom; it takes the place of command=

Complete examples: chips, fields, tabs, charts and the picture viewer.

Layout

actions = container(id="actions", screen=home)
save = button("Save", parent=actions)
cancel = button("Cancel", variant="outlined", parent=actions)

responsive(
    mobile=column(save, cancel),
    tablet=row(save, cancel),
    breakpoint=600,
    parent=actions,
)
API Purpose
row(*children) horizontal composition
column(*children) vertical composition
responsive(mobile, tablet=None, landscape=None, breakpoint=600, ...) switches layout by viewport

Virtual collections and live state

feed = virtual_collection(
    [],
    template={
        "title": "{author}",
        "subtitle": "{message}",
        "meta": "{time}",
        "image": "{avatar}",
    },
    on_end_reached=load_more,
    on_refresh=reload,
    prefetch=4,
    screen=home,
)
Method Contract
set_items(items, title=None, subtitle=None, image=None, has_more=True) replace the dataset and finish refresh; legacy rich-row keys remain supported
append_items(items, has_more=True) append a page without resetting position
prepend_items(items) insert above 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 one optimistic snapshot
finish_load(has_more=True) release a failed/empty load latch
refresh() start the guarded refresh callback
scroll_to_end() bring the newest row into view
scroll_to_top() bring the first row into view
scroll_to_item(id, key="id") bring one keyed row into view

A row can be built from components instead of a template -- row=post_row, see Rows built from components -- and since 1.12.0 a list's rows can be swiped and reordered:

Argument Contract
swipe_right= / swipe_left= a swipe(icon, text, on_swipe=None, color=None, keep=False, undo=None). Past 40% of the row's width the row leaves the list and on_swipe(item) runs; undo="message" offers Undo first and only then calls it; keep=True slides the row back
reorder=True a long press lifts a row and a drop moves it
on_reorder= on_reorder(item, index) after a drop, index a number from 0; implies reorder=True
layout="grid" with height: auto in the stylesheet the grid is as tall as its items and scrolls with its screen

Swipes and reordering are for layout="list"; on a grid the build stops with U2042. See Rows that swipe and move.

Rows that take the height they need

item_height= accepts a number — every row that height, which is what a feed of uniform cards wants — or "auto", where each row wraps its own content:

thread = virtual_collection(
    turns,
    template={"title": "{author}", "markdown": "{message}"},
    item_height="auto",
    screen=chat,
)

A conversation needs "auto": one fixed height gives "yes" the same space as a twenty-line answer, so one floats in a void and the other is cut off mid-sentence. With "auto" the text also stops being clipped to one line unless title-lines / subtitle-lines say otherwise.

Looking at the message that just arrived

Adding a row does not move the viewport, which is right for a feed and wrong for a conversation: you send a question and end up looking at your own question while the answer grows below the fold.

thread.merge_items([{"id": reply_id, "author": "Ora", "message": ""}])
thread.scroll_to_end()

The scroll is animated over the duration the theme's motion preset gives the nav moment, and motion="none" makes it a jump. scroll_to_item(id) takes the same key= the mutations take.

Two honest limits. It moves once, when you call it — it does not follow text that keeps arriving, so a long streamed answer still grows past the bottom edge. And a very long jump on Android takes longer than the stated duration, because the RecyclerView re-aims as it goes and cannot know a row's height before laying it out.

The avatar slot

A template slot named avatar draws a circle with up to two initials taken from its value, over a colour the value itself picks:

template={"avatar": "{author}", "title": "{author}", "markdown": "{message}"}

The same name lands on the same colour on the phone and in the Previewer — one palette, read directly by one and written into the generated Java by the other. An empty value hides the circle instead of leaving a coloured hole.

It is a mark for a name, not a picture: for a photo or a remote image, use the image slot.

The markdown slot

A template slot named markdown renders its value as Markdown instead of plain text — headings, emphasis, links, lists, quotes and fenced code blocks. It is the same renderer the markdown() component uses, so a code block looks the same in a row as it does on a page.

Put an assistant's answer here rather than in subtitle: read as plain text with backticks in it, an answer with code in it is not an answer.

Add code-copy: button; to the collection's stylesheet — or to a markdown() component's — and every fenced block gets a tappable Copy under it that puts exactly that block on the clipboard. A block of code you cannot copy is a block of code you retype by hand.

All of these are opt-in. A collection that asks for none of them generates the same Java and XML it always did, and never carries the Markdown renderer into the APK.

state(initial, id=None) returns a reactive value with get, set, increment, decrement, toggle, bind and bind_visibility.

count = state(0, id="cart_count")
badge = label("0 items", screen=home)
count.bind(badge, template="{value} items")
button("Add", command=lambda: count.increment(), screen=home)

Use lifecycle(screen, on_mount=None, on_resume=None, on_pause=None, on_destroy=None) to start and stop screen-owned work.

Typed SQLite Data Core

notes = db.model(
    "notes",
    fields={
        "id": db.integer(primary_key=True, auto_increment=True),
        "title": db.text(required=True, max_length=120),
        "favorite": db.boolean(default=False),
        "updated_at": db.datetime(default=db.now()),
    },
    indexes=[db.index("idx_notes_updated", ["updated_at"])],
)

db.schema("notes_app", version=1, models=[notes])
Area Public API
Fields integer, real, text, boolean, datetime, json, blob, now
Model model(name, fields, indexes=None) and index(name, fields, unique=False)
CRUD insert, insert_many, get, find, update, delete, count
Filters eq, ne, gt, gte, lt, lte, contains, starts_with, ends_with, in_, is_null, and_, or_
Order asc(name), desc(name)
Schema schema(name, version, models, migrations=None)
Migration migration, create_table, add_column, rename_column, create_index, rename_index, drop_index, rename_table, sql

CRUD callbacks:

Call on_result receives
insert new row ID
insert_many inserted row count
get JSON object or empty value
find iterable JsonRows
update / delete affected row count
count integer count

Every operation accepts on_error(message). Use parameterized filters and values; do not build SQL strings with user input.

Reactive Data

folder_notes = db.relation(
    "folder_notes",
    parent=folders,
    child=notes,
    foreign_key="folder_id",
    parent_as="folder",
    children_as="notes",
    on_delete="cascade",
)

db.schema(
    "knowledge_vault_live",
    version=1,
    models=[folders, notes],
    relations=[folder_notes],
)

live_notes = notes.observe(
    filters=[db.eq("folder_id", active_folder_id)],
    include=["folder"],
    screen=notes_screen,
    on_change=lambda rows: notes_feed.set_items(rows),
)
Area Public API
Relation db.relation(name, parent, child, foreign_key, parent_as, children_as, on_delete)
Eager read get(..., include=None) and find(..., include=None)
Observer model.observe(..., screen, on_change, on_error=None)
Observer control refresh(), update_query(...), close()

on_delete accepts restrict, cascade or set_null. Includes are limited to one level and are loaded in batches. Observers pause and resume with their screen, coalesce rapid invalidations and deliver callbacks on the UI thread. See Reactive Data for lifecycle and migration rules.

HTTP and JSON

def received(success, body):
    if success:
        title.set_value(json_get(body, "title"))
    else:
        snackbar("Request failed")

https.get("https://api.example.com/note/42", on_response=received)

https.get, post, put, patch and delete call on_response(success, body). json_get(json_text, "items.0.title") safely reads a dotted path.

Pass a dict as data= and it goes out as JSON with its types intact — a number stays a number, and the text a user typed is escaped by the serialiser rather than by you:

https.post(
    "https://api.example.com/messages",
    data={
        "model": "some-model",
        "max_tokens": 1024,
        "messages": [{"role": "user", "content": question.get_value()}],
    },
    headers={"x-api-key": storage.get("api_key", "")},
    timeout=120,
    on_response=answered,
)

Content-Type: application/json is set for you when the body starts with { or [ and you did not choose a header yourself. Pass a string as data= to send anything else — form-encoded bodies, XML — exactly as written.

timeout= is in seconds and applies to waiting for the response; the default is 60 and the ceiling is 600. Raise it when the other end thinks before it answers, as a language model does.

WebSocket

websocket.connect(
    "room",
    "wss://example.com/live",
    headers={"Authorization": "Bearer " + token},
    on_open=lambda: status.set_value("Live"),
    on_message=lambda message: messages.prepend_items([{"text": message}]),
    on_error=lambda message: status.set_value("Offline · " + message),
    reconnect=True,
)

websocket.send("room", "hello")
websocket.close("room")

connect() also accepts protocols, on_close, reconnect_delay, max_reconnect_delay and ping_interval. Sends made during the handshake are queued with a bounded limit.

Storage and cryptography

Object Methods
storage set, get, delete, clear, keys
crypto hash_password, verify_password, encrypt, decrypt
files download, path, exists, delete

Encrypted values are tied to the app/device key. Copying only the ciphertext to another installation is not a backup strategy. Never ship API secrets in client code.

Overlays and feedback

API Callback shape
bottom_sheet(title="", content="", items=None, on_select=None, close_text="Close", id=None, screen=None, on_close=None) on_select(item); on_close() whichever way it closed. With screen= it holds components given parent=<the sheet> (1.12.0)
modal(..., on_confirm=..., on_cancel=...) no arguments
menu / popup_menu / context_menu selected item
date_picker / time_picker selected value
snackbar(message, action=None, on_action=None, duration=3000) action callback
confirm(title, message, on_result=None) boolean result

Overlay objects expose open() and close().

Android integrations

API What it controls
permissions runtime permission checks and requests
notify / push local notifications and Firebase Cloud Messaging
camera / gallery capture and media selection
location current, continuous and foreground-service tracking
map_view tiles, markers, route line, user position and follow mode
routes cancellable driving, walking or cycling route request
service periodic and one-shot WorkManager tasks
audio / video background audio and Media3 video
uploads multipart transfer with progress and cancellation
haptics, speech, browser, downloads, calendar, phone, recorder, printing, incoming the rest of the phone, one call each — 1.13.0
home_widget, quick_tile, ads, updates, health outside the app — 1.13.0
timer, network, display, app_lock timers, the connection, the screen, the app lock — 1.13.0
native.use(), @native Android classes reached from Python — 1.13.0

For setup requirements and complete examples, continue to the module reference or the guide index.

The phone and outside the app — 1.13.0

Every call of these families, as the build reads them. A family whose first line is marked "at the top of the file" needs that line before any of its other calls. A call that answers later hands on_result two values: whether it worked, and the value or the word for why not.

haptics.tap()
haptics.tick()
haptics.heavy()
haptics.success()
haptics.error()
haptics.vibrate(ms=200)
haptics.available()                                                             # yes or no

speech.say(text, language="", rate=1.0, pitch=1.0, on_result=None)              # answers later, in on_result(ok, value)
speech.stop()
speech.is_speaking()                                                            # yes or no
speech.listen(prompt="", language="", on_result=None)                           # answers later, in on_result(ok, value)
speech.can_listen()                                                             # yes or no

browser.open(url)
browser.show(url="", html="", title="", on_result=None)                         # answers later, in on_result(ok, value)

files.read(name)
files.write(name, text)
files.append(name, text)
files.save_as(name, text, mime="text/plain", on_result=None)                    # answers later, in on_result(ok, value)
files.save_copy(path, name="", on_result=None)                                  # answers later, in on_result(ok, value)

downloads.start(url, name="", title="", on_result=None)                         # answers later, in on_result(ok, value)

calendar.add_event(title, start, end="", location="", notes="", all_day=False)

phone.dial(number)
phone.sms(number, text="")

recorder.start(on_result=None)                                                  # answers later, in on_result(ok, value)
recorder.stop()
recorder.is_recording()                                                         # yes or no
recorder.seconds()

printing.html(html, name="Document", on_result=None)                            # answers later, in on_result(ok, value)
printing.text(text, name="Document", on_result=None)                            # answers later, in on_result(ok, value)

incoming.links(scheme, host="", preview="")                                     # at the top of the file
incoming.shares(kind="text", preview="")                                        # at the top of the file
incoming.link()
incoming.text()
incoming.file()
incoming.clear()

home_widget.define(title, text="", caption="", icon="", name="")                # at the top of the file
home_widget.set(text, caption="", title="")
home_widget.count()
home_widget.pin()

quick_tile.define(label, icon="", preview=False)                                # at the top of the file
quick_tile.set(active=True, subtitle="")
quick_tile.is_active()                                                          # yes or no
quick_tile.opened()                                                             # yes or no
quick_tile.clear()
quick_tile.offer(on_result=None)                                                # answers later, in on_result(ok, value)

ads.setup(app_id)                                                               # at the top of the file
ads.banner(unit, position="bottom")
ads.hide_banner()
ads.prepare(interstitial="", rewarded="")
ads.interstitial(unit, on_result=None)                                          # answers later, in on_result(ok, value)
ads.rewarded(unit, on_result=None)                                              # answers later, in on_result(ok, value)
ads.consent(on_result=None)                                                     # answers later, in on_result(ok, value)
ads.needs_privacy_options()                                                     # yes or no
ads.privacy_options(on_result=None)                                             # answers later, in on_result(ok, value)

updates.check(on_result=None)                                                   # answers later, in on_result(ok, value)
updates.start(mode="flexible", on_result=None)                                  # answers later, in on_result(ok, value)
updates.install()

health.policy(url)                                                              # at the top of the file
health.available()                                                              # yes or no
health.allowed(kind)                                                            # yes or no
health.ask(kinds, on_result=None)                                               # answers later, in on_result(ok, value)
health.read(kind, start="", end="", on_result=None)                             # answers later, in on_result(ok, value)

timer.after(seconds, run)
timer.every(seconds, run)
timer.cancel(run)
timer.running(run)                                                              # yes or no

network.online()                                                                # yes or no
network.kind()
network.metered()                                                               # yes or no
network.on_change(run)

display.keep_on(on=True)
display.brightness(level)
display.immersive(on=True)
display.orientation(kind)

app_lock.after(minutes, title="")
app_lock.now()
app_lock.off()
app_lock.locked()                                                               # yes or no

Screen(id=..., orientation="portrait" | "landscape" | "auto") and run(start_screen=..., orientation=...) say which way up a screen opens. @native(preview=...) over a function, with classes named by native.use("android.os.Vibrator") at the top of the file, is Written in Python.