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 acontainerorcard. - An
idis 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.
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:
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.