ApkPy 1.2.0 — live data, media and device runtime¶
The interface no longer waits for the app.
ApkPy 1.2.0 connects the component tree to large datasets, observable state, streaming transfers, persistent sockets, native video, remote push, continuous location and structured documents. The same Python callbacks still run in the Previewer and compile into an inspectable Android project.
Native verification still matters
The release passed compiler tests and a real Android Java compilation. Device integrations should still be tested on an emulator or physical Android device as part of each application's own release process.
What this update changes¶
- Large collections stop allocating every row. A template is recycled for
visible items in the Previewer and Android
RecyclerView. - State becomes observable. Text and visibility bindings update when the value changes, while lifecycle callbacks stop screen work at the right time.
- Uploads stream instead of buffering entire files. Progress, cancellation, form fields and Android content URIs use one API.
- WebSocket connections become a supported runtime primitive. Messages, reconnects, ping/pong and a bounded pre-connect queue stay off the UI thread.
- Video becomes native media. The Previewer plays real picture and sound; Android uses Media3/ExoPlayer for progressive MP4, HLS and DASH.
- Remote push becomes conditional Android output. FCM code and Gradle dependencies are generated only when the app is configured for push.
- Maps and location become continuous. Key-free OpenStreetMap tiles, Fused Location, route calculation and an Android 14 foreground location service share one declarative surface.
- Documents stay native. Rich inline spans and Markdown compile to selectable Android text, while expandable trees recycle only visible rows.
Capability explorer¶
Thousands of records, a handful of row views.
Use it for music queues, message history, feeds, catalogues and search results. Change the template without changing the dataset.
- Previewer
- Materializes the visible window plus a small buffer.
- Android
- Generates RecyclerView, Adapter and recycled ViewHolders.
Callbacks change values, not individual widgets.
One value can update several labels and visibility targets. Screen lifecycle callbacks connect and release observers predictably.
- Bindings
- Value templates and conditional visibility.
- Lifecycle
- Mount, resume, pause and destroy.
Multipart transfers with progress and cancellation.
Files, images, audio and video share a streaming encoder. ApkPy owns the multipart boundary so application headers cannot corrupt the body.
- Progress
- Percent, bytes sent and total bytes.
- Android
- File paths and content:// URIs.
Chat, presence, live likes and order events.
WSS frames are handled away from the UI thread with ping/pong, intentional close and bounded exponential reconnect behavior.
- Handshake
- Queues up to 64 outgoing messages.
- Recovery
- Bounded backoff after network changes.
One player surface in both feedback loops.
The Previewer decodes picture and sound with drag seeking. The Android project uses Media3 and releases the player with the Activity lifecycle.
- Sources
- Progressive MP4, HLS and DASH on Android.
- Controls
- Play, pause, seek, speed, mute and source changes.
Delivery, token refresh and topic subscriptions.
The Previewer delivers deterministic local events. Configured Android apps receive FCM callbacks and native system notifications.
- Local test
- push.simulate() uses the same callbacks.
- Android
- Conditional Firebase plugin and messaging service.
The map follows GPS without fighting the user.
Starting tracking enables follow mode. A manual pan or pinch disables it, and the app can opt back in when the user presses recenter or Start.
- Foreground
- Fused Location with filtering and fallback.
- Background
- Android 14 typed foreground service and START_STICKY.
Notes, posts and trees become native views.
Use exact inline spans, readable Markdown and expandable hierarchies for knowledge bases, social discussions, release notes and structured editors.
- Text
- Selectable TextView content backed by Android Spannable.
- Trees
- RecyclerView containing only currently visible rows.
1. Virtual collections with custom templates¶
virtual_collection() accepts ordinary dictionaries. Its template maps five
visual slots — title, subtitle, image, meta and badge — to fields in
each item. Only visible rows and a small overscan buffer exist as views.
from apkpy_lib import Screen, toast, virtual_collection
library = Screen(id="library", scroll=False)
tracks = [
{
"title": "Afterimage",
"artist": "North Arcade",
"album": "Night Transit",
"duration": "3:42",
"quality": "LOSSLESS",
"artwork": "afterimage.jpg",
},
# The list may contain thousands of records.
]
track_list = virtual_collection(
tracks,
template={
"image": "{artwork}",
"title": "{title}",
"subtitle": "{artist} · {album}",
"meta": "{duration}",
"badge": "{quality}",
},
layout="list", # use "grid" with columns=... for catalogues
item_height=92,
buffer=3,
on_click=lambda item: toast(item["title"]),
screen=library,
)
# Replace the dataset without rebuilding the screen tree.
track_list.set_items(tracks)
The Previewer calculates the visible window. Android generates a
RecyclerView.Adapter and ViewHolders, so scrolling cost tracks the viewport
rather than the total record count.
2. Reactive state and screen lifecycle¶
state() returns an observable value. A callback mutates the value once and
every binding is refreshed automatically. lifecycle() scopes work to a
screen instead of leaving network or playback callbacks running forever.
from apkpy_lib import Screen, label, lifecycle, spinner, state
queue = Screen(id="queue")
queue_count = state(2, id="queue_count")
syncing = state(False, id="syncing")
phase = state("Waiting", id="phase")
count_view = label("", screen=queue)
phase_view = label("", screen=queue)
loader = spinner(screen=queue)
queue_count.bind(count_view, "{value} tracks queued")
phase.bind(phase_view)
syncing.bind_visibility(loader, when=True)
def add_track():
queue_count.increment()
def toggle_sync():
syncing.toggle()
lifecycle(
screen=queue,
on_mount=lambda: phase.set("Mounted"),
on_resume=lambda: phase.set("Active · observers resumed"),
on_pause=lambda: phase.set("Paused · work suspended"),
on_destroy=lambda: phase.set("Destroyed · resources released"),
)
Available state operations are get(), set(value), increment(amount),
decrement(amount), toggle(), bind(component, template) and
bind_visibility(component, when=True).
3. Streaming multipart uploads¶
Uploads use 64 KiB chunks instead of reading the whole payload into memory. The task ID makes cancellation explicit and stable across Previewer and Java generation.
from apkpy_lib import Screen, gallery, label, storage, uploads
upload_screen = Screen(id="upload")
status = label("Choose a file", screen=upload_screen)
access_token = storage.get("access_token", "")
selected_path = ""
def picked(success, path):
global selected_path
if success:
selected_path = path
def progress(percent, sent, total):
status.set_value(
"Uploading · " + str(percent) + "% · "
+ str(sent) + "/" + str(total) + " bytes"
)
def finished(success, response):
if success:
status.set_value("Upload complete")
elif response == "cancelled":
status.set_value("Upload cancelled safely")
else:
status.set_value("Upload failed · " + str(response))
def start_upload():
if selected_path == "":
status.set_value("Choose a file first")
return
uploads.image(
"artwork-upload",
"https://api.example.com/releases/artwork",
selected_path,
field="artwork",
fields={"release_id": "482"},
headers={"Authorization": "Bearer " + access_token},
on_progress=progress,
on_result=finished,
)
def choose_file():
gallery.pick(on_result=picked)
def cancel_upload():
uploads.cancel("artwork-upload")
uploads.file(), uploads.image(), uploads.audio() and uploads.video()
share the same transport. ApkPy controls Content-Type, Content-Length and
the multipart boundary; application code should supply authentication and
domain-specific headers only.
4. WebSocket and real-time messages¶
The WebSocket client supports ws:// and encrypted wss:// endpoints,
optional headers and protocols, ping/pong, server close frames, intentional
disconnects and bounded reconnection. Messages sent during the handshake wait
in a bounded queue rather than disappearing.
from apkpy_lib import Screen, lifecycle, state, storage, websocket
room = Screen(id="room")
connection = "live-room"
phase = state("Offline")
latest = state("No events yet")
access_token = storage.get("access_token", "")
def opened():
phase.set("Connected")
def received(message):
latest.set(message)
def closed(code, reason):
phase.set("Closed · " + code)
def failed(message):
phase.set("Network changed · retry scheduled")
def connect_room():
websocket.connect(
connection,
"wss://realtime.example.com/rooms/482",
headers={"Authorization": "Bearer " + access_token},
on_open=opened,
on_message=received,
on_close=closed,
on_error=failed,
reconnect=True,
reconnect_delay=1.0,
max_reconnect_delay=12.0,
ping_interval=25.0,
)
def send_message(text):
websocket.send(connection, text)
lifecycle(
screen=room,
on_mount=connect_room,
on_destroy=lambda: websocket.close(
connection, code=1000, reason="left the room"
),
)
The connection runs away from the UI thread. Callbacks return to the interface thread in both targets, which makes them safe inputs for reactive state.
5. Native video with Media3¶
The normal ApkPy installation includes the Previewer video dependencies. The
desktop player displays real frames and sound, supports drag seeking and hides
its controller after inactivity. Android uses Media3/ExoPlayer and the native
PlayerView controller.
from apkpy_lib import Screen, label, video
watch = Screen(id="watch", scroll=True)
status = label("Preparing video", screen=watch)
def progress(position, duration, buffered):
status.set_value(
str(position) + "s / " + str(duration)
+ "s · " + str(buffered) + "% buffered"
)
player = video(
"https://cdn.example.com/trailers/episode-01.mp4",
poster="episode-01.jpg",
controls=True,
preload=True,
aspect_ratio="16:9",
fit="cover",
on_ready=lambda: status.set_value("Ready"),
on_progress=progress,
on_end=lambda: status.set_value("Finished"),
on_error=lambda message: status.set_value("Error · " + str(message)),
screen=watch,
)
player.play()
player.pause()
player.seek(30)
player.set_speed(1.25)
player.set_muted(False)
The returned component also exposes stop() and
set_source(url, autoplay=False). Player preparation, pause/resume behavior
and release are connected to the Activity lifecycle.
6. Remote push with Firebase Cloud Messaging¶
The event callbacks are shared. push.simulate() makes the Previewer
deterministic; a configured Android build receives real Firebase messages,
registration-token changes and native notification delivery.
from apkpy_lib import Screen, push, state
updates_screen = Screen(id="updates")
delivery = state("Waiting for an update")
device_token = state("No token yet")
def message_received(title, body, data_json):
delivery.set(title + " · " + body)
def token_changed(token):
device_token.set(token)
def subscription_finished(success, value):
delivery.set("Subscribed · " + value if success else "Push unavailable")
def token_finished(success, value):
device_token.set(value if success else "Token unavailable")
push.listen(
screen=updates_screen,
on_message=message_received,
on_token=token_changed,
on_error=lambda message: delivery.set("Push error · " + message),
auto_notify=True,
channel_id="order_updates",
channel_name="Order updates",
)
push.subscribe("live-orders", on_result=subscription_finished)
push.get_token(on_result=token_finished)
def simulate_delivery():
# Deterministic Previewer delivery through the same callback.
push.simulate(
"Courier nearby",
"Your order is approaching",
{"order_id": "482", "kind": "delivery"},
)
Place google-services.json beside writehere.py for a real Firebase-enabled
Android build. ApkPy then adds the Firebase Gradle plugin, messaging dependency,
service and required manifest entries. Projects that do not use push receive
none of that output.
7. Maps, routes and continuous location¶
map_view() uses OpenStreetMap raster tiles without a bundled map-vendor key.
Markers, the route and the user's live position are separate layers. That is
why a map can show a start marker, a finish marker and a third live-position
dot at the same time.
from apkpy_lib import Screen, label, map_view, location, routes
trip_screen = Screen(id="trip", scroll=True)
status = label("Tracking is paused", screen=trip_screen)
DEMO_PATH = [
[37.7750, -122.4183],
[37.7762, -122.4168],
[37.7774, -122.4151],
]
trip_map = map_view(
markers=[
{"lat": 37.7750, "lng": -122.4183,
"title": "Start", "color": "#8B5CF6"},
{"lat": 37.7774, "lng": -122.4151,
"title": "Finish", "color": "#22D3EE"},
],
route=DEMO_PATH,
show_user=True,
follow_user=True,
cache_days=7,
screen=trip_screen,
)
def location_sample(success, latitude, longitude, accuracy, speed,
bearing, timestamp, provider, is_mock):
if success:
trip_map.set_location(latitude, longitude, accuracy)
def start_tracking():
# A pan or pinch disables camera follow. Start opts back in.
trip_map.set_follow_user(True)
location.watch(
"active-trip",
on_sample=location_sample,
interval=1200,
min_distance=0,
accuracy="high",
provider="auto",
max_age=12000,
max_accuracy=150,
timeout=20000,
preview_route=DEMO_PATH,
screen=trip_screen,
)
def start_background_tracking():
trip_map.set_follow_user(True)
location.start_background_tracking(
"background-trip",
on_sample=location_sample,
title="Trip tracking is active",
text="Tap to return to the route",
interval=5000,
min_distance=0,
preview_route=DEMO_PATH,
screen=trip_screen,
)
Android uses FusedLocationProviderClient, with LocationManager as a
fallback. It filters stale or excessively inaccurate readings, uses an Android
14 foreground service declared as location, and returns START_STICKY so the
system can restore a long-running trip after memory pressure.
Manual pan or pinch intentionally disables camera following. Calling
set_follow_user(True) recentres on the latest reading. Longitude animation
uses the shortest path across the 180/-180 date-line boundary.
Street-aware routes are an explicit network action¶
Route calculation does not move the GPS dot. It replaces the route geometry;
location.watch() moves the user layer when the device or emulator reports a
new position.
def route_ready(success, route_json, distance, duration, error):
if success:
trip_map.set_route(route_json)
else:
status.set_value("Route unavailable · " + error)
def calculate_route():
routes.calculate(
"trip-route",
[37.7750, -122.4183],
[37.7774, -122.4151],
on_result=route_ready,
profile="driving", # driving, walking or cycling
alternatives=False,
timeout=15,
)
def cancel_route():
routes.cancel("trip-route")
The default route service is the public OSRM demo endpoint. Production or
privacy-sensitive apps can supply a compatible self-hosted endpoint. No
coordinate is sent until routes.calculate() is called.
Previewer versus device GPS
The desktop Previewer replays only the explicit preview_route. An Android
emulator moves when a route is played from its Location controls. A
physical device moves from real location samples after permission is
granted and tracking is started.
8. Native rich text, Markdown and trees¶
rich_text(), markdown() and tree_view() cover structured content without
adding a WebView or JavaScript runtime. The Previewer keeps text selectable;
Android generates SpannableStringBuilder content and a recycled
RecyclerView for hierarchies.
from apkpy_lib import Screen, markdown, rich_text, tree_view
knowledge = Screen(id="knowledge", scroll=True)
rich_text(
[
{"text": "FIELD NOTE / 04\n", "bold": True,
"color": "#22D3EE", "size": 12},
{"text": "Small interfaces, ", "bold": True, "size": 23},
{"text": "deep structure.", "bold": True, "italic": True,
"color": "#C4B5FD", "size": 23},
],
id="document_lead",
selectable=True,
screen=knowledge,
)
markdown(
"""## Build log · 1.2
> Content remains selectable and native.
- [x] **Bold**, *italic*, ~~strike~~ and `inline code`
- [x] Lists, quotes, links and dividers
- [ ] Connect the renderer to your own storage
""",
id="release_note",
screen=knowledge,
)
tree_view(
[
{
"key": "product",
"title": "Product",
"subtitle": "18 pages",
"children": [
{"title": "Roadmap", "subtitle": "Q3 planning"},
{"title": "Release notes", "subtitle": "12 entries"},
],
},
{
"key": "archive",
"title": "Archive",
"expanded": False,
"children": [{"title": "2025"}, {"title": "2024"}],
},
],
id="workspace_tree",
expand_depth=1,
row_height=60,
screen=knowledge,
)
The Markdown subset includes headings, emphasis, links, inline/fenced code,
quotes, ordered and unordered lists, checkboxes and dividers. Rich spans accept
bold, italic, underline, strike, code, color, size and link.
Tree nodes accept title, subtitle, key, expanded and recursive
children.
This is a renderer, not a storage service. A notes app can keep Markdown in SQLite or encrypted storage, a forum can receive comment data from an API, and a knowledge base can construct its tree from parent IDs. ApkPy renders the result without silently sending that content elsewhere.
Read the complete native rich-content guide.
Audio baseline clarified for 1.2.0¶
The following player capabilities existed before the new runtime work and are documented here to avoid understating what ApkPy can already generate:
- local-file and URL playback;
- foreground Android media service and native
MediaSession; - notification and lock-screen metadata and transport controls;
- audio-focus pause, duck and resume behavior;
- queues with next, previous, shuffle and repeat;
- seeking and synchronized progress, cover, title and artist bindings;
- persistent mini-player, favourites and editable playlists;
- explicit downloads to app-private storage;
- buffering state, guarded polling and one controlled retry of the same source.
from apkpy_lib import audio, mini_player
audio.play_playlist(
sources,
titles=titles,
artists=artists,
arts=artwork,
start=0,
)
audio.now_playing(
progress=seek,
time=elapsed,
cover=cover,
title=track_title,
artist=track_artist,
)
audio.controls(play_pause=play_pause, shuffle=shuffle, repeat=repeat)
mini_player(open=player_screen)
Open the complete audio, playlist and offline-file guide.
Generated output¶
Only emitted when a virtual collection appears in source.
Bindings update views on the Android UI thread.
Supports files and content URIs without an in-memory copy.
Connection callbacks return to the Activity safely.
Native playback and lifecycle-safe release.
Generated only when push is used and configured.
Tile cache, gestures, GPS filtering and background tracking.
Native Markdown, inline styles and expandable trees without a WebView.
Current boundaries¶
Version 1.2.0 does not claim automatic audio quality selection, predictive audio caching, guaranteed gapless playback, crossfade, DRM catalogue playback, video-feed preloading, resumable uploads across process death or a hosted WebSocket/route backend or a hosted collaborative document editor. Rich content renders application data but does not replace storage, synchronization or moderation. ApkPy supplies the client-side native building blocks; the application still owns its servers, authorization policy and production data.
Local validation snapshot¶
- the compiler/transpiler suite completes with 162 passing tests;
- generated XML parses and generated Java passes structural assertions;
- the generated GPS Android project builds successfully;
- foreground location was exercised with changing emulator coordinates;
- background location remained a typed foreground service with its system notification active;
- the release wheel and source distribution pass
twine check.
Start in the Previewer. Finish the check on Android.
Fast feedback and native verification are two parts of the same workflow, not substitutes for each other.