diff --git a/rfc/710.0000-style-guide-enums.md b/rfc/710.0000-style-guide-enums.md new file mode 100644 index 0000000..c48f122 --- /dev/null +++ b/rfc/710.0000-style-guide-enums.md @@ -0,0 +1,123 @@ +--- +type: rfc +rfc: '710.0000' +title: Style guide changes for enums +description: Updates the Flutter style guide and breaking change policy for enums and enum-like classes. +status: draft +created: 2026-10-08T00:00:00Z +updated: 2026-10-08T18:55:00Z +tags: + - 710-style-guide + - 030-breaking-changes +authors: + - https://github.com/chunhtai +--- + +# RFC 710.0000: Style guide changes for enums + +## Overview + +This document proposes updating the [Flutter repository style guide](https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md) and [Flutter breaking change policy](https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes) to establish conventions for public enums and enum-like classes. + +* For public APIs, use a standard `enum` when the set of values is closed by nature or when consumers must handle every value exhaustively; otherwise, prefer an enum-like class with `static const` fields. +* Adding a value to a public `enum` (or a new subtype to a public `sealed class`) breaks exhaustive switches and requires going through the breaking change process. +* These rules apply to new APIs; migrating existing public `enum` declarations is left to case-by-case evaluation. + +## Context + +Inside the Flutter codebase, requiring exhaustive `switch` statements and expressions over `enum` values (without `default` branches) is intentional and remains unchanged. However, when external callers or `flutter/packages` switch exhaustively over a public `enum`, adding a new value breaks downstream compilation and rolls. + +Without a library-level convention in the style guide, public APIs in the framework are inconsistent: + +* `TargetPlatform` and `AppLifecycleState` are `enum` declarations even though platforms and OS lifecycle states grow over time. Adding new values breaks callers. +* `TextInputType` and `LogicalKeyboardKey` use classes with `static const` fields to allow non-breaking additions, but the style guide does not document when or how to use this pattern. + +## Proposal + +### Non-public enums + +Library-private (`_`) and `@internal` enums must use a standard `enum` declaration so framework `switch` statements and expressions are exhaustively checked. + +### Public APIs + +Use the following decision order to choose between a standard `enum` and an enum-like class: + +1. If the set of values is closed by nature (such as `Axis` with `horizontal` and `vertical`, `VerticalDirection` with `up` and `down`, or `GrowthDirection`), use a standard `enum`. +2. Otherwise, if consumers receiving the value from a callback, builder, or state object must handle every case because no safe fallback exists (such as `ConnectionState` in `FutureBuilder` and `StreamBuilder`, or `TextSelectionHandleType` in `TextSelectionControls.buildHandle`), use a standard `enum`. +3. Otherwise, prefer an enum-like class with `static const` values (such as `TextInputType` and `LogicalKeyboardKey`; existing enums like `TargetPlatform` and `AppLifecycleState` also fit this category). + +#### Standard `enum` (exhaustive) + +* Provides compile-time exhaustiveness checking for `switch` statements and expressions. +* Adding a value to a public `enum` is a breaking change and requires going through the breaking change process. The same rule applies to adding a subtype to a public `sealed class`. + +#### Enum-like class with `static const` values (non-exhaustive) + +Implement as an `@immutable` `final` class with `static const` fields + +* Adding a `static const` field is not a breaking change. +* Marking the class `final` to prevent external packages from extending or implementing it. +* Use a private `const` constructor when only Flutter defines valid values (such as a platform or lifecycle state). +* Include a distinguishing field to prevent Dart `const` canonicalization from collapsing distinct constants into a single instance. Choose the field type based on the domain of the enum (such as `int` for hardware key codes); otherwise, prefer a `String name` field with the same value as the property name by default. + +##### Example: closed to external instantiation + +When only Flutter defines valid instances, use a private `const` constructor: + +```dart +@immutable +final class MyEnum { + const MyEnum._(this.name); + + final String name; + + static const MyEnum foo = MyEnum._('foo'); + static const MyEnum bar = MyEnum._('bar'); + + @override + String toString() { + return 'MyEnum.$name'; + } + + @override + bool operator ==(Object other) { + return other.name == name; + } + + @override + int get hashCode => name.hashCode; +} +``` + +##### Example: open to external instantiation + +When callers may construct custom instances, use a public `const` constructor: + +```dart +@immutable +final class MyEnum { + const MyEnum(this.name); + + final String name; + + static const MyEnum foo = MyEnum('foo'); + static const MyEnum bar = MyEnum('bar'); + + @override + String toString() { + return 'MyEnum.$name'; + } + + @override + bool operator ==(Object other) { + return other.name == name; + } + + @override + int get hashCode => name.hashCode; +} +``` + +### Scope and migration + +These conventions apply to new public APIs. Existing public `enum` declarations that represent open sets (such as `TargetPlatform` and `AppLifecycleState`) are not migrated by this proposal and should be evaluated case-by-case, such as when adding a new value already requires a breaking change.