extend_screen
A Flutter plugin for multi-window support on desktop and dual-display support on Android.
| Platform | Feature | How it works |
|---|---|---|
| Windows / macOS / Linux | Open multiple independent OS windows | desktop_multi_window — each window is a separate OS process with its own Flutter engine |
| Android | Mirror content to a secondary display | Android Presentation API — a second FlutterEngine drives the customer-facing screen |
| iOS / Web | Graceful no-op | isSupported() returns false; all other calls are silent no-ops |
Installation
Add to your pubspec.yaml:
dependencies:
extend_screen:
path: ../extend_screen # or pub.dev package name once published
Then run:
flutter pub get
Quick Start
import 'package:extend_screen/extend_screen.dart';
// Obtain the singleton — platform is detected once and cached
final manager = await MultiWindowManager.instance();
if (await manager.isSupported()) {
// Desktop: open a new OS window that fills the secondary display
await manager.openSubWindow({'title': 'Customer Display'});
// Android POS: push state to the customer-facing screen
await manager.sendStateToSubDisplay(
OrderSummaryState(items: cart, total: 49.99),
);
}
// Clean up (call on app exit or when done)
await manager.closeAll();
Desktop Integration
1. Route sub-windows in main.dart
The desktop_multi_window package re-invokes the app executable for each
sub-window with the arguments ['multi_window', windowId, argumentJson].
You must detect this at startup and run a different widget tree:
import 'dart:convert';
import 'package:flutter/material.dart';
void main(List<String> args) {
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args.length > 2 && args[2].isNotEmpty
? jsonDecode(args[2]) as Map<String, dynamic>
: <String, dynamic>{};
WidgetsFlutterBinding.ensureInitialized();
runApp(MySubWindowApp(windowId: windowId, argument: argument));
return;
}
runApp(const MyMainApp());
}
2. Open sub-windows
final manager = await MultiWindowManager.instance();
// Default — fills the secondary display (or primary if only one display)
await manager.openSubWindow({});
// Custom size: centred on the secondary display
await manager.openSubWindow(
{'myData': 'hello'},
size: const SubWindowSize.centered(width: 800, height: 600),
);
// Explicit frame (global screen coordinates, logical pixels)
await manager.openSubWindow(
{},
size: SubWindowSize.fixed(const Rect.fromLTWH(100, 100, 1280, 800)),
);
3. Pass data to the sub-window
The argument map is JSON-encoded and available in the sub-window's
main(List<String> args) as jsonDecode(args[2]):
// Main window
await manager.openSubWindow({'orderId': 42, 'customerName': 'Alice'});
// Sub-window (in MySubWindowApp)
class MySubWindowApp extends StatelessWidget {
final Map<String, dynamic> argument;
// argument['orderId'] == 42
// argument['windowNumber'] is added automatically (1-based counter)
}
4. Close all sub-windows
await manager.closeAll();
Android Integration
1. Add the sub-screen Dart entry point
The plugin starts a second FlutterEngine with a custom Dart entrypoint.
Add this function to your app (the @pragma prevents tree-shaking):
@pragma('vm:entry-point')
void subScreenMain() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MySubScreenApp());
}
MySubScreenApp listens on MethodChannel('sub_screen_commands') for
'updateState' calls sent by the plugin bridge:
class _MySubScreenAppState extends State<MySubScreenApp> {
static const _channel = MethodChannel('sub_screen_commands');
SubDisplayState _state = const IdleState();
@override
void initState() {
super.initState();
_channel.setMethodCallHandler((call) async {
if (call.method == 'updateState') {
setState(() {
_state = SubDisplayState.fromJson(
Map<String, dynamic>.from(call.arguments as Map),
);
});
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: switch (_state) {
IdleState() => const IdleScreen(),
OrderSummaryState s => OrderSummaryScreen(items: s.items, total: s.total),
PaymentPromptState s => PaymentPromptScreen(total: s.total),
},
);
}
}
2. Update AndroidManifest.xml
Enable multi-window and presentation display support:
<activity
android:name=".MainActivity"
android:resizeableActivity="true"
...>
3. No MainActivity changes needed
The plugin registers itself automatically via GeneratedPluginRegistrant.
Keep MainActivity as a bare FlutterActivity:
class MainActivity : FlutterActivity()
4. Send state to the secondary display
final manager = await MultiWindowManager.instance();
if (await manager.isSupported()) {
// Show idle/welcome screen
await manager.sendStateToSubDisplay(const IdleState());
// Update with order items
await manager.sendStateToSubDisplay(
OrderSummaryState(
items: [
{'name': 'Coffee', 'qty': 2, 'price': 3.50},
{'name': 'Sandwich', 'qty': 1, 'price': 6.75},
],
total: 13.75,
),
);
// Show payment prompt
await manager.sendStateToSubDisplay(PaymentPromptState(total: 13.75));
// Release the secondary display engine
await manager.closeAll();
}
API Reference
MultiWindowManager
Singleton. Obtain via MultiWindowManager.instance().
| Method | Description |
|---|---|
Future<bool> isSupported() |
Whether the current device/platform has an active secondary display or multi-window support |
Future<void> openSubWindow(Map<String,dynamic> argument, {SubWindowSize size}) |
Desktop only — opens a new independent OS window |
Future<void> sendStateToSubDisplay(SubDisplayState state) |
Android only — pushes a state snapshot to the secondary-display Flutter engine |
Future<void> closeAll() |
Desktop: closes all sub-windows. Android: releases the secondary-display engine |
SubWindowSize
Controls the initial frame of a desktop sub-window. Three constructors:
| Constructor | Behaviour |
|---|---|
SubWindowSize.fullScreen() |
Fills the secondary display. Falls back to the primary if only one display is present. (default) |
SubWindowSize.centered({required double width, required double height}) |
A fixed logical size centred on the secondary display |
SubWindowSize.fixed(Rect frame) |
An exact frame in global screen coordinates (logical pixels). Use for non-standard display arrangements |
Multi-display layout assumption:
fullScreenandcenteredassume the secondary display is positioned to the right of the primary — the most common POS / dual-display setup. UseSubWindowSize.fixedfor vertical stacking or other arrangements.
SubDisplayState
Sealed class hierarchy for communicating with the Android secondary-display engine.
// Three concrete states
const IdleState()
OrderSummaryState(
items: List<Map<String, dynamic>>, // [{name, qty, price}]
total: double,
)
PaymentPromptState(total: double)
Full JSON round-trip is built in:
final json = state.toJson(); // Map<String, dynamic>
final state = SubDisplayState.fromJson(json); // sealed → concrete type
Platform Support
| Platform | isSupported() |
openSubWindow |
sendStateToSubDisplay |
|---|---|---|---|
| macOS | always true |
✅ | no-op |
| Windows | always true |
✅ | no-op |
| Linux | always true |
✅ | no-op |
| Android (secondary display connected) | true |
no-op | ✅ |
| Android (no secondary display) | false |
no-op | no-op |
| iOS | always false |
no-op | no-op |
| Web | always false |
no-op | no-op |
Performance Tips
-
Debounce rapid state updates. If your app pushes state on every keystroke or slider drag, add a short debounce (100–200 ms) so you don't saturate the MethodChannel:
Timer? _debounce; void _onCartChanged() { _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 100), () { manager.sendStateToSubDisplay(OrderSummaryState(items: cart, total: total)); }); } -
Use
ValueNotifierfor scoped rebuilds. Wrap fast-changing values (counters, totals) inValueNotifierand read them withValueListenableBuilderso only the affected widget rebuilds. -
isSupported()is cached. The first call queries the platform; subsequent calls return the cached result. Call it once ininitStateand store the result.
Hot Restart on Desktop (Development)
Sub-windows are separate OS processes. When you hot-restart the main app, the Dart VM resets all state but the sub-window processes keep running — leaving stale windows on screen that the new manager instance cannot reach.
This is handled automatically. In debug builds, MultiWindowManager.instance()
queries the OS for all alive sub-window IDs and closes them before returning. You always
start from a clean slate after a hot restart.
This cleanup only runs in kDebugMode — release builds are unaffected.
Example App
A full working demo is in the example/ directory.
cd example
# macOS
flutter run -d macos
# Windows
flutter run -d windows
# Linux
flutter run -d linux
# Android (connect a device with a secondary display for full POS demo)
flutter run -d <device-id>
The example demonstrates:
- Desktop: Main window with a counter and two "Open Sub Window" buttons (full-screen and 640 × 480 centred variants). Each sub-window has its own independent counter — no shared state between windows.
- Android POS: Cashier screen with Add Item / Payment / New Order buttons. The customer-facing display shows Idle → Order Summary → Payment Prompt states in real time.
Requirements
| Requirement | Detail |
|---|---|
| Flutter | >=3.10.0 |
| Dart | ^3.10.4 |
Android minSdk |
default Flutter minSdkVersion (no extra requirement) |
desktop_multi_window |
^0.2.0 (transitive — no need to add manually) |
Project Structure
lib/
├── extend_screen.dart ← public barrel export
└── src/
├── multi_window_manager.dart ← abstract API + singleton factory
├── sub_display_state.dart ← sealed state (Idle / OrderSummary / PaymentPrompt)
├── sub_window_size.dart ← sealed window-size spec (fullScreen / centered / fixed)
├── desktop_multi_window_manager.dart ← Windows / macOS / Linux implementation
├── android_second_display_manager.dart ← Android MethodChannel implementation
└── unsupported_multi_window_manager.dart ← iOS / Web no-op fallback
android/src/main/kotlin/app/mylekha/package/extend_screen/
└── ExtendScreenPlugin.kt ← FlutterPlugin, Presentation API, second FlutterEngine
example/ ← runnable demo app
Libraries
- extend_screen
extend_screen— Flutter plugin for multi-window (desktop) and dual-display / secondary-screen (Android POS) support.