Flutter Second Screen

A production-grade, Clean Architecture Flutter package for managing secondary screens, dual displays, and cross-screen communication. Currently available for Windows Desktop, with architectural blueprints and upcoming native support for Android and iOS.


๐ŸŒŸ Key Features

  • Multi-Display Detection: Query physical monitors, detect primary vs. secondary displays, resolutions, and position offsets.
  • Secondary Window Lifecycle: Initialize, open, maximize, focus, and close secondary windows seamlessly.
  • Bi-Directional Messaging: Typed or event-based cross-screen event bus to transfer cart data, signatures, advertisements, or status updates between windows.
  • Clean Architecture & SOLID: Strict separation of concerns (Domain, Data, Presentation) with dependency inversion, interface segregation, and platform interface extensibility.
  • Developer-Friendly Mixin & Widgets: Use SecondScreenListenerMixin, SecondScreenEventListener, or SecondScreenEventBuilder for reactive UI updates without boilerplate.
  • Multi-Platform Blueprint: Built on Flutter's PlatformInterface standard with pluggable platform classes and documented TODO blueprints for Android Presentation API and iOS external UIScreen.

๐Ÿ–ฅ Platform Support

Platform Support Status Underlying Mechanism
Windows Desktop / macOS โœ… Available window_manager_plus + screen_retriever multi-window
Android โณ In Development android.app.Presentation + DisplayManager
iOS โณ In Development UIScreen external displays + UIWindow
Linux ๐Ÿ“‹ Planned Desktop multi-window & display retriever

๐Ÿ› Architecture Overview

flutter_second_screen strictly adheres to Clean Architecture and SOLID principles, ensuring decoupling between UI, business rules, and platform-specific window managers.

๐ŸŒณ Architectural Component Tree

flutter_second_screen
โ”œโ”€โ”€ ๐Ÿ“ฑ Presentation Layer (Flutter Widgets & Mixins)
โ”‚   โ”œโ”€โ”€ Main Screen View (Cashier / POS UI)
โ”‚   โ”œโ”€โ”€ Secondary Screen View (Customer Display UI)
โ”‚   โ”œโ”€โ”€ SecondScreenListenerMixin (State mixin for reactive events)
โ”‚   โ”œโ”€โ”€ SecondScreenEventListener (Callback-based event listener widget)
โ”‚   โ””โ”€โ”€ SecondScreenEventBuilder (Builder widget reacting to stream events)
โ”‚
โ”œโ”€โ”€ ๐ŸŽฏ Unified Facade (Public API Entry Point)
โ”‚   โ””โ”€โ”€ FlutterSecondScreen (Singleton & factory for all operations)
โ”‚
โ”œโ”€โ”€ ๐Ÿง  Domain Layer (Pure Dart Business Logic - Zero External Dependencies)
โ”‚   โ”œโ”€โ”€ ๐Ÿ“ฆ Entities
โ”‚   โ”‚   โ”œโ”€โ”€ ScreenDisplay (Display ID, resolution, position offset, isPrimary)
โ”‚   โ”‚   โ”œโ”€โ”€ SecondScreenEvent<T> (Cross-screen typed messaging model)
โ”‚   โ”‚   โ”œโ”€โ”€ SecondScreenConfig (Secondary window title, size, position, flags)
โ”‚   โ”‚   โ””โ”€โ”€ SecondScreenStatus (Window lifecycle state)
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ โš™๏ธ Use Cases (Single Responsibility)
โ”‚   โ”‚   โ”œโ”€โ”€ Display Management
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ GetDisplaysUseCase
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ IsSecondaryDisplayAvailableUseCase
โ”‚   โ”‚   โ”œโ”€โ”€ Window Lifecycle
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ InitSecondaryDisplayUseCase
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ OpenSecondaryScreenUseCase
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ CloseSecondaryScreenUseCase
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ IsSecondaryScreenOpenUseCase
โ”‚   โ”‚   โ””โ”€โ”€ Cross-Screen Messaging
โ”‚   โ”‚       โ”œโ”€โ”€ SendEventToSecondaryUseCase
โ”‚   โ”‚       โ”œโ”€โ”€ SendEventToMainUseCase
โ”‚   โ”‚       โ””โ”€โ”€ ListenSecondaryScreenEventsUseCase
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ ๐Ÿ“‹ Repository Contracts (Interface Segregation)
โ”‚       โ”œโ”€โ”€ SecondScreenDisplayManager (Display detection contract)
โ”‚       โ”œโ”€โ”€ SecondScreenWindowManager (Window lifecycle contract)
โ”‚       โ”œโ”€โ”€ SecondScreenMessenger (Cross-screen messaging contract)
โ”‚       โ””โ”€โ”€ SecondScreenRepository (Aggregated domain interface)
โ”‚
โ””โ”€โ”€ ๐Ÿ’พ Data Layer (Infrastructure & Platform Implementations)
    โ”œโ”€โ”€ ๐Ÿ”„ Repository Implementation
    โ”‚   โ””โ”€โ”€ SecondScreenRepositoryImpl (Implements Domain Repository via DIP)
    โ”‚
    โ”œโ”€โ”€ ๐Ÿ“ฆ Data Models (Adapters & Serialization)
    โ”‚   โ”œโ”€โ”€ ScreenDisplayModel (Adapts platform displays to domain entities)
    โ”‚   โ””โ”€โ”€ SecondScreenEventModel (JSON serialization for cross-screen events)
    โ”‚
    โ””โ”€โ”€ ๐Ÿ”Œ Platform Data Sources (Platform Interface Pattern)
        โ””โ”€โ”€ SecondScreenPlatform (Federated platform interface)
            โ”œโ”€โ”€ DesktopSecondScreenPlatform (Windows & macOS multi-window core)
            โ”‚   โ”œโ”€โ”€ WindowsSecondScreenPlatform (Windows desktop plugin class)
            โ”‚   โ””โ”€โ”€ MacosSecondScreenPlatform (macOS desktop plugin class)
            โ”œโ”€โ”€ AndroidSecondScreenPlatform (Android Presentation API blueprint)
            โ””โ”€โ”€ IosSecondScreenPlatform (iOS UIScreen & UIWindow blueprint)

๐Ÿ“‚ Codebase File & Folder Structure

lib/
โ”œโ”€โ”€ flutter_second_screen.dart             # Public export library entrypoint
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ flutter_second_screen.dart         # FlutterSecondScreen facade class
    โ”œโ”€โ”€ presentation/                      # Presentation Layer
    โ”‚   โ”œโ”€โ”€ presentation.dart
    โ”‚   โ”œโ”€โ”€ mixins/
    โ”‚   โ”‚   โ””โ”€โ”€ second_screen_listener_mixin.dart
    โ”‚   โ””โ”€โ”€ widgets/
    โ”‚       โ””โ”€โ”€ second_screen_event_listener.dart
    โ”œโ”€โ”€ domain/                            # Domain Layer (Pure Dart)
    โ”‚   โ”œโ”€โ”€ domain.dart
    โ”‚   โ”œโ”€โ”€ entities/
    โ”‚   โ”‚   โ”œโ”€โ”€ entities.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ screen_display.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ second_screen_config.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ second_screen_event.dart
    โ”‚   โ”‚   โ””โ”€โ”€ second_screen_status.dart
    โ”‚   โ”œโ”€โ”€ repositories/
    โ”‚   โ”‚   โ”œโ”€โ”€ repositories.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ second_screen_display_manager.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ second_screen_window_manager.dart
    โ”‚   โ”‚   โ”œโ”€โ”€ second_screen_messenger.dart
    โ”‚   โ”‚   โ””โ”€โ”€ second_screen_repository.dart
    โ”‚   โ””โ”€โ”€ usecases/
    โ”‚       โ”œโ”€โ”€ usecases.dart
    โ”‚       โ”œโ”€โ”€ get_displays_usecase.dart
    โ”‚       โ”œโ”€โ”€ is_secondary_display_available_usecase.dart
    โ”‚       โ”œโ”€โ”€ init_secondary_display_usecase.dart
    โ”‚       โ”œโ”€โ”€ open_secondary_screen_usecase.dart
    โ”‚       โ”œโ”€โ”€ close_secondary_screen_usecase.dart
    โ”‚       โ”œโ”€โ”€ is_secondary_screen_open_usecase.dart
    โ”‚       โ”œโ”€โ”€ send_event_to_secondary_usecase.dart
    โ”‚       โ”œโ”€โ”€ send_event_to_main_usecase.dart
    โ”‚       โ””โ”€โ”€ listen_secondary_screen_events_usecase.dart
    โ””โ”€โ”€ data/                              # Data Layer
        โ”œโ”€โ”€ data.dart
        โ”œโ”€โ”€ datasources/
        โ”‚   โ”œโ”€โ”€ datasources.dart
        โ”‚   โ”œโ”€โ”€ second_screen_platform.dart
        โ”‚   โ”œโ”€โ”€ desktop_second_screen_platform.dart
        โ”‚   โ”œโ”€โ”€ windows_second_screen_platform.dart
        โ”‚   โ”œโ”€โ”€ macos_second_screen_platform.dart
        โ”‚   โ”œโ”€โ”€ android_second_screen_platform.dart
        โ”‚   โ””โ”€โ”€ ios_second_screen_platform.dart
        โ”œโ”€โ”€ models/
        โ”‚   โ”œโ”€โ”€ models.dart
        โ”‚   โ”œโ”€โ”€ screen_display_model.dart
        โ”‚   โ””โ”€โ”€ second_screen_event_model.dart
        โ””โ”€โ”€ repositories/
            โ”œโ”€โ”€ repositories.dart
            โ””โ”€โ”€ second_screen_repository_impl.dart

๐Ÿ”„ Data & Control Flow Diagram

flowchart TD
    subgraph UI ["Presentation Layer (Flutter UI)"]
        ScreenA["Main Screen View"]
        ScreenB["Secondary Screen View"]
        Mixin["SecondScreenListenerMixin"]
        Widget["SecondScreenEventListener"]
    end

    subgraph Facade ["Unified Facade"]
        FSS["FlutterSecondScreen"]
    end

    subgraph Domain ["Domain Layer (Pure Dart)"]
        direction TB
        UseCases["Use Cases (Open, Close, Init, SendEvent, etc.)"]
        Contracts["Repository Contracts (DisplayManager, WindowManager, Messenger)"]
        Entities["Entities (ScreenDisplay, SecondScreenEvent, SecondScreenConfig)"]
        UseCases --> Contracts
        UseCases -.-> Entities
    end

    subgraph Data ["Data Layer"]
        RepoImpl["SecondScreenRepositoryImpl"]
        PI["SecondScreenPlatform (Interface)"]
        
        subgraph Implementations ["Platform Drivers"]
            DeskPlat["DesktopSecondScreenPlatform"]
            WinPlat["WindowsSecondScreenPlatform"]
            MacPlat["MacosSecondScreenPlatform"]
            AndPlat["AndroidSecondScreenPlatform (Blueprint)"]
            IosPlat["IosSecondScreenPlatform (Blueprint)"]
        end
    end

    UI --> Facade
    Facade --> UseCases
    RepoImpl -.->|implements (DIP)| Contracts
    RepoImpl --> PI
    PI --> DeskPlat
    DeskPlat --> WinPlat
    DeskPlat --> MacPlat
    PI --> AndPlat
    PI --> IosPlat

SOLID Principles Applied:

  • Single Responsibility Principle (SRP): Display queries (SecondScreenDisplayManager), window lifecycle management (SecondScreenWindowManager), and inter-screen messaging (SecondScreenMessenger) are decoupled into focused, dedicated interfaces.
  • Open/Closed Principle (OCP): Adding support for a new operating system (e.g., Android, iOS, or macOS) only requires implementing SecondScreenPlatform without modifying the domain use cases or repository contracts.
  • Liskov Substitution Principle (LSP): Any platform implementation (WindowsSecondScreenPlatform, AndroidSecondScreenPlatform, or test mocks) can be seamlessly substituted without breaking consumers.
  • Interface Segregation Principle (ISP): Clients can depend solely on SecondScreenDisplayManager, SecondScreenWindowManager, or SecondScreenMessenger instead of one monolithic interface.
  • Dependency Inversion Principle (DIP): High-level application modules depend on domain abstractions rather than concrete platform libraries.

๐Ÿš€ Getting Started

1. Add Dependency

In your pubspec.yaml:

dependencies:
  flutter_second_screen: ^1.0.0

2. Main Entry Point Setup

In your main.dart, handle the secondary screen entry point when the process is launched:

import 'package:flutter/material.dart';
import 'package:flutter_second_screen/flutter_second_screen.dart';

void main(List<String> args) async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterSecondScreen.ensureInitialized(args);

  if (FlutterSecondScreen.isSecondaryScreen(args)) {
    // Secondary screen process
    final initialized = await FlutterSecondScreen.instance.initSecondaryDisplay(
      const SecondScreenConfig(title: 'Customer Facing Display'),
    );
    if (initialized) {
      runApp(const CustomerDisplayApp());
    }
  } else {
    // Main cashier / POS application
    runApp(const CashierMainApp());
  }
}

๐Ÿ’ก Usage Examples

Opening the Secondary Screen

final secondScreen = FlutterSecondScreen.instance;

// Check if a second physical monitor is connected
if (await secondScreen.isSecondaryDisplayAvailable()) {
  await secondScreen.openSecondaryScreen(arguments: ['second-screen']);
}

Sending Events Between Screens

// Send from Main Screen to Secondary Screen
await secondScreen.sendEventToSecondary('cartUpdate', {
  'items': [
    {'name': 'Sneakers', 'price': 89.99},
  ],
  'total': 89.99,
});

// Send from Secondary Screen back to Main Screen (e.g. Signature or Confirmation)
await secondScreen.sendEventToMain('signatureApproved', signatureData);

Listening to Events with SecondScreenListenerMixin

class CustomerDisplayScreen extends StatefulWidget {
  const CustomerDisplayScreen({super.key});

  @override
  State<CustomerDisplayScreen> createState() => _CustomerDisplayScreenState();
}

class _CustomerDisplayScreenState extends State<CustomerDisplayScreen>
    with SecondScreenListenerMixin {
  Map<String, dynamic>? cartData;

  @override
  Map<String, void Function(dynamic)> getSecondScreenEventListeners() {
    return {
      'cartUpdate': (data) {
        setState(() {
          cartData = data as Map<String, dynamic>;
        });
      },
      'reset': (_) {
        setState(() {
          cartData = null;
        });
      },
    };
  }

  @override
  Widget build(BuildContext context) {
    if (cartData == null) {
      return const Center(child: Text('Welcome!'));
    }
    return Text('Total: \$${cartData!['total']}');
  }
}

Listening to Events with SecondScreenEventListener Widget

SecondScreenEventListener(
  eventName: 'cartUpdate',
  onEvent: (data) {
    debugPrint('Received cart update: $data');
  },
  child: const MyCartWidget(),
);

๐Ÿ“ฑ Upcoming Android & iOS Implementation

flutter_second_screen defines clean platform contracts and architectural blueprints for Android and iOS in:

  • AndroidSecondScreenPlatform (lib/src/data/datasources/android_second_screen_platform.dart)
  • IosSecondScreenPlatform (lib/src/data/datasources/ios_second_screen_platform.dart)

Android Roadmap

  • Integrates with Android's DisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION).
  • Spawns a secondary FlutterEngine inside an android.app.Presentation dialog window.
  • Communicates across engines via native MethodChannel and EventChannel.

iOS Roadmap

  • Detects external displays via UIScreen.screens and UIScreenDidConnectNotification.
  • Attaches a secondary UIWindow with a FlutterViewController bound to the external UIScreen.
  • Communicates across engines via native MethodChannel and EventChannel.

๐Ÿงช Testing

The package includes comprehensive unit and widget tests covering all entities, use cases, repository implementations, mixins, and platform interfaces:

flutter test

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐ŸŒŸ Support

If you find this package helpful, please consider giving it a โญ on GitHub and a ๐Ÿ‘ on pub.dev!