From flet
Expert knowledge for creating Flet extension packages (Service Controls and UI Controls). Covers Python/Dart integration, type mapping, events, compound widgets, @control/@value decorators, Prop descriptor, project configuration, publishing, and awareness of companion extensions (flet-video screenshot + configurable controls, flet-audio AudioRecorder PCM16 streaming). Flet 0.85.x+.
How this skill is triggered — by the user, by Claude, or both
Slash command
/flet:flet-extensionThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Flet 0.85.x | Validated against real production extensions (flet-onesignal, flet-vibration, flet-video, flet-audio)
Flet 0.85.x | Validated against real production extensions (flet-onesignal, flet-vibration, flet-video, flet-audio)
flet-video — Configurable player controls (show/hide play/pause, seek bar, fullscreen, etc.) and an async take_screenshot() method on the player controlflet-audio — New AudioRecorder with PCM16 streaming and direct upload to backendpage.take_screenshot() / page.take_animation() and the new top-level Screenshot control (flet.controls.core.screenshot.Screenshot)ft.Router outlets — if your extension renders a navigator/host, expose hooks for use_route_params / use_view_path interopft.use_dialog — for UI controls that surface dialogs, prefer this hook over imperative page.show_dialogDragTargetEvent.x/y/offset properties (replaced by local_position / global_position in 0.85.0) affect any extension that re-emits or wraps drag targets — update before 0.88.0.| Aspect | ft.Service | ft.LayoutControl |
|---|---|---|
| Has UI | No | Yes |
| Base class (Python) | ft.Service | ft.LayoutControl |
| Base class (Dart) | FletService | StatefulWidget |
| Add to app | page.services.append() | page.add() / controls list |
| Extension handler | createService(control) | createWidget(control) |
| width/height | No | Yes (inherited) |
| expand | No | Yes (inherited) |
| Examples | Push notifications, audio, geolocation | WebView, video player, map |
flet-my-extension/
├── pyproject.toml # Python package config
├── README.md
├── CHANGELOG.md
├── LICENSE
├── tests/
│ └── test_my_extension.py
├── examples/
│ └── flet_my_extension_example/
│ ├── pyproject.toml # Example app config
│ └── src/
│ └── main.py
└── src/
├── flet_my_extension/ # Python code
│ ├── __init__.py # Public exports
│ ├── my_service.py # Main control (ft.Service or ft.LayoutControl)
│ ├── sub_module_a.py # Sub-module A (pure Python class)
│ ├── sub_module_b.py # Sub-module B (pure Python class)
│ └── types.py # Enums, events, dataclasses
└── flutter/flet_my_extension/ # Flutter/Dart code
├── pubspec.yaml
└── lib/
├── flet_my_extension.dart # Only exports Extension
└── src/
├── extension.dart # Extension registration
└── my_service.dart # Flutter implementation
// lib/flet_my_extension.dart
library flet_my_extension;
export 'src/extension.dart' show Extension;
// lib/src/extension.dart — FOR SERVICES
import 'package:flet/flet.dart';
import 'my_service.dart';
class Extension extends FletExtension {
@override
void ensureInitialized() {
debugPrint("MyPlugin: ensureInitialized");
}
@override
FletService? createService(Control control) {
switch (control.type) {
case "MyService": // Must match @ft.control("MyService")
return MyServiceImpl(control: control);
default:
return null;
}
}
}
// lib/src/extension.dart — FOR UI CONTROLS
import 'package:flet/flet.dart';
import 'my_widget.dart';
class Extension extends FletExtension {
@override
Widget? createWidget(Control control) {
switch (control.type) {
case "MyWidget": // Must match @ft.control("MyWidget")
return MyWidgetWidget(control: control);
default:
return null;
}
}
}
Critical rule: control.type must be identical to the string in @ft.control("MyService").
flet-pkg is a CLI tool that scaffolds Flet extension packages with auto-generated code from Flutter package analysis.
# Auto-analyze a Flutter package from pub.dev and generate code
flet-pkg create my-extension --analyze
# Skip analysis (manual implementation)
flet-pkg create my-extension --no-analyze
# Use a local Dart package instead of pub.dev
flet-pkg create my-extension --local-package /path/to/dart/pkg
# Choose extension type
flet-pkg create my-extension --type service # ft.Service
flet-pkg create my-extension --type ui_control # ft.LayoutControl
~/.cache/flet-pkg/)GenerationPlan with Python↔Dart mappings, type conversions, invoke keys# src/flet_my_extension/my_service.py
from dataclasses import field
from typing import Any, Optional
import flet as ft
from flet_my_extension.sub_module_a import SubModuleA
from flet_my_extension.types import (
MyEvent,
MyLogLevel,
ErrorEvent,
)
@ft.control("MyService") # Name must correspond to Flutter side
class MyService(ft.Service):
"""
Integration service for XYZ SDK.
Add to `page.services` — do NOT use `page.overlay`.
Example:
```python
service = MyService(app_id="your-id")
page.services.append(service)
await service.do_something("param")
```
"""
# ── Public properties (sent to Flutter) ─────────────────────────────
app_id: str = ""
log_level: Optional[MyLogLevel] = None
require_consent: bool = False
# ── Public events ────────────────────────────────────────────────────
on_event: Optional[ft.EventHandler[MyEvent]] = None
on_error: Optional[ft.EventHandler[ErrorEvent]] = None
# ── Internal fields (NOT sent to Flutter) ────────────────────────────
_sub_a: SubModuleA = field(default=None, init=False, metadata={"skip": True})
# ── Lifecycle ────────────────────────────────────────────────────────
def init(self):
"""Called when the control is mounted in the Flutter tree."""
super().init()
self._sub_a = SubModuleA(self)
# ── Properties (lazy init for robustness) ────────────────────────────
@property
def sub_a(self) -> SubModuleA:
if self._sub_a is None:
self._sub_a = SubModuleA(self)
return self._sub_a
# ── Public methods (call Flutter via _invoke_method) ─────────────────
async def login(self, user_id: str) -> None:
await self._invoke_method("login", {"user_id": user_id})
async def logout(self) -> None:
await self._invoke_method("logout")
async def get_status(self, timeout: float = 10.0) -> bool:
result = await self._invoke_method("get_status", timeout=timeout)
return result == "true"
async def get_tags(self) -> dict:
import json
result = await self._invoke_method("get_tags")
return json.loads(result) if result else {}
# ── Platform validation ──────────────────────────────────────────────
def _is_supported_platform(self) -> bool:
"""Validate before calling methods (NOT in before_update)."""
if not self.page:
return False
return self.page.platform in (
ft.PagePlatform.ANDROID,
ft.PagePlatform.IOS,
)
async def _invoke_method(
self,
method_name: str,
arguments: Optional[dict[str, Any]] = None,
timeout: Optional[float] = None,
) -> Any:
"""Override to add platform validation."""
if not self._is_supported_platform():
platform = self.page.platform.value if self.page else "unknown"
raise ft.FletUnsupportedPlatformException(
f"MyService only supports Android and iOS. "
f"Current platform: {platform}."
)
effective_timeout = timeout if timeout is not None else 25.0
return await super()._invoke_method(
method_name=method_name,
arguments=arguments or {},
timeout=effective_timeout,
)
# src/flet_my_extension/sub_module_a.py
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from flet_my_extension.my_service import MyService
class SubModuleA:
"""
Sub-module A — Namespace for functionality.
Does not inherit from ft.Service. Delegates _invoke_method to parent service.
Mirrors the modular architecture of third-party SDKs.
"""
def __init__(self, service: "MyService"):
self._service = service
async def do_something(self, param: str) -> Optional[str]:
result = await self._service._invoke_method(
"sub_a_do_something",
{"param": param},
)
return result if result else None
async def get_value(self, timeout: float = 25) -> bool:
result = await self._service._invoke_method(
"sub_a_get_value",
timeout=timeout,
)
return result == "true"
// lib/src/my_service.dart
import 'dart:convert';
import 'package:flet/flet.dart';
import 'package:flutter/foundation.dart';
import 'package:third_party_sdk/third_party_sdk.dart';
class MyServiceImpl extends FletService {
MyServiceImpl({required super.control});
bool _initialized = false;
bool _listenersSetup = false;
// ── Lifecycle ─────────────────────────────────────────────────────────
@override
void init() {
super.init();
debugPrint("MyService.init: type=${control.type}");
// Register invoke method handler BEFORE initializing
control.addInvokeMethodListener(_onInvokeMethod);
// Initialize SDK with current control properties
_initializeSdk();
}
@override
Future<void> update() async {
// Called when Python properties change
final appId = control.getString("app_id");
if (appId != null && !_initialized) {
_initializeSdk();
}
}
@override
void dispose() {
// Clean up listeners and resources on unmount
super.dispose();
}
// ── SDK Initialization ────────────────────────────────────────────────
void _initializeSdk() {
final appId = control.getString("app_id");
if (appId == null || appId.isEmpty) {
debugPrint("MyService: app_id not provided");
return;
}
try {
// Configure BEFORE initializing (read SDK docs)
final logLevel = control.getString("log_level");
if (logLevel != null) {
_configureLog(logLevel);
}
final requireConsent = control.getBool("require_consent", false)!;
if (requireConsent) {
Sdk.consentRequired(true);
}
Sdk.initialize(appId);
_initialized = true;
// Set up listeners AFTER initialization
_setupListeners();
} catch (error, stackTrace) {
_handleError("_initializeSdk", error, stackTrace);
}
}
// ── SDK Listeners → Python Events ─────────────────────────────────────
void _setupListeners() {
if (_listenersSetup) return; // Guard against duplicates
_listenersSetup = true;
Sdk.addListener((event) {
try {
control.triggerEvent("event", {
"data": event.data,
"success": true,
});
} catch (error, stackTrace) {
_handleError("event_listener", error, stackTrace);
}
});
debugPrint("MyService._setupListeners: ready");
}
// ── Invoke Method Handler (Python → Dart) ─────────────────────────────
Future<dynamic> _onInvokeMethod(String methodName, dynamic args) async {
try {
// IMPORTANT: Convert args to Map<String, dynamic>
// Can arrive as _Map<dynamic, dynamic> from Flet
Map<String, dynamic> arguments = {};
if (args != null && args is Map) {
arguments = Map<String, dynamic>.from(args);
}
debugPrint("MyService._onInvokeMethod: method=$methodName, args=$arguments");
// Use switch expression (Dart 3+)
return switch (methodName) {
"login" => await _login(arguments),
"logout" => await _logout(),
"get_status" => _getStatus(),
"get_tags" => await _getTags(),
"sub_a_do_something" => await _subADoSomething(arguments),
"sub_a_get_value" => _subAGetValue(),
_ => throw Exception("Unknown method: $methodName"),
};
} catch (error, stackTrace) {
_handleError(methodName, error, stackTrace);
return null;
}
}
// ── Method Implementations ────────────────────────────────────────────
Future<String?> _login(Map<String, dynamic> args) async {
final userId = args["user_id"] as String?;
if (userId != null) {
await Sdk.login(userId);
}
return null; // Return null for void methods
}
Future<String?> _logout() async {
await Sdk.logout();
return null;
}
String _getStatus() {
// Return "true"/"false" for booleans
// Python does: result == "true"
return Sdk.getStatus().toString();
}
Future<String?> _getTags() async {
final tags = await Sdk.getTags();
// Return as JSON string — Python does json.loads()
return jsonEncode(tags);
}
Future<String?> _subADoSomething(Map<String, dynamic> args) async {
final param = args["param"] as String?;
if (param != null) {
await Sdk.doSomething(param);
}
return null;
}
String _subAGetValue() {
return Sdk.getValue().toString();
}
// ── Error Handling ────────────────────────────────────────────────────
void _handleError(String method, Object error, StackTrace stackTrace) {
debugPrint("MyService ERROR in $method: $error");
FlutterError.reportError(FlutterErrorDetails(
exception: error,
stack: stackTrace,
library: 'flet_my_extension',
context: ErrorDescription('while executing method "$method"'),
));
// Trigger error event for Python
control.triggerEvent("error", {
"method": method,
"message": error.toString(),
"stack_trace": stackTrace.toString(),
});
}
// ── Configuration Helpers ─────────────────────────────────────────────
void _configureLog(String level) {
final sdkLevel = switch (level.toLowerCase()) {
"none" => LogLevel.none,
"debug" => LogLevel.debug,
"info" => LogLevel.info,
"warn" => LogLevel.warn,
"error" => LogLevel.error,
_ => LogLevel.warn,
};
Sdk.setLogLevel(sdkLevel);
}
}
# src/flet_my_widget/my_widget.py
from typing import Optional
import flet as ft
from flet_my_widget.types import MyWidgetEvent, MyWidgetConfig
@ft.control("MyWidget") # Name for Flutter registration
class MyWidget(ft.LayoutControl):
"""
Visual widget for displaying custom content.
Inherits from ft.LayoutControl (has width, height, expand, etc.)
Example:
```python
widget = MyWidget(
url="https://example.com",
width=400,
height=300,
on_load=lambda e: print("Loaded!"),
)
page.add(widget)
```
"""
# Properties (sent to Flutter via automatic serialization)
url: str = ""
"""URL or content source."""
auto_play: bool = False
"""If True, starts automatically."""
config: Optional[MyWidgetConfig] = None
"""Advanced configuration."""
# Events
on_load: Optional[ft.EventHandler[MyWidgetEvent]] = None
"""Called when content finishes loading."""
on_error: Optional[ft.EventHandler[ft.ControlEvent]] = None
"""Called when an error occurs."""
# Methods invoked on Flutter
async def play(self) -> None:
await self._invoke_method("play")
async def pause(self) -> None:
await self._invoke_method("pause")
async def get_duration(self, timeout: float = 10) -> Optional[float]:
result = await self._invoke_method("get_duration", timeout=timeout)
return float(result) if result else None
async def seek(self, position_seconds: float) -> None:
await self._invoke_method("seek", {"position": position_seconds})
# Platform validation (optional for UI controls)
def before_update(self):
super().before_update()
if self.page and self.page.web and not self._supports_web():
raise ft.FletUnsupportedPlatformException("MyWidget doesn't support web.")
def _supports_web(self) -> bool:
return False
All inherited automatically by any UI Control:
| Property | Type | Description |
|---|---|---|
transform | Transform | Generic Matrix4 transform (translate, rotate, scale, skew) |
aspect_ratio | Number | Automatic width/height ratio |
animate_size | AnimationValue | Implicit size animation |
on_size_change | EventHandler[LayoutSizeChangeEvent] | Event when dimensions change |
size_change_interval | int | Sampling interval in ms (default: 10) |
@ft.control("MyWidget")
class MyWidget(ft.LayoutControl):
url: str = ""
# Can now use Matrix4 transforms (inherited):
# widget = MyWidget(url="...", transform=Matrix4.rotation_z(0.5))
# widget = MyWidget(url="...", on_size_change=handle_resize)
# isolated=True: excludes control from parent updates
@ft.control("MyWidget", isolated=True)
class MyWidget(ft.LayoutControl):
...
# post_init_args: for controls with InitVar
@ft.control("MyWidget", post_init_args=1)
class MyWidget(ft.LayoutControl):
...
// lib/src/my_widget.dart
import 'package:flet/flet.dart';
import 'package:flutter/widgets.dart';
import 'package:third_party_sdk/third_party_sdk.dart';
class MyWidgetWidget extends StatefulWidget {
final Control control;
const MyWidgetWidget({super.key, required this.control});
@override
State<MyWidgetWidget> createState() => _MyWidgetWidgetState();
}
class _MyWidgetWidgetState extends State<MyWidgetWidget> {
late final SdkController _controller;
@override
void initState() {
super.initState();
_controller = SdkController();
// Register handler for Python method invocations
widget.control.addInvokeMethodListener(_onInvokeMethod);
// Setup listeners
_setupListeners();
}
@override
void didUpdateWidget(MyWidgetWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// React to changes in Python properties
final url = widget.control.getString("url");
if (url != null) {
_controller.loadUrl(url);
}
}
void _setupListeners() {
_controller.onLoad = () {
widget.control.triggerEvent("load", {
"status": "ready",
"duration": _controller.duration,
});
};
_controller.onError = (error) {
widget.control.triggerEvent("error", {
"status": "error",
"message": error,
});
};
}
Future<dynamic> _onInvokeMethod(String name, dynamic args) async {
Map<String, dynamic> arguments = {};
if (args != null && args is Map) {
arguments = Map<String, dynamic>.from(args);
}
return switch (name) {
"play" => _controller.play(),
"pause" => _controller.pause(),
"get_duration" => _controller.duration?.toString(),
"seek" => _controller.seek(
Duration(
milliseconds: ((arguments["position"] as double) * 1000).toInt(),
)
),
_ => throw Exception("Unknown method: $name"),
};
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: widget.control.getDouble("width") ?? 0,
minHeight: widget.control.getDouble("height") ?? 0,
),
child: SdkWidget(controller: _controller),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Python (ft.Service) Flutter (FletService)
───────────────────── ─────────────────────
Properties (fields) →→→→→→ control.getString("field")
control.getBool("field", default)
await _invoke_method() →→→→→→ control.addInvokeMethodListener()
↑ ↓
returns value ←←←←←← return "value"
ft.EventHandler[Type] ←←←←←← control.triggerEvent("name", data)
# Python declaration
@ft.control("MyService")
class MyService(ft.Service):
app_id: str = ""
log_level: Optional[MyLogLevel] = None
require_consent: bool = False
// Dart reading
final appId = control.getString("app_id");
final logLevel = control.getString("log_level");
final requireConsent = control.getBool("require_consent", false)!;
Enums are serialized as their .value (string):
# Python
class MyLogLevel(Enum):
DEBUG = "debug"
# Dart receives: "debug"
final levelStr = control.getString("log_level"); // "debug"
# Python — basic call (no return)
await self._invoke_method("logout")
# With arguments
await self._invoke_method("login", {"user_id": user_id})
# With return + custom timeout
result = await self._invoke_method("get_status", timeout=10.0)
return result == "true"
# Complex data return (JSON)
result = await self._invoke_method("get_tags")
return json.loads(result) if result else {}
// Dart — handler
control.addInvokeMethodListener(_onInvokeMethod);
Future<dynamic> _onInvokeMethod(String methodName, dynamic args) async {
// ALWAYS convert args
Map<String, dynamic> arguments = {};
if (args != null && args is Map) {
arguments = Map<String, dynamic>.from(args);
}
return switch (methodName) {
"logout" => await _logout(),
"login" => await _login(arguments),
"get_status" => _getStatus(),
"get_tags" => await _getTags(),
_ => throw Exception("Unknown method: $methodName"),
};
}
// Dart — trigger event
control.triggerEvent("permission_change", {
"permission": true,
});
// Error event (standard pattern)
control.triggerEvent("error", {
"method": "method_name",
"message": error.toString(),
"stack_trace": stackTrace.toString(),
});
# Python — receive event
@dataclass
class PermissionChangeEvent(ft.Event["MyService"]):
permission: bool = False
@ft.control("MyService")
class MyService(ft.Service):
on_permission_change: Optional[ft.EventHandler[PermissionChangeEvent]] = None
# Usage
service = MyService(
on_permission_change=lambda e: print(f"Permission: {e.permission}"),
)
Event name mapping:
Dart triggerEvent(name, ...) | Python on_name: EventHandler |
|---|---|
"permission_change" | on_permission_change |
"notification_click" | on_notification_click |
"error" | on_error |
Rule: triggerEvent("snake_case_name") → on_snake_case_name
from dataclasses import field
@ft.control("MyService")
class MyService(ft.Service):
# Normal field → sent to Flutter
app_id: str = ""
# Internal field → NOT sent to Flutter
_sub_module: SubModule = field(
default=None,
init=False,
metadata={"skip": True} # This excludes it from serialization
)
# Default: 25 seconds
await self._invoke_method("login", {"user_id": uid})
# Custom timeout for fast operations
result = await self._invoke_method("get_permission", timeout=10.0)
# Custom timeout for slow operations (e.g., permission request with UI)
result = await self._invoke_method(
"request_permission",
{"fallback_to_settings": True},
timeout=30.0,
)
| Side | Convention |
|---|---|
| Python: property field | snake_case (app_id, log_level) |
| Python: method | snake_case async (login, get_status) |
| Python: event | on_snake_case (on_permission_change) |
Dart: triggerEvent(name) | "snake_case" ("permission_change") |
Dart: _onInvokeMethod(name) | "snake_case" ("login", "get_status") |
@ft.control("Name") | PascalCase ("MyService", "OneSignal") |
Dart: control.type switch | Same PascalCase ("MyService") |
| Dart Type | Python Type (Service) | Python Type (UI Control) |
|---|---|---|
String | str | str |
bool | bool | bool |
int | int | int |
double | float | ft.Number |
num | float | ft.Number |
void | None | None |
dynamic | Any | Any |
Object | Any | Any |
Color | str | ft.Color |
Duration | int | int |
DateTime | str | str |
Uint8List | bytes | bytes |
Uri | str | str |
| Dart Generic | Python Type |
|---|---|
List<String> | list[str] |
List<int> | list[int] |
Set<String> | set[str] |
Map<String, dynamic> | dict[str, Any] |
Map<String, String> | dict[str, str] |
Future<T> | Unwrapped to T |
Iterable<T> | list[T] |
| Dart | Python |
|---|---|
String? | str | None |
int? | int | None |
List<String>? | list[str] | None |
These use native Flet types for richer UI integration:
| Dart Type | Flet Python Type | Dart Getter |
|---|---|---|
Alignment | ft.Alignment | control.getAlignment("name") |
BoxFit | ft.BoxFit | control.getBoxFit("name") |
Color | ft.Color | control.getString("name") |
double / num | ft.Number | control.getDouble("name") |
Widget | ft.Control | buildWidget("name") |
TextStyle | ft.TextStyle | control.getTextStyle("name", Theme.of(context)) |
Rect | ft.Rect | control.getRect("name") |
Key | skipped | N/A |
Common Flutter enums are mapped to str for serialization:
| Dart Enum | Python Type |
|---|---|
TextDirection | str |
Axis | str |
MainAxisAlignment | str |
CrossAxisAlignment | str |
TextAlign | str |
FontWeight | str |
BoxFit | str (service) / ft.BoxFit (UI) |
Alignment | str (service) / ft.Alignment (UI) |
Clip | str |
Curve | str |
Brightness | str |
| Python Type | Dart Return | Python Conversion |
|---|---|---|
None | return null | — |
bool | return result.toString() → "true"/"false" | result == "true" |
str | return value | result if result else None |
dict | return jsonEncode(map) | json.loads(result) |
int/float | return value.toString() | int(result) / float(result) |
from enum import Enum
class MyLogLevel(Enum):
"""Log levels for the SDK."""
NONE = "none"
DEBUG = "debug"
INFO = "info"
WARN = "warn"
ERROR = "error"
Serialization: Sub-modules send param.value (the string), not the raw enum object.
from dataclasses import dataclass
from typing import Optional
import flet as ft
@dataclass
class MyEvent(ft.Event["MyService"]):
"""Event triggered by the SDK.
The type parameter of ft.Event is the parent control class.
"""
data: str = ""
success: bool = False
@dataclass
class ErrorEvent(ft.Event["MyService"]):
"""Standard error event."""
method: Optional[str] = None
message: Optional[str] = None
stack_trace: Optional[str] = None
Every extension should include an ErrorEvent and on_error handler:
# types.py
@dataclass
class ErrorEvent(ft.Event["MyService"]):
method: Optional[str] = None
message: Optional[str] = None
stack_trace: Optional[str] = None
# my_service.py
@ft.control("MyService")
class MyService(ft.Service):
on_error: Optional[ft.EventHandler[ErrorEvent]] = None
// my_service.dart
void _handleError(String method, Object error, StackTrace stackTrace) {
debugPrint("MyService ERROR in $method: $error");
FlutterError.reportError(FlutterErrorDetails(
exception: error,
stack: stackTrace,
library: 'flet_my_extension',
context: ErrorDescription('while executing method "$method"'),
));
control.triggerEvent("error", {
"method": method,
"message": error.toString(),
"stack_trace": stackTrace.toString(),
});
}
[project]
name = "flet-my-extension"
version = "0.1.0"
description = "My Flet extension."
requires-python = ">=3.10"
readme = "README.md"
authors = [{ name = "Your Name", email = "[email protected]" }]
license = "MIT"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = ["flet>=0.80.0"]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
# CRITICAL: Includes Flutter/Dart code in the Python wheel
[tool.setuptools.package-data]
"flutter.flet_my_extension" = ["**/*"]
[dependency-groups]
dev = [
"flet[all]>=0.80.0",
"pytest>=7.2.0",
"pytest-cov>=7.0.0",
]
[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src", "tests"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
Why [tool.setuptools.package-data] is critical: The Flet build system locates Dart code inside the installed Python package. Without this, the wheel won't contain the Dart files and flet build will fail.
name: flet_my_extension
description: "My Flet extension."
version: 0.1.0
publish_to: none # Do NOT publish to pub.dev separately
environment:
sdk: ^3.5.2
flutter: ">=3.29.0"
dependencies:
flutter:
sdk: flutter
flet: ^0.80.5
third_party_sdk: ^5.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
[project]
name = "flet-my-extension-example"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"flet>=0.80.0",
"flet-my-extension @ file://../../../", # Local dev dependency
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
# CRITICAL for src/ layout
[tool.flet.app]
path = "src"
# pyproject.toml of the APP (not the extension)
[tool.flet.extensions]
flet_my_extension = "flet_my_extension.Extension"
When a Flutter widget accepts typed sub-widget parameters (e.g., ActionPane?), these become sub-controls:
# Parent control
@ft.control("MySlider")
class MySlider(ft.LayoutControl):
thumb: Optional["MyThumb"] = None # Sub-control
track: Optional["MyTrack"] = None # Sub-control
# Sub-control
@ft.control("MyThumb")
class MyThumb(ft.Control):
color: str = ""
size: float = 20.0
List patterns (List<BarItem>) are also supported:
items: list["BarItem"] = field(default_factory=list)
For complex SDKs with multiple namespaces (like OneSignal's User, Notifications, Debug):
class User:
"""User namespace — pure Python class, delegates to parent service."""
def __init__(self, service: "MyService"):
self._service = service
async def set_tag(self, key: str, value: str) -> None:
await self._service._invoke_method(
"user_set_tag", {"key": key, "value": value}
)
async def get_tags(self) -> dict:
import json
result = await self._service._invoke_method("user_get_tags")
return json.loads(result) if result else {}
Dart Stream<T> get onXxx patterns are detected and converted to event handlers:
// Dart
Stream<PermissionState> get onPermissionChange => ...
# Python
on_permission_change: Optional[ft.EventHandler[PermissionChangeEvent]] = None
# WRONG: _set_attr in __init__ (pre-0.80.x pattern)
class MyService(ft.Service):
def __init__(self, app_id, **kwargs):
super().__init__(**kwargs)
self._set_attr("appId", app_id) # AttributeError in 0.80.x
# CORRECT: dataclass fields
@ft.control("MyService")
class MyService(ft.Service):
app_id: str = ""
# WRONG: @ft.control without string name
@ft.control
class MyService(ft.Service): ... # ValueError: must have type_name
# CORRECT:
@ft.control("MyService")
class MyService(ft.Service): ...
# WRONG: import from flet.core (structure changed)
from flet.core.control_event import ControlEvent # ModuleNotFoundError
# CORRECT:
import flet as ft # ft.ControlEvent, ft.EventHandler, ft.Event
# WRONG: add Service to page.add() or overlay
page.add(my_service) # Service has no UI
page.overlay.append(my_service) # Not a dialog/overlay
# CORRECT:
page.services.append(my_service)
# WRONG: add LayoutControl to page.services
page.services.append(my_widget) # Widget has UI, not a service
# CORRECT:
page.add(my_widget)
# WRONG: invoke methods without platform validation
async def login(self, user_id):
await self._invoke_method("login", {"user_id": user_id})
# Will throw AttributeError on desktop
# CORRECT:
async def _invoke_method(self, method, args=None, timeout=None):
if not self._is_supported():
raise ft.FletUnsupportedPlatformException(...)
return await super()._invoke_method(...)
# WRONG: internal field without metadata skip
_sub_module: SubModule = field(default=None, init=False)
# CORRECT:
_sub_module: SubModule = field(default=None, init=False, metadata={"skip": True})
// WRONG: not converting args (ClassCastException: _Map<dynamic, dynamic>)
Future<dynamic> _onInvokeMethod(String name, dynamic args) async {
final userId = args["user_id"] as String; // May fail
// CORRECT:
Map<String, dynamic> arguments = {};
if (args != null && args is Map) {
arguments = Map<String, dynamic>.from(args);
}
final userId = arguments["user_id"] as String?;
// WRONG: returning bool without converting to String
String _hasVibrator() {
return Vibration.hasVibrator(); // Returns bool, not String
// CORRECT:
String _hasVibrator() {
return Vibration.hasVibrator().toString(); // "true" or "false"
// WRONG: control.type mismatch with @ft.control
// Python: @ft.control("myservice") | Dart: case "MyService"
// CORRECT: use EXACTLY the same string
// Python: @ft.control("MyService") | Dart: case "MyService"
// WRONG: duplicate listeners on every update()
@override
Future<void> update() async {
_setupListeners(); // Adds listeners repeatedly
// CORRECT:
bool _listenersSetup = false;
void _setupListeners() {
if (_listenersSetup) return;
_listenersSetup = true;
// ...
}
// WRONG: no error handling in SDK listeners
Sdk.addListener((event) {
control.triggerEvent("event", event.toMap());
// If event.toMap() throws, the Flutter app crashes
// CORRECT:
Sdk.addListener((event) {
try {
control.triggerEvent("event", event.toMap());
} catch (error, stackTrace) {
_handleError("listener_event", error, stackTrace);
}
});
| Error | Cause | Solution |
|---|---|---|
ModuleNotFoundError: flet.core | Old internal import path | import flet as ft |
AttributeError: _set_attr | Pre-0.80.x property pattern | Use dataclass fields |
AttributeError: invoke_method_async | Call on unsupported platform | Validate page.platform |
ValueError: must have type_name | @ft.control without string | @ft.control("PascalCase") |
ClassCastException: _Map<dynamic> | Dart args not converted | Map<String, dynamic>.from(args) |
control.type never matches | Case mismatch with @ft.control | Use identical string |
| Internal field sent to Flutter | Missing metadata skip | metadata={"skip": True} |
| Dart code not in wheel | Missing package-data | Add [tool.setuptools.package-data] |
| Duplicate events fired | Listeners added on every update | Guard with _listenersSetup flag |
types.py:
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import flet as ft
class VibrationPattern(Enum):
SHORT = "short"
MEDIUM = "medium"
LONG = "long"
DOUBLE = "double"
SOS = "sos"
@dataclass
class VibrationEvent(ft.Event["Vibration"]):
pattern: str = ""
duration_ms: int = 0
success: bool = True
@dataclass
class VibrationErrorEvent(ft.Event["Vibration"]):
method: Optional[str] = None
message: Optional[str] = None
vibration.py:
from dataclasses import field
from typing import Any, Optional
import flet as ft
from flet_vibration.types import VibrationErrorEvent, VibrationEvent, VibrationPattern
@ft.control("Vibration")
class Vibration(ft.Service):
on_vibrated: Optional[ft.EventHandler[VibrationEvent]] = None
on_error: Optional[ft.EventHandler[VibrationErrorEvent]] = None
async def vibrate(self, pattern: VibrationPattern = VibrationPattern.SHORT) -> None:
await self._call("vibrate", {"pattern": pattern.value})
async def vibrate_duration(self, duration_ms: int) -> None:
duration_ms = max(1, min(10000, duration_ms))
await self._call("vibrate_duration", {"duration_ms": duration_ms})
async def cancel(self) -> None:
await self._call("cancel")
async def has_vibrator(self) -> bool:
result = await self._call("has_vibrator", timeout=5.0)
return result == "true"
def _is_supported(self) -> bool:
if not self.page:
return False
return self.page.platform in (ft.PagePlatform.ANDROID, ft.PagePlatform.IOS)
async def _call(self, method: str, args: Optional[dict[str, Any]] = None,
timeout: Optional[float] = None) -> Any:
if not self._is_supported():
platform = self.page.platform.value if self.page else "unknown"
raise ft.FletUnsupportedPlatformException(
f"Vibration only supported on Android/iOS. Current: {platform}"
)
return await super()._invoke_method(
method_name=method, arguments=args or {},
timeout=timeout if timeout is not None else 10.0,
)
init.py:
from flet_vibration.vibration import Vibration
from flet_vibration.types import VibrationErrorEvent, VibrationEvent, VibrationPattern
__all__ = ["Vibration", "VibrationEvent", "VibrationErrorEvent", "VibrationPattern"]
__version__ = "0.1.0"
Dart — extension.dart:
import 'package:flet/flet.dart';
import 'vibration_service.dart';
class Extension extends FletExtension {
@override
void ensureInitialized() {}
@override
FletService? createService(Control control) {
switch (control.type) {
case "Vibration":
return VibrationService(control: control);
default:
return null;
}
}
}
Dart — vibration_service.dart:
import 'package:flet/flet.dart';
import 'package:flutter/foundation.dart';
import 'package:vibration/vibration.dart';
class VibrationService extends FletService {
VibrationService({required super.control});
@override
void init() {
super.init();
control.addInvokeMethodListener(_onInvokeMethod);
}
@override
void dispose() {
Vibration.cancel();
super.dispose();
}
Future<dynamic> _onInvokeMethod(String methodName, dynamic args) async {
try {
Map<String, dynamic> arguments = {};
if (args != null && args is Map) {
arguments = Map<String, dynamic>.from(args);
}
return switch (methodName) {
"vibrate" => await _vibrate(arguments),
"vibrate_duration" => await _vibrateDuration(arguments),
"cancel" => await _cancel(),
"has_vibrator" => await _hasVibrator(),
_ => throw Exception("Unknown method: $methodName"),
};
} catch (error, stackTrace) {
_handleError(methodName, error, stackTrace);
return null;
}
}
Future<String?> _vibrate(Map<String, dynamic> args) async {
final pattern = args["pattern"] as String? ?? "short";
final durationMs = _patternDuration(pattern);
await Vibration.vibrate(duration: durationMs);
control.triggerEvent("vibrated", {
"pattern": pattern,
"duration_ms": durationMs,
"success": true,
});
return null;
}
Future<String?> _vibrateDuration(Map<String, dynamic> args) async {
final durationMs = args["duration_ms"] as int? ?? 300;
await Vibration.vibrate(duration: durationMs);
control.triggerEvent("vibrated", {
"pattern": "custom",
"duration_ms": durationMs,
"success": true,
});
return null;
}
Future<String?> _cancel() async {
await Vibration.cancel();
return null;
}
Future<String> _hasVibrator() async {
final has = await Vibration.hasVibrator() ?? false;
return has.toString();
}
int _patternDuration(String pattern) {
return switch (pattern) {
"short" => 100,
"medium" => 300,
"long" => 600,
"double" => 300,
"sos" => 1200,
_ => 300,
};
}
void _handleError(String method, Object error, StackTrace stackTrace) {
debugPrint("VibrationService ERROR in $method: $error");
control.triggerEvent("error", {
"method": method,
"message": error.toString(),
});
}
}
Example app:
import flet as ft
import flet_vibration as fv
async def main(page: ft.Page):
page.title = "Vibration Demo"
vibration = fv.Vibration(
on_vibrated=lambda e: print(f"Vibrated! Pattern: {e.pattern}, {e.duration_ms}ms"),
on_error=lambda e: print(f"Error: {e.message}"),
)
page.services.append(vibration)
async def vibrate_pattern(pattern: fv.VibrationPattern):
try:
await vibration.vibrate(pattern)
except ft.FletUnsupportedPlatformException as e:
print(f"Not supported: {e}")
page.add(
ft.Text("Vibration Demo", size=24, weight=ft.FontWeight.BOLD),
ft.Wrap([
ft.FilledButton("Short", on_click=lambda _: vibrate_pattern(fv.VibrationPattern.SHORT)),
ft.FilledButton("Medium", on_click=lambda _: vibrate_pattern(fv.VibrationPattern.MEDIUM)),
ft.FilledButton("Long", on_click=lambda _: vibrate_pattern(fv.VibrationPattern.LONG)),
], spacing=8),
)
ft.run(main)
# tests/test_my_extension.py
import pytest
from flet_my_extension import MyService, MyEvent, MyLogLevel, ErrorEvent
def test_service_creation():
service = MyService(app_id="test-id", log_level=MyLogLevel.DEBUG)
assert service.app_id == "test-id"
assert service.log_level == MyLogLevel.DEBUG
def test_enum_values():
assert MyLogLevel.DEBUG.value == "debug"
assert MyLogLevel.ERROR.value == "error"
def test_event_creation():
event = MyEvent(data="test", success=True)
assert event.data == "test"
assert event.success is True
def test_error_event():
event = ErrorEvent(method="login", message="failed")
assert event.method == "login"
assert event.message == "failed"
def test_default_values():
service = MyService()
assert service.app_id == ""
assert service.log_level is None
assert service.on_error is None
def test_exports():
"""Verify all public API is exported."""
from flet_my_extension import __all__
assert "MyService" in __all__
assert "MyEvent" in __all__
assert "ErrorEvent" in __all__
# Run example app on device/emulator
cd examples/flet_my_extension_example
flet run # Tests on current platform
# Build for target platform
flet build apk # Android
flet build ipa # iOS
version in pyproject.tomlCHANGELOG.md[tool.setuptools.package-data] includes Dart filespython -m buildunzip -l dist/*.whl | grep fluttertwine upload dist/*publish_to: none in pubspec.yamlversion in sync between pyproject.toml and pubspec.yamlflet>=0.80.0| Before | After (0.80.x+) |
|---|---|
_set_attr("appId", value) | app_id: str = "" (dataclass field) |
from flet.core.control_event import ... | import flet as ft |
page.overlay.append(service) | page.services.append(service) |
ft.app(target=main) | ft.run(main) |
ft.colors.BLUE | ft.Colors.BLUE |
ft.icons.HOME | ft.Icons.HOME |
await method_async() | await method() |
page.platform == "android" | page.platform == ft.PagePlatform.ANDROID |
@ft.control("PascalCaseName") with explicit string nameft.Service or ft.LayoutControlmetadata={"skip": True} on all internal fieldsOptional[ft.EventHandler[EventType]]ft.Event["ControlClassName"]_invoke_method override__init__.py with __all__ exportsErrorEvent and on_error handlercontrol.type case matches @ft.control("Name") exactlycontrol.addInvokeMethodListener in init()Map<String, dynamic>.from(args).toString()_handleError()_listenersSetup flag_handleError with FlutterError.reportError + triggerEvent("error")dispose() cleans up resourcescreateService() | UI: createWidget() in extension.dart[tool.setuptools.package-data] in pyproject.tomlpublish_to: none in pubspec.yaml[tool.flet.app] path = "src"flet>=0.80.0 in dependenciesCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub brunobrown/claude-flet-plugin --plugin flet