Compatibility and limits¶
Know what runs where.
ApkPy keeps the Previewer and generated Android project on one public API, but a desktop preview is not a device certification. This page separates verified behavior, generated output and application responsibilities.
Supported toolchain¶
| Part | Supported range | Notes |
|---|---|---|
| Python | 3.8–3.13 | The package declares and tests this range |
| JDK | 17–21 | Use apkpy doctor to detect an incompatible JDK |
| Android SDK | Android Studio SDK or apkpy setup |
A real SDK is required for Gradle compilation |
| Desktop Previewer | Windows, macOS or Linux with Tk support | Native device APIs may use a desktop simulation |
| Android output | Java, XML and Gradle project | No Python runtime is embedded in the APK |
Production Feeds behavior¶
| Capability | Previewer | Generated Android |
|---|---|---|
| Virtual list/grid | Reusable pooled widgets | Native RecyclerView |
| End prefetch | Viewport threshold | RecyclerView.OnScrollListener |
| Duplicate request guard | Loading latch | Loading latch |
| Page insertion | Preserves virtual offset | notifyItemRangeInserted() |
| Pull-to-refresh | Top gesture and refresh() |
Native SwipeRefreshLayout |
| Prepend | Preserves visible offset | Range insert plus offset restore |
| Update one item | Repaints the affected pooled row | notifyItemChanged() |
| Remove one item | Closes the removed position | notifyItemRemoved() |
| Merge/rollback | Reconciles the visible dataset | Native DiffUtil |
| Optimistic history | Named in-memory snapshots | Named in-memory snapshots |
Conditional generation¶
ApkPy scans the current application before adding support code:
| Your source uses | Generated project receives |
|---|---|
Plain virtual_collection() |
RecyclerView and its adapter |
on_end_reached |
Scroll listener, prefetch threshold and loading latch |
on_refresh |
SwipeRefreshLayout dependency and refresh wrapper |
prepend_items() |
Range insertion and scroll-offset restoration |
update_item() / remove_item() |
Targeted adapter notifications |
merge_items() / rollback() |
DiffUtil helper |
| Optimistic methods | Snapshot maps only for the affected collection |
An application that does not use feeds receives none of this runtime.
Data Core behavior¶
| Capability | Previewer | Generated Android |
|---|---|---|
| Database engine | Python sqlite3 |
Native SQLiteOpenHelper |
| Operation queue | One ordered worker | One ordered ExecutorService |
| Callback thread | Tk interface thread | Android main Handler |
| Batch binding | SQLite parameters in one transaction | Reused SQLiteStatement in one transaction |
| Schema metadata | apkpy_schema_meta |
apkpy_schema_meta |
| Destructive migration | Private backup, transaction, restore | Private backup, transaction, restore |
| Model repository | Runtime model object | Generated repository per model |
| Foreign keys | SQLite foreign keys enabled | onConfigure() enables SQLite foreign keys |
| Relation include | One batched query per relation | One batched query per relation |
| Observation | Screen lifecycle + snapshot comparison | Activity lifecycle + generated invalidation tracker |
Projects without db.model() receive no typed repositories, data executor,
schema metadata or migration runtime. Legacy SQL calls remain available.
What was validated¶
The generated Java was inspected for SQLiteOpenHelper, SQLiteStatement,
the single data executor, main-thread callback handler, repositories, indexes,
foreign keys, batched includes, observer generations, lifecycle hooks,
selective post-commit invalidation, sequential migrations, OnScrollListener,
notifyItemRangeInserted, notifyItemChanged, notifyItemRemoved, DiffUtil
and per-collection optimistic history. A separate plain collection test checks
that those helpers are omitted when unused.
These checks prove repeatable generation and compilation. They do not replace testing an application's own backend, device permissions, OEM behavior or release signing.
The Python ApkPy translates¶
ApkPy reads your module and writes Java. It translates a fixed vocabulary of
Python rather than running it, so this list is the whole of it. Anything
outside it stops the build with U2033 at its line,
with a form that does compile. 1.14.0 widened the list a long way: most of
what an app is written with is on it. 1.14.1 added classes.
Files
One writehere.py, plus any plain Python modules beside it that it
imports with import helpers. A helper holds functions, classes and
constants; screens, themes and CSS stay in the application. See
More than one file. New in 1.10.0.
Control flow
if / elif / else, while, for, break, continue, return and
pass. Inside a function, for ... else and while ... else: the else
runs when the loop ends without a break. try / except / else /
finally, with more than one except, a type or a tuple of types, and
as error; raise ValueError("...") and assert. The types are CPython's,
with their family tree: except LookupError catches a KeyError.
A condition compares with ==, !=, <, >, <= or >=, chains them
(0 < x < 10), tests membership with in and not in, tests is None and
is not None, asks isinstance(value, (int, float)), and joins those with
and, or and not.
What is on each side decides how two values are compared, and ApkPy reads
that from the source. Against text written out (name == "Ana") it is a
comparison of text; against a number written out (count > 3), of numbers.
Between two values that are not written out it is a comparison of numbers
when ApkPy knows either side is one (a number in the source, int(...),
float(...), len(...), a sum, a range() index, a function that returns
a number), and of text otherwise, as Python compares two strings. Text from
an input stays text until you convert it: int(a) < int(b), not a < b.
for loops over range() (with a step), a list or a tuple, a dict (its
keys), text (its characters), split(), d.items(), enumerate(),
zip(), reversed() and sorted(), and unpacks as it goes:
for key, value in prices.items():.
Functions
Positional and keyword parameters, default values, *args and **kwargs;
a function inside a function, recursion, and a lambda kept in a name.
global for a name at the top of the file. A call lines its arguments up
with the parameters by position, by name, or both; a missing one without a
default, a repeated one, or a name the function does not have stops the
build with U2037.
Values
- Assignment to a name, to several (
a = b = 0), and unpacking:a, b = parts,a, b = b, a,first, *rest = items. Every augmented operator (+=,//=,**=, ...). - Changing an item:
row["k"] = v,items[i] = v,row["k"] += 1,del items[0]. None,TrueandFalseas values, anda or b,not xor a comparison used as a value.- Lists, dicts and tuples written out. A tuple shows with round brackets,
(1, 2), and cannot change: changing one stops the build, as CPython raises there. - Indexing (
items[-1],text[0]), slicing with a step (text[::-1]), the conditional expressiona if test else b, and comprehensions inside a function -- list and dict, withif, nested -- withany()andall().
Arithmetic
+, -, *, /, //, % and **, with Python's rules: // and %
round towards minus infinity (-7 // 2 is -4), / always gives a float,
and dividing by zero raises ZeroDivisionError. Text repeats with *, and
lists join with +. Adding a number to text written out stops the build,
as CPython raises TypeError there. Convert what somebody typed before you
count with it: float(price) * 2.
Builtins
len, int, float, str, bool, round, list, tuple, abs, min,
max, sum, sorted (with reverse=), reversed, enumerate, zip,
range, any, all, divmod, pow, chr, ord and isinstance.
print() writes to Logcat on the phone and to the console in the Previewer.
Text
upper, lower, strip, lstrip, rstrip, replace, split,
splitlines, join, title, capitalize, swapcase, find, count,
startswith, endswith, zfill, ljust, rjust, center, isdigit,
isalpha, isalnum and isnumeric. Indexes and slices count characters,
so an emoji is never cut in half.
split() gives a list with Python's rules: the separator is text rather than a
regular expression, empty parts are kept, maxsplit is honoured, and with no
separator it splits on runs of whitespace.
f-strings take {value}, {value!r}, widths and alignment ({n:>5},
{n:05d}), {x:.2f}, {x:,.2f} and percentages ({x:.0%}). .format()
and % formatting take the same.
Lists and dicts
Lists: append, extend, insert, pop, remove, index, count,
sort (with reverse=), reverse and clear. Dicts: get (with a
default), keys, values, items, pop and in. A pop() is a line of
its own or the value of an assignment (last = items.pop()), as on the phone
it changes the list as well as answering.
A list written at module level that the app changes is kept in one process-wide store, so every screen sees the same list -- exactly as a module-level list behaves in Python.
Classes
New in 1.14.1. A class at the top of the file or of a helper module,
with __init__, methods, a parent class and super(), class attributes
(count = 0, read as Shape.count and changed with Shape.count += 1),
@staticmethod, @property, and __str__, __repr__ and __eq__.
An object behaves as it does in CPython. Every name that holds it sees the
same changes, so tasks[0].done = True is seen through the list and through
any other name for that task. is asks whether two names hold the same
object. isinstance(x, Shape) and type(x).__name__ follow the family
tree, and the method that runs is the one of the object's own class.
Reading a field that was never set raises AttributeError.
A field that is always given the same kind is read as that kind, so
self.price * self.qty is arithmetic and self.items.append(x) changes
the object's list. A method can be handed over as a value:
command=cart.clear. An object made at the top of the file is made once,
when the app starts, and every screen sees the same one.
class Task:
def __init__(self, title):
self.title = title
self.done = False
def toggle(self):
self.done = not self.done
def __str__(self):
return ("[x] " if self.done else "[ ] ") + self.title
Dataclasses and errors of your own
@dataclass gives a class the __init__, __repr__ and __eq__ CPython
writes from its fields, unless the class writes its own. The fields take
field(default=...), field(default_factory=list), init=, repr= and
compare=; a dataclass can extend another, and ClassVar and
__post_init__ work as in CPython.
A class that extends an error is one more error for raise and except:
except ValueError catches class TooLong(ValueError): pass, and str(e)
is its message. Its body is pass, a docstring, or an __init__ that hands
the message on with super().__init__(message) -- with a default message,
if you like.
from dataclasses import dataclass, field
@dataclass
class Item:
name: str
price: float = 0.0
tags: list = field(default_factory=list)
class SoldOut(Exception):
pass
math
Write import math and use the standard module; the Previewer gets Python's
own and the phone gets java.lang.Math.
sqrt exp log log10 fabs pow hypot floor ceil trunc
sin cos tan asin acos atan atan2 degrees radians
pi e tau
Where numbers used to disagree, and no longer do
Three differences between the two runtimes were fixed rather than documented away, because each one was invisible until it mattered:
round(2.5)gave2in the Previewer and3on the phone. Python rounds a half to the even neighbour andMath.roundalways rounds up; the generator now emitsMath.rint, which has Python's rule.math.pow(10, 8)printed1.0E8on the phone and100000000.0in the Previewer. Java switches to exponent notation from 1e7 and Python only from 1e16, so numbers are now written out with Python's rule.math.floor(2.7)printed2.0on the phone.floor,ceilandtruncreturn anintin Python 3, and now do here too.
math.sqrt(-1) raises on both sides. Java would have returned NaN and
put that word on screen; it now throws, so one try / except covers the
Previewer and the phone together.
Left out on purpose
math.log2, because Java has no Math.log2 and log(x)/log(2) disagrees
with Python on 8 of the first 60 powers of two. math.inf and math.nan,
because Python writes inf where Java writes Infinity. A translation that
is right most of the time is worse than one that says no.
Not translated -- the build stops and says so
Each of these stops the build with U2033, naming the line and a form that
does compile:
- in a class: more than one parent, a parent that is not a class of the
app (
dict),@classmethod, special methods other than__init__,__str__,__repr__and__eq__, and a list or a dict as a class attribute -- give each object its own in__init__; - in a dataclass:
frozen=True,order=True,kw_only=TrueandInitVar; - fields and methods on an error class -- put the detail in its message;
- a class inside a function,
getattr()/setattr()on an object, anddel obj.field; - a part of the screen built at the top of the file from an object -- set its value when the screen opens;
with, and files throughopen()-- use thefilesAPI;- sets,
set()and set comprehensions -- use a list; - generators and
yield,match,nonlocaland:=; sorted(..., key=...);- a comprehension at module level (inside a function it is translated);
- setting an attribute of a component (
status.text = v) -- useset_value(); - an inner function that changes a list of the function around it;
a = b = []: in CPython both names hold the same list, and here they would be two;for ... elseat the top of the file (inside a function it is translated);- modules other than
math,randomand theresubset:json,base64,uuid, anddatetime(use thedatetimeAPI).
When what you need is not here
The list above is what ApkPy translates, not what Android can do. For the rest there is your own Java: a named block with the Java that runs on the phone and the answer the Previewer gives instead, plus the Gradle dependency, manifest line and R8 rule it needs. Inside that block the agreement between the two runtimes is yours to keep.
Differences that do not stop the build
Everything on the phone is text, and ApkPy decides from the source rather than from the running value. These still give a different answer from CPython:
- Whole numbers are 64-bit. A result past about 9.2 × 10^18 stops the app with an error, where CPython carries on.
a + b, when both came out of a dict (p = row["price"]) and neither is known to be a number, joins them as text. Add the items themselves (row["a"] + row["b"]) or convert withint()."5" + 1gives6when the source does not say"5"is text; CPython raisesTypeError.- A number read from one dict and written into another (
b["n"] = a["n"]) goes in as text, unless it is0or the result of a sum. d[k]with a missing key gives empty text, except inside atrythat catchesKeyError; CPython raises.- An error that comes from Java carries Java's message:
int("x")saysFor input string: "x". Its type is CPython's,ValueError. str(flag)of a storedFalseorNoneis false in anif, where CPython says true.capitalize,swapcaseandisnumericuse Java's character tables:ß, ligatures and CJK numerals can differ.x ** ywith decimals: about 4 in 3,000 differ in the last digit.- A tuple inside a list or another tuple shows with square brackets:
[(1, 2)]reads[[1, 2]]. isinstance()of text the code does not follow, which looks like a number, saysintorfloat.(1, 2) == [1, 2]is true; CPython says false.- A list the code does not prove is a list -- one read from storage, say --
shows as its JSON:
["a","b"]. - A switch's
get_value()is"true"or"false", on both sides.
Regular expressions — 1.7.0 subset¶
re.match, re.search, re.sub and re.findall now have an Android
translation in ApkPy 1.7.0. Use constant Unicode patterns and flags inside callbacks/helper
functions; input and replacement text may be dynamic.
import re
def check_text():
text = message.get_value()
found = re.search(r"\bREF-(?P<code>[0-9]{4,8})\b", text, re.I)
if found is not None:
reference.set_value(found.group("code"))
else:
reference.set_value("No reference")
matches = re.findall(r"#[\w]+", text)
tags.set_items(matches)
cleaned.set_value(re.sub(r"\s+", " ", text).strip())
match checks the beginning, not the entire string: add \Z for full-input
format checks. Match objects support group, groups, start, end, span
and checks against None; group selectors must be constant. findall returns
strings without captures, a capture with one group, and grouped sequences with
multiple captures. Assign its result before iteration or passing it to a list.
Supported flags are IGNORECASE, MULTILINE, DOTALL, VERBOSE, ASCII and
UNICODE (including short aliases and | combinations). Named groups,
lookahead, greedy/lazy quantifiers, common character classes and boundaries
are supported. Replacements use Python's \1 / \g<name> syntax; $ is literal.
Dynamic patterns, re.compile, fullmatch, finditer, split, bytes,
callable replacements, lookbehind, pattern backreferences, conditionals,
atomic groups and possessive repetition are outside this subset. Unsupported
forms fail the build with C1701; do not assume everything accepted by the
desktop re module can be exported to Android.
The generator fixes Unicode classes and case folding to the build Python's tables. Use the same Python version for preview and build. Regex calls are synchronous: bound input sizes and avoid pathological patterns. Java's native regex helper adds no Python runtime, permission or Android dependency, and is only emitted when used. This supports format validation and text extraction; it does not prove an email address exists or replace server-side validation.
Never translatable
requests, numpy, pandas, os, pathlib, threading, sqlite3 and
anything else that needs a Python interpreter: there is none on the phone.
Use https, files, db and background_job instead. U2033 names the
replacement when it recognises what you reached for.
Deliberate boundaries¶
Production Feeds does not provide:
- a backend, cursor format or API authentication;
- automatic offline synchronization;
- conflict resolution between REST, WebSocket and local database records;
- durable optimistic transactions after the process is killed;
- automatic retry queues or request deduplication by HTTP response ID;
- Paging 3, Room or Firebase as mandatory dependencies;
- item-level business rules such as who may edit or delete a record.
This division is intentional. ApkPy provides efficient native collection behavior while the application retains control over product rules and data ownership.
Previewer versus device¶
NFC — new in 1.9.0¶
The NFC API supports foreground tag IDs/metadata, NDEF text and
URI reading, and a one-record text/URI write to compatible tags. Callbacks are
(ok, value); tag results are JSON strings read with json_get(). Its manifest
permission is normal, not an Android runtime request. No NFC helper or
permission is emitted when the app does not call the API.
There is no HCE/card emulation, raw ISO-DEP/APDU, bank-card protocol, MIFARE Classic authentication, Beam, cold-start tag launch or screen-off reading. Writes replace existing records and are not transactional. A freshly formatted tag must be removed and re-presented for independent read-back and capacity. The desktop panel is simulation, not physical NFC validation. See the development status; this API is not in published 1.8.0.
Contacts — new in 1.9.0¶
The Contacts API supports one phone/email selection,
display-name search with limit/offset, detail reads and native create/edit
forms. Only list/get request READ_CONTACTS; no direct deletion or
WRITE_CONTACTS is included. Editor return does not prove a save. The compiler
omits the helper when unused; no new Android dependency is required.
Search uses literal substrings and ASCII case-insensitive ordering. Paging skips a provider cursor, and changes between requests may shift offsets. No photos, groups, bulk writes, observers or vCard support is included. The fictional desktop simulator cannot validate Android grants, account sync or OEM editors. See verification limits.
Device-only checks¶
Use the Previewer for layout, callbacks, data flow and rapid iteration. Use an Android emulator or physical device before release for:
- permissions and background restrictions;
- notification channels and lock-screen controls;
- hardware codecs, camera, microphone, GPS and Bluetooth;
- lifecycle behavior after process recreation;
- network security configuration and certificate behavior;
- keyboard, accessibility, screen density and manufacturer-specific UI.
If the Previewer and Android differ, preserve the Python API and repair both
the Previewer and generator source. Editing generated Java alone is temporary;
the next apkpy build replaces it.
Release checklist¶
- [ ] Create a new virtual environment and install the built wheel.
- [ ] Run one small example from the installed package.
- [ ] Generate a fresh Android project with
apkpy build. - [ ] Compile the generated project using JDK 17–21.
- [ ] Test narrow and wide screens in the Previewer and Android.
- [ ] Test every backend failure path and rollback.
- [ ] Confirm
has_more=Falsestops repeated page requests. - [ ] Check that refresh returns the authoritative first page.
- [ ] Review the generated manifest and dependencies.
- [ ] Only then create signing material or publish the package.
Start with the Data Core guide, continue to Reactive Data, then inspect the complete 1.3.2 release notes.
For broader release evidence and the stability contract, continue to How ApkPy is tested. For the renderer boundary, use Previewer versus Android.