Skip to content

3.47.1

Latest

Choose a tag to compare

@lhoward lhoward released this 20 Aug 06:39
· 4 commits to main since this release

3.47.1

Engine updated to Flutter 3.47.1 (5d53178869); flutter-embedded-linux
submodule bumped to latest upstream.

Highlights

  • NonisolatedNonsendingByDefault enabled: unannotated async API inherits the
    caller's actor, so platform channel round trips no longer hop out to the
    cooperative pool and back.
  • Platform channel sends, and the codec work around them, now run on the
    platform thread — as they do in Flutter's own channels, where a task queue is
    the opt-out.
  • Plugin registration no longer races, and platform views work for the first
    time.

Breaking changes

  • NonisolatedNonsendingByDefault changes the mangling of public API taking
    async closures. Dependents need a rebuild.
  • The reply-less FlutterMethodChannel.invoke(method:arguments:) and
    FlutterBasicMessageChannel.send(message:) are now
    @FlutterPlatformThreadActor and non-async; the underlying send is
    synchronous, so the await only ever bought the hop onto the platform actor.
    Callers not already on the platform actor keep writing try await; callers on
    it drop the await.
  • FlutterPlugin.register(with:) and addMethodCallDelegate are isolated to
    the platform actor, as is FlutterPluginRegistrar.handleMethod.
  • FlutterPlatformView is now class-bound (: AnyObject) and covers the six
    methods the embedder implements, adding clearFocus(),
    resize(width:height:), touch(deviceId:eventType:x:y:) and
    offset(top:left:). A value type can no longer conform.
  • FlutterView.viewController is gone. FlutterView is a handle onto the
    embedder's view, vended afresh on each access; the platform views registry now
    lives on the engine.

Channels and threading

  • withPriority runs its block inline when no priority is given, so a caller
    already on the platform actor stays there; previously it always spawned a
    Task, which starts on the generic executor, so the engine send ran off the
    platform thread despite send() being @FlutterPlatformThreadActor — the
    very hop the feature was meant to remove.
  • Both messengers route through an isolated _send, so the priority path hops
    back rather than calling the engine off the platform thread.
  • The desktop messenger's reply path drops an inner Task.

Plugins

  • Plugin registration spawned a task to reach the platform-actor-isolated
    setMethodCallHandler, so register() returned before the handler existed
    and a message arriving in the gap surfaced in Dart as
    MissingPluginException. Isolating register() and addMethodCallDelegate
    closes the gap.

Platform views

Platform views could never have worked before this release:

  • The registration path was dead — nothing ever assigned
    FlutterView.viewController — and would not have taken effect anyway, since
    FlutterView is a struct that both engine.view and registrar.view mint
    afresh on every access, so the handler was stored into a copy.
  • create() parsed its arguments via value as? [String: Any], but value(as:)
    is a throwing method rather than a property, so the cast never succeeded.
  • create() returned nil instead of the texture id Dart composites from.
  • The touch case was spelled "touchMethod" where Dart sends "touch";
    offset was missing entirely; resize, clearFocus, touch and offset
    were unimplemented.
  • The view protocol admitted value types, so clearing focus on the previously
    focused view mutated a copy.

Now: platform view state lives on the engine, and the registry is registered on
first use so it displaces the handler the embedder's view installs in its own
constructor. An app that registers no factory never creates it, and the embedder
keeps the channel exactly as before. The five methods the embedder stubs
(setDirection, acceptGesture, rejectGesture, enter, exit) throw
methodNotImplemented, as it does. Arguments are read off
AnyFlutterStandardCodable directly, since the generic bridge round-trips
through JSON, which is too costly for a per-touch path.

Examples

  • Counter example dependencies refreshed; analysis_options.yaml added.

Upgrade notes

  • Rebuild dependents: enabling NonisolatedNonsendingByDefault changes public
    symbol mangling.
  • Drop await from reply-less invoke(method:arguments:) /
    send(message:) calls made from the platform actor; other callers are
    unchanged.
  • Make FlutterPlatformView conformances classes and implement the four new
    methods.
  • Remove any use of FlutterView.viewController; platform view registration is
    now automatic on first factory registration.
  • Replace uses of the removed *Block shims in CxxFlutterSwift.h with the
    embedder's own FlutterDesktopMessengerSetCallback /
    FlutterDesktopPluginRegistrarSetDestructionHandler.

3.47.0

Engine updated to Flutter 3.47.0 (5f77625673).

Highlights

  • The C++ shim layer is gone. FlutterSwift now calls the eLinux embedder's
    function-pointer C API directly through C++ interop, boxing its own closures
    into the accompanying user_data, so CxxFlutterSwift.cpp and its
    Objective-C block typedefs have been deleted.

Breaking changes

  • The block-taking shims are removed from CxxFlutterSwift.h:

    • FlutterDesktopMessengerSendWithReplyBlock
    • FlutterDesktopMessengerSetCallbackBlock
    • FlutterDesktopPluginRegistrarSetDestructionHandlerBlock
    • FlutterDesktopEngineSetView

    Two of these only duplicated public API the embedder already vends
    (FlutterDesktopMessengerSetCallback,
    FlutterDesktopPluginRegistrarSetDestructionHandler); the fourth had no
    callers, as FlutterEngine.setView reaches flutter::FlutterELinuxEngine
    through interop.

Memory and lifecycle fixes

  • Every send-with-reply leaked its copied block: the engine only calls the
    cleanup callback when the send outright fails. The Swift reply thunk now
    consumes the retain.
  • The messenger callback setter stored a _Block_copy but handed the engine the
    uncopied block as user_data, which only held together because nothing
    freed the copy first.
  • Plugin registrar destructors are kept per registrar as a list rather than a
    single entry. eLinux vends one registrar per engine however many plugins ask
    for one, so the old map let the second plugin's handler evict the first,
    leaving that plugin attached to a freed registrar after Stop().

Build

  • release added to the excluded sources so SwiftPM does not pick up the build
    directory.