Skip to content

ApkPy 1.8.0 — The camera, and smaller releases

Three things. Your app gets a camera of its own instead of borrowing the phone's, your signed release stops carrying code it never runs, and Python that quietly did nothing on the phone now either works or stops the build saying why.

A camera inside your app

Until now camera.capture() handed the job to whatever camera app the phone had. 1.8.0 opens a camera your app owns: photo and video, both lenses, zoom, focus, exposure, flash and torch, a timer, pause and resume while recording, a review step, and manual ISO, shutter, white balance and focus for apps that need them.

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

home = Screen(id="home")
status = label("Ready", id="status", screen=home)


def captured(success, path):
    if success:
        status.set_value(path)
    else:
        status.set_value("Nothing was kept")


def failed(reason):
    status.set_value("Camera: " + reason)


def take_photo():
    camera.open(mode="photo", lens="back", flash="auto",
                grid=True, on_result=captured, on_error=failed)


def record_clip():
    camera.record(quality="fhd", max_duration=30, audio=True,
                  on_result=captured, on_error=failed)


button("Photo", id="photo", command=take_photo, screen=home)
button("Video", id="video", command=record_clip, screen=home)
run(start_screen=home)

camera.capture() keeps the callback it always had, so an app written against 1.7.0 gains the new camera without a line changing. camera.record(...) is camera.open(mode="video", ...) said shortly.

on_result(success, path) runs once, when the person accepts, cancels, or a fatal error closes the camera. Cancelling is not an error, and does not call on_error.

A path from Android is not a filesystem path

Android answers a content:// URI for a file private to your app. It is a handle, not a location on disk, and the file is not published to the gallery or uploaded anywhere unless your app does that itself.

Manual controls, and hardware that says no

# 10 ms is 1/100 second. Needs a lens with manual sensor control.
camera.capture(iso=200, shutter_ms=10, white_balance="daylight",
               focus_distance=-1, on_result=captured, on_error=failed)

A manual setting the phone cannot honour reports an error rather than pretending it applied, which is the difference between a photography app you can trust and one that quietly lies about its settings. camera.capabilities() asks the hardware what it can actually do before you offer it.

The camera's own interface is yours to shape

controls, labels, colors, title and button_radius change the camera screen without you rebuilding it:

camera.capture(
    controls=["close", "shutter", "flip"],
    title="RECEIPT CAMERA",
    labels={"capture": "Take receipt", "use_photo": "Keep receipt"},
    colors={"accent": "#a5b4fc", "on_accent": "#151a2d"},
    button_radius=10,
    on_result=captured,
)

Controls you leave out leave no empty slot, and close and shutter cannot be removed. Hiding a control hides its button, not the feature: grid=True still draws the grid without a grid button, and pinch-to-zoom still works with no zoom slider.

A viewfinder inside your own layout

When you do not want a camera screen at all, camera_view() puts a live viewfinder in your own layout, with your own buttons around it:

visor = camera_view(id="visor", screen=home, lens="back", fit="cover",
                    on_ready=ready, on_capture=saved, on_error=failed)

button("Take photo", id="shoot", command=lambda: visor.capture(), screen=home)
button("Switch lens", id="flip", command=lambda: visor.flip(), screen=home)

No toolbar, no review step, no decisions made for you.

Releases are about a third of the size

apkpy release now runs R8, Android's shrinker, which drops library code your app cannot reach. A screen using two Material widgets was shipping the whole of Material.

same app, signed release
1.7.0 4,496 KB
1.8.0 1,536 KB

An app pulling in Firebase, WorkManager, media3 and RecyclerView still comes to 2,085 KB.

Names are deliberately not obfuscated. Renaming would turn every class into a.b.c, which would make crash.last() useless to your app and force a mapping file to be kept for the life of every published version. The saving is in the shrinking, so ApkPy keeps that and leaves the renaming off.

Memory drops by about 6 MB too, because unreachable code is no longer mapped into the process. Start-up is unchanged — code that is never called was never costing time, only space. Release builds take longer to produce, and apkpy run is unaffected.

The build and release guide has the measurements and what to check on a device before publishing.

Python that used to fail quietly

A search filter is usually written like this, and on the phone it did nothing:

def visible_notes():
    query = query_state.get().strip().lower()
    result = []
    for item in NOTES:
        if not query or query in item["title"].lower():
            result.append(item)
    return result

The whole if -- condition, body and all -- left the app, and the build said nothing. The list came back empty. Several shapes behaved like that, and in 1.8.0 each one either works or stops the build saying why:

Python Before 1.8.0
in / not in inside and, or, not the if vanished translated
x not in y on its own the if vanished translated
"a" if q in text else "b" became empty text translated
for part in text.split(","): ran no times translated, with Python's rules
i + 1 on a range() index 01 on the phone, 1 in the Previewer translated
if i % 2 == 0: on a range() index the if vanished translated
x = -1, x = a if test else b, x = math.pi the line vanished translated
a condition with no translation (is, 0 < x < 10) the branch vanished, or ran on half the test stops the build, U2033
a side with no translation inside and, not or a ternary, such as text * 2 == 4 Double.parseDouble(""): a crash when it ran stops the build, U2033
"n=%s" % n, or price * 2 on text from an input became empty text stops the build, U2033
a, b = parts, row["k"] = v, for key, value in pairs: the line, or the whole loop, vanished stops the build, U2033
a list comprehension at module level the list was created empty stops the build, U2033
an f-string format such as {n:03d} 7 on the phone, 007 in the Previewer stops the build, U2033

in asks the question Python asks. Against a list, tuple, set or dict written in the source, a list kept at module level, or a split() call, it is membership; against anything else it searches the text. That is decided from the source and never from the value, because a search box holding [a is text.

split() follows Python's rules rather than Java's. The separator is text, not a regular expression, so "a.b".split(".") gives two parts; empty parts are kept; maxsplit is honoured; and split() with nothing splits on runs of whitespace. The generated helper was compiled and run against Python's own str.split on 628 cases, with no difference.

Every case that now stops the build was already giving a different result on the phone than in the Previewer -- or none at all. The build is simply where you now hear about it, together with a form that does compile.

What still differs without stopping the build

ApkPy decides from the source, not from the running value, and a few shapes still give a different answer: text[0] on text, + on a function's parameter, and in against a local variable that holds a list. The compatibility page lists each one and what to write instead.

The benchmark, measured again

Fixing those failures showed that the ApkPy row on the benchmark page had measured an app whose list was empty: its comprehension at module level had compiled to nothing. The notes app was rewritten so it builds on 1.8.0, checked on the device to show, filter, add and search its 100 notes, and timed again beside BeeWare/Toga in the same session, on the Pixel 9 emulator:

Same session, debug builds APK Cold start, first three PSS
ApkPy 1.8.0 candidate 5.37 MiB 2,565 ms 67.0 MiB
BeeWare/Toga 34.46 MiB 5,536 ms 83.4 MiB

About twice as soon to start, about a fifth less memory, and a 6.4x smaller APK. The 590 ms published before is withdrawn: it was never a like-for-like number. Times on this emulator also moved by more than 2x within one day, which is why the comparison stays inside one session. The benchmark page has the method and every session.

In the Previewer

The camera is drawn inside the phone frame, not in a window of its own, and it never opens your webcam or microphone. It says so on screen: PREVIEW SIMULATOR · NO CAMERA ACCESS.

The viewfinder shows a synthetic calibration scene, or an image you import. The files it produces are real: photos are genuine JPEGs, and video is a playable MP4 — and the footer states exactly what that MP4 is rather than implying it matches the phone: Video simulation: 640px · 12 fps · no audio.

Zoom, crop, mirroring and white balance affect the output. Focus, flash and manual exposure are rehearsals, not measurements of real optics. Pause and resume, duration limits, review, retake, cancellation and every callback work locally, so the branches your app has to handle can all be exercised at a desk.

Verification, and what is not verified

Check Result
Feature suite 1,040 tests passed
split() helper compiled and run against str.split on 628 cases: no difference
Example apps and documented snippets 92 transpiled with the engine from before and after these changes: every app that built still builds; two scripts that are not apps now stop, both on code that was being dropped
Transpiler suite 258 checks passed
Previewer, seen not assumed photo mode, video mode with Record/Pause/Mic, grid, zoom and EV sliders, and the honest simulator banners, all photographed
Review contract Retake and Use photo/video are fixed in both runtimes, not optional
Android build a generated project compiles and installs
EXIF from manual settings reported as ISO 200 and 10 ms on a device — taken from the implementation report, not re-measured here
Video with audio, lens switching, rotation reported working on one device — not re-measured here
Other phone models not verified: one device, one manufacturer

Not implemented, and not planned for this release: RAW/DNG, vendor HDR, night and portrait modes, slow motion, concurrent cameras, live frame analysis, publishing to the gallery, and capture from the background or lock screen. Barcodes remain the separate scan API.

The camera guide has the full option tables, the interface customisation contract, and the Previewer's limits in detail.