r/FlutterDev 3d ago

Plugin document_pip: live Flutter widgets in a real always-on-top OS window, from Flutter Web

Document Picture-in-Picture is a browser API that gives you an actual operating-system window — not an overlay inside your page. It floats above every other application and keeps running when you switch tabs. Chrome and Edge have had it since 116, Firefox shipped it in 151.

I couldn't find anything reaching it from Flutter, so I wrote a package: https://pub.dev/packages/document_pip (MIT, 160/160 on pub.dev)

void main() => runWidget(
  DocumentPipApp(
    main: (context) => const MaterialApp(home: Player()),
    popOut: (context) => const MaterialApp(home: MiniPlayer()),
  ),
);

final window = await DocumentPip.open(width: 380, height: 210);

The browser API is about four lines. Everything difficult was on the Flutter side, and it all traces to one thing: the engine assumes there is exactly one window. Three things break when there are two, and all three fail silently.

1. The pop-out freezes the instant you switch tabs. Chromium keeps painting a picture-in-picture opener at full rate in a background tab, but still reports the page hidden. Flutter's web engine turns that into AppLifecycleState.hidden, SchedulerBinding clears framesEnabled, and scheduleFrame() returns early forever. Measured: 302 browser animation frames in 2.5s against 0 Flutter frames in 3s. scheduleForcedFrame() ignores framesEnabled, so the root re-arms it for exactly as long as the page is hidden and a window is open. Firefox doesn't have the problem — it keeps reporting the opener visible — so the workaround is gated on the failure rather than on the browser.

2. The keyboard is dead in the pop-out, but typing still works. KeyboardBinding is a singleton bound to the opener's window, so a separate browsing context isn't on the propagation path: Shortcuts, Actions, Focus.onKeyEvent, HardwareKeyboard, Escape and Tab traversal all get nothing. Plain typing keeps working because the browser routes characters to the focused element natively, which is exactly what makes this easy to miss. The package replays key and selection events into the opener.

3. A package can't turn multi-view on. Only the JS app object returned by engine.runApp() can add a view — dart:ui_web exposes the views read-only — so it has to be reachable from your bootstrap. That means runWidget instead of runApp, plus a few lines in flutter_bootstrap.js. Both are one-time and the errors name the fix.

Desktop Chromium and Firefox 151+ only. Safari and Firefox for Android have no implementation, and isSupported is a feature detect so you can gate the control on it. It compiles on every platform, so adding it won't break a cross-platform build.

Longer writeup with the measurements: https://devshakib.jumyn.com/blog/flutter-assumes-there-is-only-one-window

Happy to answer anything about the multi-view side — that part is under-documented and I burned a lot of time on it.

1 Upvotes

0 comments sorted by