agentsclimarketplace

Flet extension

Skill brunobrown/claude-flet-plugin/skills/flet-extension

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+.From its SKILL.md

Install
npx -y skills add brunobrown/claude-flet-plugin --skill flet-extension

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

49.4 KB, as published. Nobody here has run it

Flet Extension Development — Complete Reference

Flet 0.85.x | Validated against real production extensions (flet-onesignal, flet-vibration, flet-video, flet-audio)

Flet 0.85.x context for extensions

  • Companion extensions evolved in lockstep with 0.85.0:
    • flet-video — Configurable player controls (show/hide play/pause, seek bar, fullscreen, etc.) and an async take_screenshot() method on the player control
    • flet-audio — New AudioRecorder with PCM16 streaming and direct upload to backend
  • Core APIs your extension may want to integrate with:
    • page.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 interop
    • ft.use_dialog — for UI controls that surface dialogs, prefer this hook over imperative page.show_dialog
  • The deprecated DragTargetEvent.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.

Extension Architecture Overview

Two Extension Types

Aspectft.Serviceft.LayoutControl
Has UINoYes
Base class (Python)ft.Serviceft.LayoutControl
Base class (Dart)FletServiceStatefulWidget
Add to apppage.services.append()page.add() / controls list
Extension handlercreateService(control)createWidget(control)
width/heightNoYes (inherited)
expandNoYes (inherited)
ExamplesPush notifications, audio, geolocationWebView, video player, map

When to Use Which

  • Service: SDKs that run in background, platform APIs (push, permissions, location, audio)
  • UI Control: Widgets that render on screen and occupy visual space (players, maps, charts)

Directory Structure

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

Extension Registration Pattern

// 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

  1. Download: Fetches Flutter package source from pub.dev (cached in ~/.cache/flet-pkg/)
  2. Parse: Extracts Dart API (classes, methods, properties, enums) from source files
  3. Analyze: Creates a GenerationPlan with Python↔Dart mappings, type conversions, invoke keys
  4. 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"

Service Control Pattern (Dart)

FletService Implementation

// 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);
  }
}

UI Control Pattern (Python)

Main Control Class

# 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

New 0.81.0 LayoutControl Properties

All inherited automatically by any UI Control:

PropertyTypeDescription
transformTransformGeneric Matrix4 transform (translate, rotate, scale, skew)
aspect_ratioNumberAutomatic width/height ratio
animate_sizeAnimationValueImplicit size animation
on_size_changeEventHandler[LayoutSizeChangeEvent]Event when dimensions change
size_change_intervalintSampling 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)

@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):
    ...

UI Control Pattern (Dart)

StatefulWidget Implementation

// 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 ↔ Flutter Communication

Overview

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)

1. Python → Flutter: Properties

# 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"

2. Python → Flutter: Invoke Methods

# 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"),
    };
}

3. Flutter → Python: Events

// 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

4. Internal Fields (NOT Sent to Flutter)

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

SideConvention
Python: property fieldsnake_case (app_id, log_level)
Python: methodsnake_case async (login, get_status)
Python: eventon_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 switchSame PascalCase ("MyService")

Type Mapping (Dart → Python)

Standard Type Mappings

Dart TypePython Type (Service)Python Type (UI Control)
Stringstrstr
boolboolbool
intintint
doublefloatft.Number
numfloatft.Number
voidNoneNone
dynamicAnyAny
ObjectAnyAny
Colorstrft.Color
Durationintint
DateTimestrstr
Uint8Listbytesbytes
Uristrstr

Generic Type Mappings

Dart GenericPython 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

DartPython
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 TypeFlet Python TypeDart Getter
Alignmentft.Alignmentcontrol.getAlignment("name")
BoxFitft.BoxFitcontrol.getBoxFit("name")
Colorft.Colorcontrol.getString("name")
double / numft.Numbercontrol.getDouble("name")
Widgetft.ControlbuildWidget("name")
TextStyleft.TextStylecontrol.getTextStyle("name", Theme.of(context))
Rectft.Rectcontrol.getRect("name")
KeyskippedN/A

Flutter Enum Mappings

Common Flutter enums are mapped to str for serialization:

Dart EnumPython Type
TextDirectionstr
Axisstr
MainAxisAlignmentstr
CrossAxisAlignmentstr
TextAlignstr
FontWeightstr
BoxFitstr (service) / ft.BoxFit (UI)
Alignmentstr (service) / ft.Alignment (UI)
Clipstr
Curvestr
Brightnessstr

Return Type Conventions

Python TypeDart ReturnPython Conversion
Nonereturn null—
boolreturn result.toString() → "true"/"false"result == "true"
strreturn valueresult if result else None
dictreturn jsonEncode(map)json.loads(result)
int/floatreturn 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:

# 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 Configuration

pyproject.toml (Extension)

[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.

pubspec.yaml (Flutter)

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

Example App pyproject.toml

[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"

Extension Registration in App

# pyproject.toml of the APP (not the extension)
[tool.flet.extensions]
flet_my_extension = "flet_my_extension.Extension"

Advanced Patterns

Compound Widgets / Sub-Controls

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)

Sub-Modules (Namespace Classes)

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 {}

Stream Getters → Event Handlers

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

Anti-Patterns

Python Anti-Patterns

# 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})

Dart Anti-Patterns

// 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);
    }
});

Common Errors Table

ErrorCauseSolution
ModuleNotFoundError: flet.coreOld internal import pathimport flet as ft
AttributeError: _set_attrPre-0.80.x property patternUse dataclass fields
AttributeError: invoke_method_asyncCall on unsupported platformValidate page.platform
ValueError: must have type_name@ft.control without string@ft.control("PascalCase")
ClassCastException: _Map<dynamic>Dart args not convertedMap<String, dynamic>.from(args)
control.type never matchesCase mismatch with @ft.controlUse identical string
Internal field sent to FlutterMissing metadata skipmetadata={"skip": True}
Dart code not in wheelMissing package-dataAdd [tool.setuptools.package-data]
Duplicate events firedListeners added on every updateGuard with _listenersSetup flag

Complete Service Extension Template

Vibration Service Example

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)

Testing Extensions

Unit Tests

# 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__

Integration Testing

# 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

Publishing

PyPI Publishing Checklist

  1. Update version in pyproject.toml
  2. Update CHANGELOG.md
  3. Verify [tool.setuptools.package-data] includes Dart files
  4. Build: python -m build
  5. Check wheel contents: unzip -l dist/*.whl | grep flutter
  6. Upload: twine upload dist/*

pub.dev Considerations

  • Extensions use publish_to: none in pubspec.yaml
  • Dart code is distributed inside the Python wheel, NOT on pub.dev
  • The Flet build system extracts Dart code from installed Python packages

Version Management

  • Keep version in sync between pyproject.toml and pubspec.yaml
  • Follow semantic versioning (MAJOR.MINOR.PATCH)
  • Track Flet compatibility: flet>=0.80.0

Breaking Changes Reference (Pre-0.80.x)

BeforeAfter (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.BLUEft.Colors.BLUE
ft.icons.HOMEft.Icons.HOME
await method_async()await method()
page.platform == "android"page.platform == ft.PagePlatform.ANDROID

Implementation Checklist

Python Side

  • @ft.control("PascalCaseName") with explicit string name
  • Correct base class: ft.Service or ft.LayoutControl
  • metadata={"skip": True} on all internal fields
  • Events typed as Optional[ft.EventHandler[EventType]]
  • Event classes extend ft.Event["ControlClassName"]
  • Enums with string values
  • Platform validation in _invoke_method override
  • __init__.py with __all__ exports
  • ErrorEvent and on_error handler

Dart Side

  • control.type case matches @ft.control("Name") exactly
  • control.addInvokeMethodListener in init()
  • Args converted: Map<String, dynamic>.from(args)
  • Booleans returned as .toString()
  • All listeners wrapped in try/catch → _handleError()
  • Listener setup guarded with _listenersSetup flag
  • _handleError with FlutterError.reportError + triggerEvent("error")
  • dispose() cleans up resources
  • Service: createService() | UI: createWidget() in extension.dart

Project Configuration

  • [tool.setuptools.package-data] in pyproject.toml
  • publish_to: none in pubspec.yaml
  • Example app with [tool.flet.app] path = "src"
  • flet>=0.80.0 in dependencies

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.