Repository navigation
Adds enum styleguide changes rfc #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c5236a9
7cf86b1
184759b
1c0b092
9a171d3
e966ce7
b2905a3
30046a9
87d088e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I rewrote this part to only focus on enum giving out to the developer to consume, otherwise they should all be enum-like. Let me know what you think @loic-sharma , @LongCatIsLooong , @justinmc
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This however has risk for developer override class though. If we want to be more strict, they we will likely need to go back to broader set like layout related enum such as MainAxisAlignment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to combine bullet point 1 and 2 since they seem to be saying the same thing? Basically something like
I'm for prefer exposing non-exhaustive enums when in doubt: if we use a |
||
| 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`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're not changing the breaking change policy right? So the wording here may have to change a bit to be put in the style guide.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not the exact wording. will rephrase when i actually put this in part of styleguide. I was thinking to have a section in breaking change to talk about what we are not consider breaking change, which will include this and package private. WDYT There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you mean adding a new case to a closed enum? If we add a new value to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think we all agree on this, not sure if we need a written rule for this case. I am thinking adding a clause like: Adding an enum to a open enum class is not a breaking change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm are we looking to change the breaking change definition in the breaking change policy? I mean today in dart I can't think of a way to introduce a static member to a |
||
|
|
||
| #### 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 { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't find a good use case for sealed class without more work. let me know you have other ideas in mind There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There aren't that many sealed classes in the framework yet if I recall, but there are several classes that could have been made sealed, from the top of my mind: The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's one I wrote... that probably shouldn't have been a sealed class based on the breaking changes addressed in this document. https://github.com/flutter/flutter/blob/55f060daa5c8fa9c7c7aefa579daa2e59fbe15bf/packages/flutter/lib/src/widgets/system_context_menu.dart#L240
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sealed class can't have a public constructor, so it seems like the compiler will ask for exhaustive match There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think extension types are cut out for this: not subtypeable, no need for extension type const MyEnum._(int index) {
static const MyEnum foo = MyEnum._(0);
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another benefit of extension types is that can be used in a const set or as the key in a const map. However, I'd recommend against extension types because they negatively impact the debugging experience: dart-lang/sdk#53461 Demo of debugging experience of extension types...void main(List<String> arguments) {
var e1 = E1.foo; // e1 = E1 (E1.foo)
var e2 = E2.foo; // e2 = 1
var e3 = E3.foo; // e3 = E3 (E3.foo)
}
enum E1 { foo, bar }
extension type const E2._(int index) {
static const E2 foo = E2._(0);
static const E2 bar = E2._(1);
// Extension types can't declare members with the same name as a member declared by 'Object'.
// Try specifying a different name for the member.
// dart(extension_type_declares_member_of_object)
// @override
// String toString() => 'E2.$value';
}
@immutable
final class const E3._(final int index) {
static const E3 foo = E3._(0);
static const E3 bar = E3._(1);
static const List<String> _names = ['foo', 'bar'];
@override
bool operator ==(Object other) {
return other is E3 && other.index == index;
}
@override
int get hashCode => index.hashCode;
@override
String toString() => 'E3.${_names[index]}';
}
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extention type can cause some trouble for equality and toString for debugging, for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah unfortunately you won't be able to override |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you only have a private constructor, you can avoid overriding equals and hashcode. This will enable using the values as keys in const maps and sets. Even with adding new const instances latter equals and hashcode will work as expected because there is only ever the const instances. The only thing that you cannot do is later decide to make the constructor public. Because then equals and hashcode must be overriden which makes the values no fit in const maps as keys. Is the reason that you override hashcode and equals so that you can decide later to make an enum extensible? If, yes, document this in the RFC. |
||
| bool operator ==(Object other) { | ||
| return other.name == name; | ||
| } | ||
|
|
||
| @override | ||
| int get hashCode => name.hashCode; | ||
| } | ||
| ``` | ||
|
|
||
| ##### Example: open to external instantiation | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If useful, refer to prior art: |
||
|
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it is useful to explicitly state that overriding equals and hashcode is important for extensibility and pattern matching. If you don't override them pattern matching doesn't work for values created by third parties. |
||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be open. most people care about resume and pause only. IIRC the latest of the enum, detached, is added for very specific use case (for warmup). I think if we have to consider every possible usage. all the enum will need to be closed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's true but I think in general I would prefer state machine states to be exhaustive, as the state transition is usually an important part of the contract. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have an issue for these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we haven't added one yet for a long time, but mostly due to we stop adding more platform and prioritize mobile. but they are a lot likely to expand if we do so, and I imagine it will break a bunch of people
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For pluggable platforms, I plan to deprecate
TargetPlatformin favor of a non-exhaustive enum that's open for extension by external code. Design TBD though :)