diff --git a/rfc/440.0001-extensible-enums-in-federated-plugins.md b/rfc/440.0001-extensible-enums-in-federated-plugins.md new file mode 100644 index 0000000..055fa10 --- /dev/null +++ b/rfc/440.0001-extensible-enums-in-federated-plugins.md @@ -0,0 +1,575 @@ +--- +type: rfc +rfc: '440.0001' +title: Extensible Enums in Federated Plugins +description: Establishes a standard extensible constant pattern for Flutter federated plugins to support growing platform capabilities without breaking backward compatibility. +status: draft +created: 2026-07-01T00:00:00Z +updated: 2026-11-06T21:59:03.309868Z +tags: + - 440-plugins + - 720-ecosystem +authors: + - https://github.com/Mairramer +--- + +# RFC 440.0001: Extensible Enums in Federated Plugins + +## Overview + +In Flutter federated plugins, packages often expose platform-specific configurations and capabilities (e.g., camera pixel formats, biometric authentication types, sensor modalities, audio/video codecs). Historically, these domain sets have been modeled as standard Dart `enum`s. + +However, standard Dart enums represent strictly closed sets. When an underlying operating system (Android, iOS, Web, Windows, macOS, Linux) introduces a new feature and maintainers add a corresponding value to the enum, it triggers a compile-time breaking change for all downstream consumers and platform implementations that switch over the enum without a wildcard or `default` branch. + +To enable the Flutter ecosystem to evolve rapidly alongside living operating systems without forcing frequent major semantic version bumps, this RFC establishes **The Extensible Constant Pattern**: a standardized, backward-compatible, type-safe pattern for open domain sets across Flutter packages. + +* **Document Shortlink:** [flutter.dev/go/extensible-enums-plugins](https://flutter.dev/go/extensible-enums-plugins) +* **Tracking Issue:** [flutter/flutter#189696](https://github.com/flutter/flutter/issues/189696) +* **Audience:** Flutter plugin authors, platform interface maintainers, and application developers consuming platform constants. + +--- + +## Table of Contents + +* [Overview](#overview) +* [1. Problem Statement and Motivation](#1-problem-statement-and-motivation) + * [1.1 The Incompatibility: Closed Enums vs. Living Operating Systems](#11-the-incompatibility-closed-enums-vs-living-operating-systems) + * [1.2 Exhaustiveness and Breaking Changes in Dart 3](#12-exhaustiveness-and-breaking-changes-in-dart-3) + * [1.3 The Two Extensibility Dimensions](#13-the-two-extensibility-dimensions) + * [1.4 Historical Context and Prior Art](#14-historical-context-and-prior-art) +* [2. Design Goals and Principles](#2-design-goals-and-principles) + * [2.1 Goals](#21-goals) + * [2.2 Non-Goals](#22-non-goals) + * [2.3 Decision Guide: Standard Enum vs. Extensible Constant Class](#23-decision-guide-standard-enum-vs-extensible-constant-class) +* [3. The Extensible Constant Pattern Specification](#3-the-extensible-constant-pattern-specification) + * [3.1 Canonical Class Anatomy](#31-canonical-class-anatomy) + * [3.2 Value Equality and `switch` Compatibility](#32-value-equality-and-switch-compatibility) + * [3.3 Discovery via `knownValues` vs. `values`](#33-discovery-via-knownvalues-vs-values) + * [3.4 Platform Specialization Patterns](#34-platform-specialization-patterns) + * [3.5 Serialization and Wire Protocols](#35-serialization-and-wire-protocols) +* [4. Primary Case Study: `camera` and `ImageFormatGroup`](#4-primary-case-study-camera-and-imageformatgroup) + * [4.1 The Breaking Incident (PR #11765)](#41-the-breaking-incident-pr-11765) + * [4.2 Applying the Extensible Constant Pattern](#42-applying-the-extensible-constant-pattern) + * [4.3 Consumer and Platform Implementation Ergonomics](#43-consumer-and-platform-implementation-ergonomics) +* [5. First-Party Plugin Applications in `flutter/packages`](#5-first-party-plugin-applications-in-flutterpackages) + * [5.1 Biometric Authentication (`local_auth`)](#51-biometric-authentication-local_auth) + * [5.2 Media Formats and Stream Protocols (`video_player`)](#52-media-formats-and-stream-protocols-video_player) + * [5.3 File Type Groups and Uniform Type Identifiers (`file_selector`)](#53-file-type-groups-and-uniform-type-identifiers-file_selector) + * [5.4 Store Purchase and Proration Modes (`in_app_purchase`)](#54-store-purchase-and-proration-modes-in_app_purchase) +* [6. Options Considered](#6-options-considered) + * [Option 1: Enum with a Private Fallback Value](#option-1-enum-with-a-private-fallback-value) + * [Option 2: Extension Types (Dart 3.3+)](#option-2-extension-types-dart-33) + * [Option 3: Interface Class with Static Constants](#option-3-interface-class-with-static-constants) + * [Option 4: Raw Primitives (Strings / Integers)](#option-4-raw-primitives-strings--integers) + * [Option 5: Open Class Hierarchy (Polymorphism)](#option-5-open-class-hierarchy-polymorphism) + * [Option 6: Class with Properties and Extension Getters](#option-6-class-with-properties-and-extension-getters) + * [Option 7: Final Class with Equals/HashCode Override (Adopted Pattern)](#option-7-final-class-with-equalshashcode-override-adopted-pattern) + * [Option 8: Base Class as Primitive with Subclasses](#option-8-base-class-as-primitive-with-subclasses) + * [Comprehensive Comparison Matrix](#comprehensive-comparison-matrix) +* [7. Migration Strategies and Open Questions](#7-migration-strategies-and-open-questions) + * [7.1 Migration Path for Existing Packages](#71-migration-path-for-existing-packages) + * [7.2 Open Questions](#72-open-questions) + +--- + +## 1. Problem Statement and Motivation + +### 1.1 The Incompatibility: Closed Enums vs. Living Operating Systems + +Federated plugins decouple an application-facing package from its platform-specific implementations via a shared platform interface package. While the Dart programming language models `enum`s as finite, exhaustive sets, host operating systems are inherently open and continuously evolving. + +Each annual OS release (e.g., Android API bumps, iOS updates, Windows SDKs, Web standards) exposes new hardware capabilities, formats, and configurations. When a plugin wraps native APIs whose domains expand over time, treating the domain as a closed set fundamentally conflicts with the underlying platform lifecycle. + +### 1.2 Exhaustiveness and Breaking Changes in Dart 3 + +Dart 3 enhanced pattern matching and exhaustiveness checking for `switch` statements and expressions. If an application developer writes: + +```dart +// Consumer code switching over an enum without a default branch: +String formatName = switch (cameraFormat) { + ImageFormatGroup.yuv420 => 'YUV 420', + ImageFormatGroup.jpeg => 'JPEG', + ImageFormatGroup.nv21 => 'NV21', +}; +``` + +When the platform interface adds a single new enum value (e.g., `ImageFormatGroup.rgba8888`), Dart's compiler issues an error on the consumer's switch: + +```text +The type 'ImageFormatGroup' is not exhaustively matched by the switch cases. +Missing cases: +- ImageFormatGroup.rgba8888 +``` + +Because adding an enum value breaks downstream compilation, every addition technically constitutes a breaking change under Semantic Versioning (SemVer), necessitating a major version bump (`1.x.x` $\rightarrow$ `2.0.0`) of the platform interface and all federated packages that depend on it. + +### 1.3 The Two Extensibility Dimensions + +Architectural discussions across `flutter/packages` surface two distinct extensibility dimensions that a unified solution must accommodate: + +1. **Common Platform Interface Evolution (Horizontal):** The core platform interface package introduces new constants supported across multiple platforms over time (e.g., adding a format common to newer Android and iOS SDKs). +2. **Platform Implementation Specialization (Vertical):** A specific platform implementation (e.g., `camera_android`, `camera_windows`, or a community desktop package) needs to introduce platform-specific constants that do not apply to other platforms or are not yet represented in the shared platform interface. + +### 1.4 Historical Context and Prior Art + +* **Flutter Packages Issue (PR [#11765](https://github.com/flutter/packages/pull/11765)):** Adding `rgba8888` to `ImageFormatGroup` in `camera_platform_interface` triggered a breaking change across dependent packages, precipitating this RFC. +* **Dart Native Systems (`dart-lang/native#3413`):** The Dart Native and FFI team confronted this exact challenge when modeling host operating systems (`OS`) and CPU architectures (`Architecture`). Rather than using an `enum`, they adopted a `final class` pattern with `fromString` constructors and value equality, allowing third-party platforms to extend the system safely. + +--- + +## 2. Design Goals and Principles + +### 2.1 Goals + +1. **Non-breaking Additions:** Adding new constants to the platform interface must not break compilation for existing consumers or implementations. +2. **Platform Specialization:** Allow individual platform packages to define and forward custom, platform-specific constants through the shared interface. +3. **Type Safety & Ergonomics:** Maintain compile-time type safety, strong typing, namespace scoping, and IDE autocompletion. +4. **Pattern Matching & Control Flow:** Ensure clean integration with Dart 3 `switch` statements and switch expressions without runtime overhead. +5. **Serialization:** Support trivial serialization to and deserialization from primitive strings for JSON and platform channel messaging. + +### 2.2 Non-Goals + +1. **Deprecating Genuinely Closed Enums:** Replacing enums that represent fixed domain logic (e.g., boolean-like states or cardinal orientations). +2. **Reworking Pigeon Code Generation:** Replacing Pigeon's internal wire protocol serializers, which handle their own code-generated fallback rules. + +### 2.3 Decision Guide: Standard Enum vs. Extensible Constant Class + +To guide package authors, this RFC establishes a clear decision framework: + +| Domain Characteristic | Use Standard Dart `enum` | Use Extensible Constant Pattern | +| :--- | :---: | :---: | +| **Domain Lifecycle** | Strictly closed and mathematically or logically bounded (e.g., `CameraLensDirection`: `front`, `back`, `external`). | Open and dictated by host OS features (e.g., pixel formats, codecs, sensor types). | +| **Platform Additions Expected** | Unlikely to ever change without a complete architectural redesign. | Expected to grow as mobile/desktop OSs add capabilities. | +| **Platform-Specific Subtypes** | No platform can define bespoke values. | Platforms need bespoke values (e.g., Windows BGRA, Android YV12). | +| **Exhaustiveness Requirement** | Desirable: compiler must guarantee every single case is handled. | Anti-pattern: consumers must be forced to provide a fallback/default branch. | + +--- + +## 3. The Extensible Constant Pattern Specification + +### 3.1 Canonical Class Anatomy + +The recommended standard for extensible constants is a `final class` that wraps a primitive `name` identifier, with private and public named constructors, value equality, and static constant definitions: + +```dart +/// Represents an extensible set of constants. +final class ExtensibleConstant { + /// The underlying string identifier of this constant. + final String name; + + // Private constructor for core constants defined within the package. + const ExtensibleConstant._(this.name); + + /// Creates a constant from an arbitrary string value. + /// + /// Used by platform implementations and application code to instantiate + /// custom or platform-specific constants. + const ExtensibleConstant.fromString(this.name); + + // Core predefined constants: + static const ExtensibleConstant firstOption = + ExtensibleConstant._('firstOption'); + static const ExtensibleConstant secondOption = + ExtensibleConstant._('secondOption'); + + /// Predefined constants known at compile time to this package. + /// + /// Does not include custom or third-party platform constants. + static const List knownValues = [ + firstOption, + secondOption, + ]; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ExtensibleConstant && other.name == name; + + @override + int get hashCode => name.hashCode; + + @override + String toString() => 'ExtensibleConstant.$name'; +} +``` + +### 3.2 Value Equality and `switch` Compatibility + +Because `operator ==` and `hashCode` are overridden based on `name`: + +1. Two instances with matching `name` strings evaluate as equal (`identical` fast-path ensures zero overhead for static `const` instances). +2. Dart 3 `switch` statements and expressions match instances by equality cleanly. +3. Because the class is an open type (not an `enum` or `sealed class`), the Dart compiler naturally requires a wildcard pattern (`_ =>`) or `default:` clause, eliminating future compilation breaks when new constants are added. + +```dart +String handleConstant(ExtensibleConstant value) { + return switch (value) { + ExtensibleConstant.firstOption => 'Handled First Option', + ExtensibleConstant.secondOption => 'Handled Second Option', + _ => 'Handled fallback: ${value.name}', + }; +} +``` + +### 3.3 Discovery via `knownValues` vs. `values` + +Standard enums expose a built-in `.values` property. In an extensible domain, the core package cannot know about values instantiated at runtime or in separate packages. + +Providing a static list named `values` creates an illusion of exhaustiveness that leads to runtime defects (e.g., if a UI loop assumes `.values` contains all valid options). + +* Packages adopting this pattern **MUST NOT** name their discovery list `values`. +* Packages **SHOULD** name the collection `knownValues` to explicitly signal that it represents only the constants known to the core package at compile time. + +### 3.4 Platform Specialization Patterns + +When a platform implementation needs to introduce bespoke constants, it can choose between two ergonomic patterns: + +#### Pattern A: Extension Methods (Recommended for most plugins) + +The platform package defines an `extension` on the core class, exposing static constants or getters: + +```dart +// In package:camera_windows/camera_windows.dart: +extension WindowsImageFormats on ImageFormatGroup { + /// Windows-specific BGRA format. + static final ImageFormatGroup bgra8888 = + ImageFormatGroup.fromString('bgra8888'); +} +``` + +#### Pattern B: Platform Subclassing (When platform provenance is paramount) + +If the platform interface uses `base class` instead of `final class` (Option 8), platform implementations can define a distinct subclass: + +```dart +// In package:camera_android/camera_android.dart: +final class AndroidImageFormatGroup extends ImageFormatGroup { + const AndroidImageFormatGroup(super.value); + + static const AndroidImageFormatGroup bgra8888 = + AndroidImageFormatGroup('bgra8888'); +} +``` + +This pattern makes platform origin explicit in code (`AndroidImageFormatGroup.bgra8888`) and allows runtime type guards (`if (format is AndroidImageFormatGroup)`). + +### 3.5 Serialization and Wire Protocols + +Because instances are backed by a primitive `name` string: + +* **Serialization:** Passing across platform channels or JSON payloads requires only reading `instance.name`. +* **Deserialization:** Reconstructing an instance from native messages uses `ExtensibleConstant.fromString(rawString)`. Unknown strings sent from newer native OS versions deserialize into valid instances without failing. + +--- + +## 4. Primary Case Study: `camera` and `ImageFormatGroup` + +### 4.1 The Breaking Incident (PR #11765) + +In `camera_platform_interface`, `ImageFormatGroup` was originally defined as: + +```dart +// Legacy definition in camera_platform_interface: +enum ImageFormatGroup { + unknown, + yuv420, + jpeg, + nv21, +} +``` + +When PR [#11765](https://github.com/flutter/packages/pull/11765) added `rgba8888` to support modern camera capture pipelines, consumer apps and test suites (such as `android_camera_camerax_test`) immediately broke because their `switch` statements did not account for `rgba8888`. + +### 4.2 Applying the Extensible Constant Pattern + +Migrating `ImageFormatGroup` to the Extensible Constant Pattern resolves the issue: + +```dart +final class ImageFormatGroup { + /// The string identifier of this format. + final String name; + + const ImageFormatGroup._(this.name); + + /// Creates a custom or platform-specific image format group. + const ImageFormatGroup.fromString(this.name); + + static const ImageFormatGroup unknown = ImageFormatGroup._('unknown'); + static const ImageFormatGroup yuv420 = ImageFormatGroup._('yuv420'); + static const ImageFormatGroup jpeg = ImageFormatGroup._('jpeg'); + static const ImageFormatGroup nv21 = ImageFormatGroup._('nv21'); + static const ImageFormatGroup rgba8888 = ImageFormatGroup._('rgba8888'); + + /// Formats known at compile time to camera_platform_interface. + static const List knownValues = [ + unknown, + yuv420, + jpeg, + nv21, + rgba8888, + ]; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ImageFormatGroup && other.name == name; + + @override + int get hashCode => name.hashCode; + + @override + String toString() => 'ImageFormatGroup.$name'; +} +``` + +### 4.3 Consumer and Platform Implementation Ergonomics + +* **Consumer Switches:** Existing switch statements continue to work cleanly by adding a default branch: + + ```dart + String formatDescription(ImageFormatGroup format) { + return switch (format) { + ImageFormatGroup.yuv420 => 'YUV 420 for live preview streaming', + ImageFormatGroup.jpeg => 'Standard compressed photographic capture', + ImageFormatGroup.rgba8888 => 'Raw 32-bit pixel buffer for compute', + _ => 'Platform-specific or custom format (${format.name})', + }; + } + ``` + +* **Adding Future Formats:** When iOS adds a new ProRes RAW pixel format or Android adds a specialized depth buffer format, `camera_platform_interface` can add `static const ImageFormatGroup proResRaw` in a minor (`4.x.0`) release with zero risk of breaking downstream code. + +--- + +## 5. First-Party Plugin Applications in `flutter/packages` + +The challenges resolved in `camera` recur across multiple first-party federated plugins maintained by the Flutter team in [`flutter/packages`](https://github.com/flutter/packages). Adopting this standard provides immediate consistency and safety across official packages: + +### 5.1 Biometric Authentication (`local_auth`) + +`local_auth` currently models biometric modalities as `enum BiometricType { face, fingerprint, iris, weak, strong }`. As host operating systems introduce new biometric modalities (such as palm recognition, vein pattern matching, or passive ambient authentication), `BiometricType` can adopt the extensible constant pattern: + +```dart +final class BiometricType { + final String name; + const BiometricType._(this.name); + const BiometricType.fromString(this.name); + + static const BiometricType face = BiometricType._('face'); + static const BiometricType fingerprint = BiometricType._('fingerprint'); + static const BiometricType iris = BiometricType._('iris'); + static const BiometricType weak = BiometricType._('weak'); + static const BiometricType strong = BiometricType._('strong'); + + static const List knownValues = [ + face, + fingerprint, + iris, + weak, + strong, + ]; + + @override + bool operator ==(Object other) => + identical(this, other) || other is BiometricType && other.name == name; + + @override + int get hashCode => name.hashCode; + + @override + String toString() => 'BiometricType.$name'; +} +``` + +### 5.2 Media Formats and Stream Protocols (`video_player`) + +The `video_player_platform_interface` package defines contracts for native media playback. As platform-specific backends (`video_player_android`, `video_player_avfoundation`, `video_player_web`) evolve, host operating systems introduce new streaming protocols (such as HLS, DASH, SmoothStreaming) and HDR surface formats (such as Dolby Vision, HDR10+, HLG, and AV1). + +Modeling stream protocols and pixel formats as extensible constants prevents downstream breaking changes whenever a platform package adopts a modern platform-native format or hardware acceleration profile. + +### 5.3 File Type Groups and Uniform Type Identifiers (`file_selector`) + +In `file_selector_platform_interface`, file type filters (`XTypeGroup`) map to operating system file specifications, including Uniform Type Identifiers (UTIs) on Apple platforms (`public.image`, `public.heic`, `public.audio`), MIME types, and file extensions. + +Because operating systems periodically introduce new system UTIs and media container groups, modeling platform file category identifiers as extensible constants ensures that native platform packages (`file_selector_macos`, `file_selector_windows`, `file_selector_linux`) can support new native file classes without breaking existing implementations. + +### 5.4 Store Purchase and Proration Modes (`in_app_purchase`) + +In `in_app_purchase_platform_interface`, transaction lifecycles and subscription upgrade modes bridge to Google Play Billing (Android) and StoreKit (iOS/macOS). + +When mobile store backends introduce new billing features, purchase verification states, or subscription replacement modes (such as immediate with time proration, deferred, or charge full price), extensible constants allow store implementations (`in_app_purchase_android`, `in_app_purchase_storekit`) to propagate native states through the platform interface safely. + +--- + +## 6. Options Considered + +### Option 1: Enum with a Private Fallback Value + +Add a private sentinel member (e.g., `_fallback`) to a standard enum to force consumers to write a `default:` case. + +```dart +enum ImageFormatGroup { + yuv420, + jpeg, + nv21, + rgba8888, + _fallback, +} +``` + +* **Pros:** Familiar syntax; forces `default` branches in consumer code. +* **Cons:** `ImageFormatGroup.values` includes `_fallback`, polluting UI iterations; does not allow third-party or platform implementations to instantiate custom constants; subverts Dart's type semantics. + +--- + +### Option 2: Extension Types (Dart 3.3+) + +Wrap a primitive `String` with an `extension type`: + +```dart +extension type const ImageFormatGroup._(String value) { + static const ImageFormatGroup unknown = ImageFormatGroup._('unknown'); + static const ImageFormatGroup yuv420 = ImageFormatGroup._('yuv420'); + static const ImageFormatGroup rgba8888 = ImageFormatGroup._('rgba8888'); +} +``` + +* **Pros:** Zero runtime overhead (erased to `String` at runtime); non-breaking constant additions; natural switch fallback requirement. +* **Cons:** No built-in list discovery; raises minimum SDK constraint to Dart 3.3+; unfamiliar ergonomics for junior contributors. + +--- + +### Option 3: Interface Class with Static Constants + +Use `interface class` with static `const` instances and a private constructor: + +```dart +interface class ImageFormatGroup { + const ImageFormatGroup._(); + static const ImageFormatGroup unknown = ImageFormatGroup._(); + static const ImageFormatGroup yuv420 = ImageFormatGroup._(); +} +``` + +* **Pros:** Non-breaking additions; allows third parties to `implement` the class. +* **Cons:** Instances sharing a parameterless constructor evaluate as identical, breaking distinct equality unless internal fields are added; verbose boilerplate. + +--- + +### Option 4: Raw Primitives (Strings / Integers) + +Pass unencapsulated `String` or `int` constants across APIs: + +```dart +const String formatYuv420 = 'yuv420'; +``` + +* **Pros:** Utmost simplicity; zero overhead. +* **Cons:** Zero compile-time type safety; prone to typos; poor autocompletion and documentation discovery. + +--- + +### Option 5: Open Class Hierarchy (Polymorphism) + +Represent each constant as a discrete class implementing an unsealed interface: + +```dart +abstract class ImageFormatGroup {} +class Yuv420Format implements ImageFormatGroup {} +``` + +* **Pros:** Non-breaking additions; formats can carry complex payloads. +* **Cons:** Excessive boilerplate and over-engineering for simple domain tags. + +--- + +### Option 6: Class with Properties and Extension Getters + +Use a `base class` without equality overrides, testing values via boolean getters: + +```dart +base class ImageFormatGroup { + const ImageFormatGroup(this.value); + final String value; + + static const ImageFormatGroup unknown = ImageFormatGroup('unknown'); + static const ImageFormatGroup yuv420 = ImageFormatGroup('yuv420'); + + bool get isUnknown => value == 'unknown'; + bool get isYuv420 => value == 'yuv420'; +} +``` + +* **Pros:** Non-breaking; extensible via extension getters; compatible as keys in `const` maps and sets because `operator ==` is not overridden. +* **Cons:** Incompatible with `switch` statements; requires chaining verbose `if-else` blocks. + +--- + +### Option 7: Final Class with Equals/HashCode Override (Adopted Pattern) + +The pattern specified in [Section 3](#3-the-extensible-constant-pattern-specification), inspired by `dart-lang/native#3413`. + +* **Pros:** Seamless `switch` support; extensible via constructor and extensions; trivial string serialization; robust type safety. +* **Cons:** Cannot be used as keys in `const` maps or sets due to `operator ==` override. + +--- + +### Option 8: Base Class as Primitive with Subclasses + +Use a `base class` with value equality, allowing platform implementations to inherit: + +```dart +base class ImageFormatGroup { + final String value; + @protected + const ImageFormatGroup(this.value); + + @override + bool operator ==(Object other) => + identical(this, other) || other is ImageFormatGroup && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +// In camera_android: +final class AndroidImageFormatGroup extends ImageFormatGroup { + const AndroidImageFormatGroup(super.value); + static const AndroidImageFormatGroup bgra8888 = AndroidImageFormatGroup('bgra8888'); +} +``` + +* **Pros:** Explicit platform provenance; supports runtime type checks (`is AndroidImageFormatGroup`). +* **Cons:** Base-typed variables cannot directly access subclass constants without extension getters or explicit equality comparisons. + +--- + +### Comprehensive Comparison Matrix + +| Option | Non-breaking Evolution | `switch` Support | Platform Extensible | Type Safe & Autocomplete | `const` Map/Set Key | Runtime Overhead | +| :--- | :---: | :---: | :---: | :---: | :---: | :---: | +| **Option 1: Enum with Fallback** | ⚠️ Partial | ✅ Yes | ❌ No | ✅ High | ✅ Yes | Minimal | +| **Option 2: Extension Types** | ✅ Yes | ✅ Yes | ⚠️ Via Constructor | ✅ High | ✅ Yes | Zero | +| **Option 3: Interface Class** | ✅ Yes | ❌ No | ✅ Implements | ✅ High | ⚠️ Const issue | Low | +| **Option 4: Raw Primitives** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ None | ✅ Yes | Zero | +| **Option 5: Class Hierarchy** | ✅ Yes | ⚠️ Type switch | ✅ Subclass | ✅ High | ❌ No | Moderate | +| **Option 6: Class with Properties** | ✅ Yes | ❌ `if-else` | ✅ Extension | ✅ High | ✅ Yes | Low | +| **Option 7: Adopted Pattern (`final class` + `==`)** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ High | ❌ No | Low | +| **Option 8: Base Class + Subclasses** | ✅ Yes | ✅ Yes | ✅ Subclass | ✅ High | ❌ No | Low | + +--- + +## 7. Migration Strategies and Open Questions + +### 7.1 Migration Path for Existing Packages + +For packages currently exposing standard enums (such as `ImageFormatGroup` or `BiometricType`), migrating to the Extensible Constant Pattern requires careful version management: + +1. **Step 1 (Deprecation Window):** Deprecate the existing `enum` with a target removal milestone, introducing the replacement class under a distinct name if necessary, or execute a single coordinated major bump (`N.0.0`) of the platform interface. +2. **Step 2 (Forwarder in App-Facing Package):** App-facing packages can translate the extensible class back to legacy enum values for deprecated getters while exposing the new class for modern APIs. +3. **Step 3 (Lint Guidance):** Maintainers should provide documentation examples showing developers how to convert exhaustive `switch` statements by adding `_ =>` default branches. + +### 7.2 Open Questions + +1. **Ecosystem-wide Linter Rule:** Should Flutter introduce a custom `dart analyze` lint or package rule that flags standard `enum` usage in `*_platform_interface` packages when domain sets represent external OS APIs? +2. **Extension Types in Public APIs:** As Dart SDK minimums advance across all first-party packages, should the ecosystem reconsider Extension Types (Option 2) for zero-cost abstraction, or does class-based nominal typing remain preferred for API clarity?