Skip to content

Latest commit

Β 

History

57 Commits

Folders and files

Repository files navigation

PyFlutter

Python Flutter Status

PyFlutter lets Python developers describe a user interface in Python and have it rendered by Flutter's native engine. The Python application runs on your machine, a small Rust bridge relays messages, and a Flutter shell draws the widgets and calls native packages.

Status: alpha, development mode only. The full loop (Python β†’ Rust bridge β†’ Flutter shell β†’ callbacks back to Python, with hot reload) works while you develop on a connected device or desktop. Shipping a self-contained app that embeds Python is not implemented yet (see What works today). The project is called PyFlutter in the code and the CLI; a rename (Flarix) is under consideration.


Why PyFlutter?

  • Pythonic API: Component / StatefulComponent, Signals, or Qt-style fluent widgets (add_widget(), .clicked.connect()).
  • Flutter's real renderer: Material 3 widgets drawn by Flutter, not re-implemented.
  • Compact binary protocol: Protobuf frames between Python, Rust and Dart; incremental patches instead of resending the whole tree when only properties change.
  • Packages on demand: native Flutter packages are mapped to Python classes by hand-written shims (see Native packages); you only add the ones you use.

Architecture

graph LR
    subgraph Python ["Python application (your machine)"]
        App["App / Component"] --> State["Signals / State"]
        State --> Render["Tree resolve + diff"]
    end

    subgraph Bridge ["Rust bridge (subprocess)"]
        Render -->|"Protobuf / JSON patch frames over stdio"| Rust["Relay"]
    end

    subgraph Flutter ["Flutter shell"]
        Rust -->|"local TCP socket"| Dart["Widget builder"]
        Dart --> Plugins["Per-package shims -> real Flutter packages"]
    end
Loading

Native package calls travel the same way: Python sends {plugin, method, args}, a hand-written Dart shim calls the real package, and the answer comes back to the waiting Python call.


What works today

Area State
pyflutter run on Android / desktop devices with hot reload (r) and hot restart (R) Works
Material widgets, reactive state, forms, navigation stack, SnackBar / dialogs Works (coverage is partial, see PYFLUTTER_VISION.md)
Single UI thread: callbacks and builds never overlap, many updates cost one frame (pf.run_on_ui for your own threads) Works
Incremental tree patches, reconnection resync, session token on the local socket Works
Native packages (22 catalog plugins, see below) Real shims calling the real Flutter packages; installed per project with pyflutter add. They compile (flutter analyze) individually and all together; they still need testing on devices
hive boxes and sqflite Implemented in Python (JSON files, sqlite3), no Flutter package involved
Standalone app that embeds Python (APK / IPA / desktop bundle) Not implemented: pyflutter build compiles the Flutter shell, but no Python interpreter is embedded yet
Web and iOS Not supported yet (the shell imports dart:io / dart:ffi; iOS needs the embedded runtime)
Installation with pip install outside a repository clone Not available yet: the CLI needs the dart_runtime/ and rust_bridge/ folders of this repository

AUDIT_BUGS.md lists every known bug and missing piece with the planned fix.


πŸš€ Quickstart

1. Installation

git clone https://github.com/PerfectWin7777/pyflutter.git
cd pyflutter/py_framework
pip install -e .

2. Create Your First App (main.py)

import pyflutter as pf

class CounterApp(pf.Component):
    def __init__(self):
        super().__init__()
        self.count = 0

    def increment(self):
        self.count += 1
        self.update()  # Triggers instant reactive UI update

    def build(self):
        return pf.Scaffold(
            app_bar=pf.AppBar(
                title=pf.Text("Counter", color=pf.Colors.WHITE),
                background_color="#1877F2",
            ),
            body=pf.Center(
                pf.Card(
                    pf.Column([
                        pf.Text("Compteur", font_size=14, color=pf.Colors.GREY),
                        pf.SizedBox(height=8),
                        pf.Text(str(self.count), font_size=48, font_weight="bold"),
                        pf.SizedBox(height=16),
                        pf.ElevatedButton("+1", on_click=self.increment),
                    ], cross_axis_alignment="center"),
                    padding=24,
                    border_radius=16,
                )
            ),
            floating_action_button=pf.FloatingActionButton(
                icon=pf.Icons.ADD,
                on_click=self.increment,
            ),
        )

if __name__ == "__main__":
    # Run interactively on a connected device or desktop (development mode):
    pf.run(CounterApp())

3. Run

# Run interactively with hot reload (needs the Rust bridge built once: see Requirements)
python main.py            # or: pyflutter run main.py

# Flutter shell build (does NOT embed Python yet)
pyflutter build apk --release
pyflutter build windows --release

Requirements

  • Python 3.10+
  • Flutter SDK (3.32 or newer) in PATH, and a device, emulator or desktop target (pyflutter devices)
  • Rust toolchain and protoc to build the bridge once: cargo build --manifest-path rust_bridge/Cargo.toml
  • Android: Android SDK / NDK as required by Flutter

Showcase examples

See examples/:

  • examples/counter: Material 3 counter with a FloatingActionButton.
  • examples/facebook_feed: social feed with dataclasses, like toggling, bottom navigation.
  • examples/pyshop: e-commerce showcase (Qt-style layouts, live search, reactive cart, SnackBars, url_launcher).
cd examples/counter
python main.py

State lifecycle

A StatefulComponent keeps its State between frames. The state is matched by where the widget sits in the tree (widget types and positions from the root, like Flutter), never by source line, so editing the code does not lose it. Rules:

  • State.dispose() is called once when the widget leaves the tree (a removed branch, a popped page) and on a hot restart: stop timers, threads and subscriptions there.
  • A page covered by another one in the Navigator keeps its states until it is popped.
  • In a list that can be reordered, filtered or inserted into, give each stateful widget a key=: its state then follows the key inside its parent instead of the position.

Native packages

PyFlutter does not try to expose every pub.dev package automatically, and it does not put every package in every app. The model is:

  1. Each supported package has an entry in dart_runtime/plugin_catalog/: a plugin.yaml (package, version, native settings) and a hand-written Dart shim that calls the real package API.
  2. A Python module pyflutter.plugins.<package> mirrors the package's classes and methods.
  3. A project lists the plugins it uses in pyflutter.yaml (plugins:); pyflutter add <name> edits that list and wires the shim, the pubspec.yaml dependency and the native Android settings. Only those packages are compiled into the app.
pyflutter plugin list                 # catalog and what this project uses
pyflutter add local_auth              # install a catalog plugin
pyflutter add some_other_package      # no shim yet: adds the Flutter dependency, then
pyflutter plugin new some_other_package   # scaffolds the shim, Python module and test to fill from the package docs
pyflutter remove local_auth

Each project works on its own copy of the Flutter runtime, created in <project>/.pyflutter/runtime/ the first time you run, build or add (add .pyflutter/ to your .gitignore; pyflutter create does). dart_runtime/ in this repository is only the template, so installing plugins or declaring permissions never touches it. pyflutter.yaml is the source of truth: plugins:, dependencies.flutter (packages without a shim, saved with the version Flutter resolved) and permissions:. Removing a plugin or a permission removes it from the Android manifest and the iOS plist too.

A plugin that is not installed fails with a clear error (Plugin "x" is not installed in this runtime. Install it with: pyflutter add x). When a Flutter runtime is connected, a plugin error or timeout raises PluginError / PluginTimeoutError; it is never replaced by simulated data, and a missing or malformed answer never reads as a success.

Plugin (pub.dev package) Python module Notes
shared_preferences, path_provider, device_info_plus, url_launcher, file_picker pyflutter.plugins.<name> Key-value storage, system folders, device info, links/phone/email, file and folder dialogs
connectivity_plus, share_plus, image_picker, audioplayers pyflutter.plugins.<name> Network state, share sheet, gallery/camera picking, audio playback
flutter_local_notifications pyflutter.plugins.flutter_local_notifications Show / cancel notifications (needs core library desugaring, applied automatically)
local_auth, permission_handler, flutter_secure_storage pyflutter.plugins.<name> Biometrics, runtime permissions, encrypted storage
camera, video_player, chewie, webview_flutter pyflutter.plugins.<name> Real widgets CameraPreview, VideoPlayer, Chewie, WebView
syncfusion_flutter_pdfviewer, pdfx, flutter_pdfview, printing pyflutter.plugins.<name> PDF viewers (SfPdfViewer, PdfView, PDFView), page rendering, print and share. Syncfusion needs its own licence
hive, sqflite pyflutter.plugins.hive, pyflutter.plugins.sqflite Pure Python (JSON boxes, sqlite3), no Flutter package

Without a connected Flutter runtime (unit tests, scripts) plugin calls use a small local simulation so code can be tested offline. The first call of each simulated plugin logs an [offline] ... is simulated warning, and the security plugins (local_auth, permission_handler, flutter_secure_storage) refuse instead of faking an answer unless PYFLUTTER_ALLOW_INSECURE_MOCKS=1 is set (meant for tests). Calls that wait for a person (pickers, permission prompts, authentication, sharing) wait up to 120 s, the others 3 s.

To check a catalog change: python tools/verify_catalog.py (needs the Flutter SDK) installs every plugin into a temporary copy of the runtime, runs flutter pub get and flutter analyze, then installs them all together to catch version conflicts.


CLI reference

Command Arguments / flags Description
pyflutter run [entrypoint] [-d <device>] [-p <port>] [--attach] Run the app interactively on a device or desktop
pyflutter devices List devices and emulators detected by Flutter
pyflutter build [target] [--debug|--release|--profile] [--split-per-abi] Build the Flutter shell (default: apk, debug). No embedded Python yet
pyflutter create <name> Scaffold a project
pyflutter init Initialise a project in the current directory
pyflutter sync Sync pyflutter.yaml permissions to the Android manifest and iOS plist
pyflutter add <package> Install a catalog plugin (or add a Flutter package that has no shim yet)
pyflutter remove <package> Remove a plugin / package
pyflutter plugin list | new <package> List the plugin catalog, or scaffold the mapping of a new package

Build targets: apk, appbundle, windows, linux, macos, web, ipa (web and ipa are not supported yet, see above).


Tests and checks

pip install -e "py_framework[dev]"      # the framework, pytest and ruff
ruff check py_framework                 # lint (rules in py_framework/pyproject.toml)
cd py_framework && python -m pytest -q  # the Python test-suite (unknown widget props raise in the tests)
python tools/check_contract.py          # Python <-> contract <-> Dart props consistency

The Rust relay tests (tests/test_bridge_relay.py) run the real bridge binary: build it first (cd rust_bridge && cargo build) or they are skipped; PYFLUTTER_REQUIRE_BRIDGE=1 turns a missing binary into a failure. The Dart runtime has a small unit test (cd dart_runtime && flutter test) and must pass flutter analyze; python tools/verify_catalog.py [--together] installs the plugins of the catalog into a temporary runtime and analyses them (needs the Flutter SDK).

The GitHub Actions workflow (.github/workflows/ci.yml) runs all of this: Python 3.10 to 3.13 on Linux (and 3.12 on Windows and macOS), the oldest supported dependencies, cargo clippy -D warnings and the relay tests, flutter analyze and flutter test, the contract check and the plugin catalog (every plugin, weekly).


Roadmap

  • Duplex reactive bridge (Protobuf frames, Rust relay, Flutter shell) in development mode
  • Material 3 widget catalog (partial coverage)
  • Callback lifecycle (sweep, pinned one-shot callbacks), deterministic state keys
  • Hot reload / hot restart, reconnection resync
  • Packages on demand (pyflutter add with a plugin catalog)
  • Per-project copy of the Flutter runtime (<project>/.pyflutter/runtime/)
  • Embedded Python interpreter for standalone builds
  • Pip distribution with a prebuilt bridge
  • Hot reload of every project module, file watcher
  • Documentation site

The detailed, ordered plan is in AUDIT_BUGS.md.


License

MIT (the LICENSE file is still to be added).

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages