FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [View Raw Code]   [Original HTTPS Page]

pythonnative/docs/guides/browser-preview.md at main · pythonnative/pythonnative · GitHub

Latest commit

 

History

History
238 lines (197 loc) · 10.5 KB

File metadata and controls

238 lines (197 loc) · 10.5 KB

Browser preview

pn preview renders your app in a browser tab, inside a phone frame, with Fast Refresh on every save. It's the fastest way to build UI in PythonNative: edit a component, save, and see the result in well under a second, with no simulator boot, no device deploy, and no native build.

pn preview

The browser renderer speaks the same bridge protocol as the Swift and Kotlin renderers. Your components, hooks, reconciliation, logical navigation, and async work run in the pn preview Python process. The page implements widgets with DOM elements, layout with Yoga WebAssembly, and animation graphs with JavaScript. Platform controls, fonts, and device APIs differ from those in a mobile app.

Quick start

From a project directory (one created by pn init, with an app/main.py that defines App):

pn preview

This starts the dev server, opens http://localhost:8765/?token=... in your default browser (the page trades your dev token for a cookie and drops it from the address bar), mounts your App in a phone frame, and watches app/ for changes. Edit any component and save: the page updates in place while preserving component state (counters, text input, scroll position, the navigation stack).

pn preview imports your real app code, so install your project's dependencies in the same environment first (pip install whatever you declared in [requirements].packages). pn preview warns about missing ones on startup. If an import fails, the preview logs the traceback in its console and the terminal; install the package or fix the code and save to recover.

pn preview                        # the project entry point (app/main.py -> App)
pn preview app.screens.home       # mount a different module's App
pn preview --port 9000            # another port
pn preview --host 127.0.0.1       # don't expose the server on the LAN
pn preview --no-open              # start the server, open the tab yourself
pn start                          # the same server without opening a browser

pn start and pn preview are the same command; pn preview only adds opening the tab. Simulators and devices launched with pn run connect to the same server, so one terminal drives every client.

The toolbar

Along the top of the page:

  • Device: iPhone 15, iPhone 15 Pro Max, iPhone SE, Pixel 8, Pixel Fold (inner), iPad mini, or Fill window. The frame's size, safe area insets, and notch follow the choice, so use_safe_area_insets and use_window_dimensions return realistic values. Switching devices re-lays out the running app without remounting it.
  • Rotate (r): swap width and height.
  • Dark / Light (d): toggle the color scheme. use_color_scheme, use_theme, and {"light": ..., "dark": ...} dynamic colors all follow it live.
  • Back (Esc): the system back action. use_back_handler subscribers on the active screen get the first chance to consume it; otherwise the navigation stack pops, matching the Android hardware back button.
  • Reload app (Shift+R): tear the app down and mount it again from scratch, discarding all state (a full remount, not a Fast Refresh).
  • Console (`): show or hide a log panel with the same lines the terminal prints, plus the page's own errors.

The status dot shows the WebSocket connection. If you stop pn preview the page waits and reconnects when the server returns; the app is mounted fresh on reconnect.

How it works

pn preview sets PN_PLATFORM=web and runs [pythonnative.preview.serve][pythonnative.preview.serve], which:

  1. Starts a [DevServer][pythonnative.devserver.DevServer] on a background thread: HTTP for the page and its assets, WebSocket for the page and for dev clients.
  2. Installs a [WebTransport][pythonnative.bridge.web.WebTransport] as the bridge transport. Transactions the reconciler commits become ["apply", request_id, commit] messages to the page; the page's events come back as ["cb", ...] callbacks. Commits acknowledge exact revisions, and native Yoga WebAssembly layout returns a batch of changed frames.
  3. Runs Python callbacks and tasks on the standard application asyncio loop. The page owns DOM widgets and Yoga WebAssembly layout. Network I/O and Python work execute independently of the browser's UI thread.
  4. When the page connects, tells it the entry module. The page asks the Host module to create a screen, exactly as PNViewController and PNScreenFragment do, and a [NativeScreenHost][pythonnative.hosts.native.NativeScreenHost] mounts your App.
  5. Watches app/ through the dev server and applies each change with [apply_reload][pythonnative.hot_reload.apply_reload], the same function the on-device dev client uses.

Layout is owned by the engine, not the DOM: the flex layout engine computes an absolute frame for every element, and the page positions each element with that frame. Text is measured by the page with the same font it renders, so wrapping and intrinsic sizes match what you see.

Navigation

Root navigators (StackNavigator, TabNavigator, and DrawerNavigator) drive a real stack of screens in the page, the same way they drive UINavigationController and fragments on device. Each pushed screen remains a logical child of the application's shared tree; navigate(...) selects or adds a route, go_back() pops, and covered screens keep their state. Pushes and pops animate with the screen's animation and presentation options.

Each stack draws a navigation bar from the top screen's options, the way the native stacks do: title, header_shown, header_large_title, header_back_visible, header_back_title, header_tint_color, header_style, and header_title_style, with header_left and header_right rendered in the bar. The back button pops through the same path as an iOS swipe back, so a before_remove listener can veto it. A stack at the root of the app replaces the host's own bar, as it does on iOS.

Dev mode and errors

The preview always runs with dev diagnostics on: unknown style keys and duplicate list keys print [PN] WARN messages, hook-order violations raise immediately, and uncaught errors from renders, effects, and event handlers show a full-screen RedBox with the traceback. Fix the code and save; a successful reload clears the overlay. Errors that happen before any screen exists (an import error in app/main.py) print in the terminal and the page console.

Fast Refresh

Saving a .py file under app/ reloads the changed modules and swaps every affected component function into the live tree in place, so the next render reuses the existing hook state. Edits to a component body keep your counters, form values, scroll positions, and navigation stack. When a clean swap isn't possible, the screen remounts instead so you're never stuck with a stale tree. See the Fast Refresh guide for the mechanics; the preview and device builds share the same engine.

Branching on the platform

When the preview is running, [Platform.OS][pythonnative.Platform] is "web":

import pythonnative as pn

pad = pn.Platform.select({"web": 12, "ios": 16, "android": 16, "default": 12})

Platform.select's "native" key matches iOS and Android only; the browser is a development surface, so use an explicit "web" key (or "default") for it. You can also check [Platform.is_web][pythonnative.Platform] or the pythonnative.utils.IS_WEB flag directly.

What's faithful, and what's approximated

The preview is a development tool, optimized for fidelity of layout and logic rather than pixel-perfect platform chrome.

Faithful:

  • Flex layout, sizing, padding, gap, absolute positioning, and safe areas for the chosen device frame.
  • Component lifecycle, hooks, effects, context, error boundaries, Suspense.
  • Navigation (stack push and pop, tabs, drawer, modals) and per-screen state.
  • The async runtime, use_resource, use_query, timers, and state-driven updates.
  • Text wrapping and intrinsic sizing.
  • Colors (hex, rgb(a), named, {"light", "dark"}), border radius, borders, opacity, shadows, and overflow clipping.
  • Animated timing, spring, and decay, run through the same animate protocol the native animators implement; transforms, opacity, and colors update through the page's animation graph.
  • Gestures: the page streams raw pointer events to the Python gesture arbiter. Mobile renderers recognize gestures in Swift and Kotlin.
  • Scrolling, FlatList / SectionList windowing, pull to refresh, and scroll-driven Animated.event bindings.

Approximated or absent:

  • Fonts are the browser's system stack, not San Francisco or Roboto, so glyph metrics differ slightly.
  • Native controls (Switch, Slider, Picker, DatePicker, SegmentedControl) are styled HTML inputs rather than platform widgets.
  • WebView is an <iframe>, which many sites refuse to load in.
  • Image loads whatever the browser can (PNG, JPEG, GIF, WebP, SVG) from files under app/ or URLs; asset resolution otherwise matches device builds.
  • Device APIs: Alert, Clipboard, Linking, Share, Haptics (navigator.vibrate), NetInfo, AppState, and Device are implemented in the page. Everything else (Camera, Location, Notifications, Biometrics, Storage, ...) uses the pure-Python fallbacks in pythonnative.native_modules.fallback: in-memory stores, "unknown" states, and "unavailable" results.
  • Components without a browser implementation render a labeled placeholder box so the layout around them stays truthful.

When the chrome matters, verify on device with pn run.

Sharing the preview

The server binds to all interfaces by default so your own phones can reach it, but it only answers clients that present your dev token. The LAN URLs pn start prints carry the token, and anyone who has one can read your app's source for as long as the token stays the same, so open them only on your own devices. One page drives the app at a time: the newest tab to connect takes over, and the previous one shows a notice until you reload it. Use --host 127.0.0.1 to keep the preview local.

Next steps


Back | FazBrowse Home | New Git URL