More than one file¶
ApkPy reads one writehere.py, so for a long time an application had nowhere
to put anything: every screen, every callback and every piece of arithmetic
landed in the same file. The largest app
in this repository is 212 lines, and that is not a coincidence.
Now writehere.py can import plain Python modules that sit next to it.
import money
from apkpy_lib import Screen, label, run
home = Screen(id="home")
total = label("", id="total", screen=home)
total.set_value(money.euros(1999)) # EUR 19.99
# money.py, beside writehere.py
CURRENCY = "EUR"
def euros(cents):
whole = cents / 100
return CURRENCY + " " + str(round(whole, 2))
The helper is merged into your application before anything is translated, so
money.euros becomes an ordinary method of the generated Activity and
CURRENCY becomes a field. The Previewer needs nothing special: beside the
script, this is the import Python already does.
Run the complete example
with python writehere.py, then build it with apkpy run.
What a helper holds¶
Functions and constants. Screens, themes, widgets and CSS stay in
writehere.py, where ApkPy reads them in the order they appear. A helper that
declares a Screen or calls Theme(...) stops the build with
U2036 rather than being quietly ignored.
A helper may import another helper. They are merged innermost first, so a helper can call the one it imports.
Only import helpers, not from helpers import¶
This is deliberate. ApkPy merges the whole file into one namespace, so the
from form would leave your app holding names that the Previewer's real Python
would not — a difference between the desktop and the phone that would only
appear later, which is the failure this project spends its time removing.
For the same reason, a name defined in two files stops the build. In one namespace there is no way to keep both, and picking one silently is worse than asking you to rename it.
Numbers that stay numbers¶
A function parameter used to be text, always, because "everything is String" is
how values cross the boundary between your Python and the generated Java. That
is right at the boundary and wrong inside the function: cents / 100 stopped
the build even when every call passed a number.
Now the call sites decide. A parameter counts as a number when the function is called at least once and every call passes something ApkPy can prove is one:
- a number written in the source,
2or19.99; int(...),float(...)orlen(...);- arithmetic between those;
- a name the whole module only ever assigns from them;
- another function of yours whose every
returnis a number.
That last one is what lets helpers feed each other:
def with_vat(cents):
return cents + cents * VAT / 100
def euros(cents):
return CURRENCY + " " + str(round(cents / 100, 2))
total.set_value(euros(with_vat(2449))) # EUR 30.12
Everything else stays text. One call site that passes a value ApkPy cannot prove is a number — including a keyword argument, which does not say which position it fills — and the parameter is text again for every call.
This is deliberately narrow. Anything ApkPy says yes to here becomes real arithmetic in the generated Java, and a wrong yes would turn a sum into glued text without a word. Text and numbers only have to agree on the phone and on the desktop; they do not have to be guessed.
Functions that build UI¶
A header, a card or a bar that appears on several screens is written once, as a function, and called once per screen:
def header(title, subtitle, screen):
label(title, id="screen_title", screen=screen)
label(subtitle, id="screen_subtitle", screen=screen)
def stat(name, value, screen):
box = card(id="stat", screen=screen)
label(name, id="stat_name", parent=box)
number = label(value, id="stat_value", parent=box)
return number
header("Today", "One run a day keeps the streak alive.", today)
distance = stat("Distance", "0.0 km", today) # distance.set_value(...) later
for week, km in [("This week", "18.4 km"), ("Last week", "22.1 km")]:
stat(week, km, history)
The Previewer runs this as Python. For the phone -- which lays out every screen before the app runs -- ApkPy puts each call's body where the call is before it translates anything, with the arguments in place of the parameters and the function's own names made unique to that call. The phone ends up with the same components on the same screens; a test runs the same app both ways and compares them screen by screen.
- Return a component to use it later.
distance = stat(...)bindsdistanceto the label the function returned. - The same
id=on every copy is fine. It is what the CSS reads, so#statstyles every card; each copy still gets its own view on the phone. screen.idand joined text are worked out while compiling, soid="back_" + screen.idandf"on {screen.id}"give each screen its own.- A module-level
forthat builds UI is unrolled -- over a list written in the file, a list defined at the top of it, orrange(N), includingfor name, value in [(...), ...]:. - A function can live in a helper file, and one UI function can call another.
Called from a button, a callback or a job, a UI function would build a screen
that already exists; that stops the build with
U2038, as does a loop that builds UI from data that
only arrives while the app runs -- that is what list_view() and
virtual_collection() are for.
What this does not do¶
- A helper holds functions, classes and constants -- and functions that
build UI. A class in a helper is used as
shop.Product("tea", 2.0), and since 1.14.1 it is translated like a class ofwritehere.py. - Only files beside your application. No packages, no subfolders, no
from . import. An import ApkPy does not recognise is left exactly as it was, soimport mathand everything the compiler already reads keep working. - Parameters travel as text. The Java signature is still
String cents; what changed is that arithmetic inside the body is allowed to treat it as a number.
Checked for this change¶
- 29 focused tests, plus 1,151 feature tests, 35 general tests and 258 transpiler checks.
- 100 example apps and documented snippets transpiled before and after: nothing that built stopped building.
- The example above was rendered in the Previewer and built with Gradle —
BUILD SUCCESSFUL— and the generated Java read by hand. - Installed on a Xiaomi 25069PTEBG running Android 16 and driven: the example opened,
showed
EUR 30.12— the arithmetic crossing the file boundary, right on the device — and survived a rotation with the values intact.