presentation_displays
A Flutter plugin to manage and display content on secondary screens (HDMI, Wireless, AirPlay).
This plugin creates a separate FlutterEngine for the secondary display, allowing you to render a completely independent UI on the external screen while communicating with the main application via Method Channels.
Features
- Multi-Screen Support: Run a separate Flutter widget tree on an external display.
- Plug & Play: Automatically detects connected displays.
- Data Transfer: Send data objects (Map/JSON) from the main screen to the secondary screen.
- Cross-Platform: Supports Android and iOS.
1. Flutter Setup (Entry Points)
To run a UI on a secondary screen, you must define a specific entry point called secondaryDisplayMain. This acts as the "main" function for your external display.
In your lib/main.dart (or wherever your entry points are defined):
import 'package:flutter/material.dart';
// 1. The Main Entry Point
void main() {
debugPrint('first main');
runApp(const MyApp());
}
// 2. The Secondary Entry Point
// IMPORTANT: This must be named 'secondaryDisplayMain' and annotated with @pragma('vm:entry-point')
@pragma('vm:entry-point')
void secondaryDisplayMain() {
debugPrint('second main');
runApp(const MySecondApp());
}
// 3. Your Secondary App Widget
class MySecondApp extends StatelessWidget {
const MySecondApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
// Define specific routes for the secondary screen
onGenerateRoute: generateRoute,
initialRoute: 'presentation',
);
}
}
// 4. Your Main App Widget
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
onGenerateRoute: generateRoute,
initialRoute: '/',
);
}
}
2. Android Setup
No special configuration is required for Android. Ensure your minSdkVersion is compatible with Flutter defaults.
3. iOS Setup
⚠️ Critical: This plugin requires iOS 13.0+ and the adoption of the UIScene Lifecycle. If your app is still using the legacy UIWindow logic without Scenes, it must be migrated.
Step A: Update Info.plist
Open ios/Runner/Info.plist and add the UIApplicationSceneManifest. This configures the app to handle both the main application screen and the external display role.
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleExternalDisplay</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>External Display</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
</dict>
</array>
</dict>
</dict>
Step B: Update AppDelegate.swift
You must adopt the FlutterImplicitEngineDelegate protocol to handle plugin registration for the main engine, and hook into SwiftPresentationDisplaysPlugin to register plugins for the secondary engine.
Replace your AppDelegate.swift content with:
import UIKit
import Flutter
import presentation_displays_hig
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - FlutterImplicitEngineDelegate
// This method is called when the main Flutter engine is initialized
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
// Register plugins for the MAIN engine
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
// Assign the callback for when the SECONDARY controller is added
SwiftPresentationDisplaysPlugin.controllerAdded = controllerAdded
}
// This method is called by the plugin when the secondary display is connected
func controllerAdded(controller: FlutterViewController) {
// Register plugins for the SECONDARY engine
GeneratedPluginRegistrant.register(with: controller)
}
}
Usage
1. Initialize Display Manager
import 'package:presentation_displays_hig/displays_manager.dart';
DisplayManager displayManager = DisplayManager();
2. List Connected Displays
List<Display> displays = [];
Future<void> getDisplays() async {
final values = await displayManager.getDisplays();
if (values != null) {
setState(() {
displays = values;
});
}
}
3. Show Presentation
Use the displayId from the list above and the routerName defined in your MySecondApp widget.
// Index 0 is usually the built-in screen, Index 1 is the external display
if (displays.length > 1) {
await displayManager.showSecondaryDisplay(
displayId: displays[1].displayId!,
routerName: "presentation"
);
}
4. Transfer Data
You can send Map or JSON data to the secondary screen.
await displayManager.transferDataToPresentation({
"title": "Customer Order",
"total": 150.00,
"items": ["Apple", "Banana"]
});
5. Receive Data (Secondary Screen)
In the widget running on the secondary screen (e.g., SecondaryDisplay), use the callback to receive data.
@override
Widget build(BuildContext context) {
return SecondaryDisplay(
callback: (argument) {
// argument contains the Map/Data sent from the main screen
setState(() {
receivedData = argument;
});
},
child: YourCustomWidget(data: receivedData),
);
}
6. Hide Presentation
await displayManager.hideSecondaryDisplay(displayId: displays[1].displayId!);