Repository navigation
Declared app root and Pythonic state: redefine to update, no update function #11
Description
What
The notebook's live app — redefining the UI in a cell changes the screen, and notebook and screen share state — as a declarative API. The maintainer ruled out an explicit update function (INTENT §5.1, quoted there): main.App.update(...) and getValue() / setValue() were constraints of the old implementation. Also covers DefaultCoroutineScope / MainCoroutineScope from the notebook's imports.
Chosen API
from pythonx.compose.runtime import Composable, app, state, remember_saveable
messages = state("") # a Compose MutableState; read and written as .value
@app
@Composable
def Root():
Text(messages.value) # reading .value in composition subscribes, as in Kotlin
# in another cell
messages.value = "streamed text" # the screen recomposes; no call to refresh anything
@app
@Composable
def Root(): # redefining the root *is* the change; the screen follows
Column(content=lambda: Text(messages.value))@appdeclares the application root and returns the function unchanged, like@Composable. Declaring it again replaces the root. Internally, the root sits in a ComposeMutableStatethat the host's composition reads, so replacing it is an ordinary state write and Compose recomposes. Nothing in the public API is called to update.state(initial)ismutableStateOfwith a.valueproperty.remember_saveable(initial)inside a composable returns the same kind of object and survives configuration changes. NogetValue()/setValue()in the Pythonic surface.mainstays the application's module (a template underexamples/), not this package.
Why this one
- B — an
App(content=Root)object rebuilt per cell. Rebinding a module global changes nothing unless something watches the module, so the binding is either an explicit refresh (ruled out) or a hidden watcher. - C — the host renders
main.Rootby name and observes module attributes. Implicit and fragile: it needs import or attribute hooks on someone else's module, and couples the host to a naming convention. - Chosen (
@app). The decorator is the declaration, the same idiom as@Composable, and replacing the root is a Compose state write, which is the mechanism Compose already uses to update the screen.
Needs from the binder
A host entry point that composes the declared root and reads its MutableState within composition, so a replacement recomposes. Today PythonComposition exists only in PythonMultiplatform's test fixtures. Requested from the lead with this issue number.
Done when
A render test against PythonMultiplatform declares a root, renders it, redeclares it, and sees the new pixels without any update call. A second test writes state.value from outside composition and sees the screen recompose. Both are observed red first.
Activity
[-]Provide remember_saveable, coroutine scopes and live screen update[/-][+]Declared app root and Pythonic state: redefine to update, no update function[/+]b-re-w commentedon Oct 2, 2026
Fitting the host contract (python-multiplatform #18)
The host draws with
PythonContent(module, attribute)(python-multiplatform-compose, packagepython.multiplatform.compose): it readsmodule.attribute, a State whose.valueis a zero-argument Python root callable, and redraws on the next frame when that value changes.So the mechanism is:
pythonx.compose.runtime.app_rootis that State. The host is configured once withPythonContent("pythonx.compose.runtime", "app_root"). Neither the app nor the notebook names it.@appsetsapp_root.value = fnand returnsfnunchanged. Redeclaring the root in a cell is that write, so the screen follows. No update call exists in the public API.app_rootis a real ComposeMutableStatecreated through the binder'sandroidx.compose.runtime.mutableStateOf, so Compose observes the write. It is created lazily on first use, because the binder may be installed afterpythonxis imported.state(initial)is that samemutableStateOf, read and written through.value.app_root.valueisNone, and the host draws nothing.Blocked on python-multiplatform #38: calling
mutableStateOffrom Python and writing.valueare not bound yet. Implementation and the red-first tests (declare, render, redeclare, render again; writestate.valuefrom outside composition) follow once it lands. A pure-Python stand-in for the State would not make Compose recompose, so none is shipped in the meantime.b-re-w commentedon Oct 2, 2026
Host side landed: python-multiplatform #18 via PR #48. The module
:python-multiplatform-compose, packagepython.multiplatform.compose, provides@Composable PythonContent(root: PyObject)andPythonContent(module: String, attribute: String).rootis a Python proxy of a Kotlin State/MutableState whosevalueis a zero-argument Python callable or null; null draws nothing. Replacing the root releases the previous callable, and disposing the composition releases all of them. The host spelling for this package is fixed asPythonContent("pythonx.compose.runtime", "app_root"). Still blocked on python-multiplatform #38 (calling mutableStateOf and writing .value from Python).Docs: Point the pinned int refusal at the binder issue that reverses it
b-re-w commentedon Oct 3, 2026
Decided 2026-10-04 (relayed by the ecosystem lead): the root-swap entry point is the binder's. python-multiplatform's
PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")reads theapp_rootState inside the composition, and the python-multiplatform #26 diagnosis showed a redeclared root is actually redrawn. Recorded in SPEC S5.4 via #93.b-re-w commentedon Oct 3, 2026
Evidence: python-multiplatform
ksp-fixtures/notebook-e2e(#26, merged via #170, develop844cf291; SPEC B-10implementedon desktop). It ran 20 of 20 passed against this package'sdevelopwheel on 2026-10-04 (after python-multiplatform #146 and #168 and this repo's #88). The wheel is unpacked into the host's site-packages, and the tests requirepythonx.compose.__file__to be inside it. Recorded in SPEC via #96. Caveat: no CI workflow runs that module yet, so this is one local run.Done-when met.
NotebookRootTestdeclares the root, which draws exactly what it declares. It writes the shared state from outside the composition, and the screen follows. It redeclares the root with@app, and the new screen appears within 4 frames, pixel-equal to Kotlin, with no update call. The two negatives (@appstubbed out; the host handed the root once) leave the screen unchanged, so the wiring is what makes it pass. The entry point is the binder'sPythonAppView.DefaultCoroutineScope,MainCoroutineScopeandremember_saveable, which this issue also listed, were never decided; they move to #94.