// 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 CLI Tool
Overview
flet-pkg is a CLI tool that scaffolds Flet extension packages with auto-generated code from Flutter package analysis.
Usage
# 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
Pipeline: download → parse → analyze → generate
Download: Fetches Flutter package source from pub.dev (cached in ~/.cache/flet-pkg/)
Parse: Extracts Dart API (classes, methods, properties, enums) from source files
Analyze: Creates a GenerationPlan with Python↔Dart mappings, type conversions, invoke keys
Generate: Produces Python control, types, init, and Dart service files from templates
When to Use flet-pkg vs Manual
flet-pkg: When wrapping an existing Flutter/Dart package — auto-generates type mappings, method stubs, enum definitions
Manual: When creating a novel extension or when the Flutter package is very complex with custom patterns
Service Control Pattern (Python)
Main Control Class
# 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,
)
Sub-Module (Pure Python Class)
# 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"
@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)
@ft.control New Parameters (0.81.0)
# 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):
...
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
)
5. Timeout
# 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,
)
Naming Conventions
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")
Type Mapping (Dart → Python)
Standard Type Mappings
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
Generic Type Mappings
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]
Nullable Types
Dart
Python
String?
str | None
int?
int | None
List<String>?
list[str] | None
Flet-Aware Mappings (UI Controls)
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
Flutter Enum Mappings
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
Return Type Conventions
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)
Types, Events & Enums
Enum Pattern
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.
Event Dataclass Pattern
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
Standard ErrorEvent Pattern
Every extension should include an ErrorEvent and on_error handler:
[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.