Skip to content

Adds enum styleguide changes rfc - #32

Open
chunhtai wants to merge 9 commits into
flutter:mainfrom
chunhtai:enum
Open

chunhtai wants to merge 9 commits into
flutter:mainfrom
chunhtai:enum

Conversation

@chunhtai

@chunhtai chunhtai commented Oct 8, 2026

Copy link
Copy Markdown

Add style guide guideline for using enum in public API

Tracking issue: flutter/flutter#193960

Pre-launch Checklist

  • I read RFC 000.0001: Taxonomy and followed the file and path naming and metadata standards.
  • I read RFC 000.0002: Process and confirmed this proposal meets the threshold for a full RFC.
  • I read and agree to the Code of Conduct.
  • I signed the CLA.
  • I have linked an issue from flutter/flutter with the label design doc.
  • All existing and new tests are passing.
  • I have enabled "Allow edits from maintainers" on this PR so the bot can automatically assign an RFC number (or I will run dart run bin/assign_rfc_number.dart locally when instructed).

If you need help, consider asking for advice on the #hackers channel on Discord.

Comment thread rfc/710.0000-style-guide-enums.md Resolved

```dart
@immutable
final class MyEnum {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LongCatIsLooong

I can't find a good use case for sealed class without more work. let me know you have other ideas in mind

@LongCatIsLooong LongCatIsLooong Oct 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 Element class where an Element is either a ComponentElement or a RenderObjectElement
The TextSelection class where a selection is either collapsed (TextPosition) or a non-collapsed (uhh we don't have that subclass, but maybe NonCollapsedTextSelection).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

sealed class MyEnum {
  // this is private by default
  const MyEnum();
  static const MyEnum foo = MyEnum2(0);
  static const MyEnum bar = MyEnum2(1);

  static const List<MyEnum> values = <MyEnum>[foo, bar];
}

class MyEnum2 extends MyEnum {
  const MyEnum2(this.index);
  final int index;
}


```dart
@immutable
final class MyEnum {

@LongCatIsLooong LongCatIsLooong Oct 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 Element class where an Element is either a ComponentElement or a RenderObjectElement
The TextSelection class where a selection is either collapsed (TextPosition) or a non-collapsed (uhh we don't have that subclass, but maybe NonCollapsedTextSelection).


```dart
@immutable
final class MyEnum {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 hashCode and == boilerplates, has value semantics if the underlying type is a value type. If you rewrite the MyEnum example:

extension type const MyEnum._(int index) { 
  static const MyEnum foo = MyEnum._(0);
}

@loic-sharma loic-sharma Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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]}';
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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 MyEnum(1) == MyAnotherEnum(1) will be true

@LongCatIsLooong LongCatIsLooong Oct 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah unfortunately you won't be able to override toString. Also I just realized another library can easily implement your extension type (so if you can't turn that extension type into an enum without breaking those implementers)

Use a standard `enum` when the values are complete by definition within Flutter's model (such as `Axis` with `horizontal` and `vertical`, or `VerticalDirection` with `up` and `down`) or when consumers must handle every case without a fallback (such as layout-related enums like `MainAxisAlignment` or `WrapAlignment`).

* 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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Do you mean adding a new case to a closed enum? If we add a new value to TextDirection almost every app breaks so I doubt we will be able to get away with changes like that even if we have that in our breaking change policy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean adding a new case to a closed enum

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an enum to a open enum class is not a breaking change.

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 final class from outside of the class, but I think the dart team is looking to allow introducing static members to an existing namespace (via extension methods), when that's added to the language, adding a new "case" can introduce name conflicts in client code and that should be a breaking change IMO.

Comment thread rfc/710.0000-style-guide-enums.md Resolved

### 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.

@LongCatIsLooong LongCatIsLooong Oct 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should AppleLifecycleState have been made an "open" enum? I think there are cases developers / plugin authors want to handle that exhaustively, and when a new case is added to AppleLifecycleState they want to be notified when compiling against newer framework versions?

@chunhtai chunhtai Oct 9, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

@LongCatIsLooong LongCatIsLooong Oct 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.
What if I introduce a new state (because, say, Apple introduced a new state), and the contract becomes "after pause, you may either enter this new state or resumed", so if a client fails to handle this new case it will fail to update it's own state and gets stuck in a paused state?

@justinmc justinmc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I'm totally on board with this, just some comments on the details. Good call splitting it out.


Language-level non-exhaustive ("open") enums were requested in [dart-lang/sdk#63678](https://github.com/dart-lang/sdk/issues/63678), but the Dart team noted ([comment](https://github.com/dart-lang/sdk/issues/63678#issuecomment-5375670420)) that this feature is unlikely to be prioritized in the near future. 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.

Copy link
Copy Markdown

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?

Copy link
Copy Markdown
Author

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

@loic-sharma loic-sharma Oct 9, 2026 •

Copy link
Copy Markdown
Member

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 TargetPlatform in favor of a non-exhaustive enum that's open for extension by external code. Design TBD though :)

Comment thread rfc/710.0000-style-guide-enums.md Resolved
Comment thread rfc/710.0000-style-guide-enums.md Resolved
Comment thread rfc/710.0000-style-guide-enums.md Resolved

```dart
@immutable
final class MyEnum {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Comment thread rfc/710.0000-style-guide-enums.md Resolved
Comment thread rfc/710.0000-style-guide-enums.md Outdated

Use the following decision order when choosing between a standard `enum` and an enum-like class:

1. If the set of values is closed by nature (such as `Axis` or `VerticalDirection`), use a standard `enum`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider rewording this. Perhaps something like: If the set of values will not change....

For example, today iOS text fields have a fixed set of context menu buttons, which we could represent as an enum. However, iOS might add a new button in a future version. This is a case where you should use non-exhaustive enums.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment for the overview above where it states "is closed by nature".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, today iOS text fields have a fixed set of context menu buttons, which we could represent as an enum. However, iOS might add a new button in a future version. This is a case where you should use non-exhaustive enums.

I think context menu item button type is not closed by nature so the statement still hold.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though I think it should be enum because it is likely be handled exhaustively

@loic-sharma loic-sharma Oct 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though I think it should be enum because it is likely be handled exhaustively

I agree all cases should be matched, but enum's unreachable_switch_case lint disallows adding an default case to an exhaustive switch. You cannot "future proof" a switch on an enum. For example:

enum MyEnum { foo, bar }

String stringify(MyEnum e) {
  return switch (e) {
    MyEnum.foo => 'foo',
    MyEnum.bar => 'bar',
    // dart(unreachable_switch_case)
    // This case is covered by the previous cases.
    // Try removing the case clause, or restructuring the preceding patterns.
    _ => 'future proof against future MyEnum values',
  };
}

This forces users of the enum to be broken when new value are added, which is why I'd recommend against enums if new values might be added.

Comment thread rfc/710.0000-style-guide-enums.md Resolved
Comment thread rfc/710.0000-style-guide-enums.md Outdated
@override
String toString() {
final name = _names[index];
return '${objectRuntimeType(this, 'MyEnum')}(name: MyEnum.$name)';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, what do you think of simplifying this to:

Suggested change
return '${objectRuntimeType(this, 'MyEnum')}(name: MyEnum.$name)';
return 'MyEnum.$name';

Benefits:

  • The <type>.<name> pattern mirrors Enum.toString()'s behavior better
  • objectRuntimeType is overkill if nothing extends MyEnum.

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`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

@chunhtai chunhtai Oct 9, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

@LongCatIsLooong LongCatIsLooong Oct 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Use closed enum in public API only if

  • clients only produce enum values and can never consume the enum value (i.e., the framework is the sole consumer, so we can't let the client send us something we don't understand).
  • you have reasons to believe that the exhaustiveness needs to be part to the API contract (e.g., states of a state machine).

I'm for prefer exposing non-exhaustive enums when in doubt: if we use a final class then later decides enum is more appropriate, making that change can't be source breaking it seems?

Comment thread rfc/710.0000-style-guide-enums.md Outdated
Implement as an `@immutable` `final` class with `static const` fields

* Adding a `static const` field is not a breaking change.
* Marking the class `final` prevents external packages from using `implement` or `extend`, keeping future member additions non-breaking without triggering `switch` exhaustiveness.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For flutter/packages we want to be able to extend the enum-like class with platform specific values. (e.g. ImageFormat can be extended to AndroidImageFormat for Android only formats.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is also a doc that goes into more details about enums in federated plugins: flutter.dev/go/extensible-enums-plugins

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the wording to be a suggestion rather than a requirement

@bparrishMines bparrishMines Oct 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If final is a suggestion then I think we could also

  1. Recommend base be used instead in that case.
  2. Recommend adding @protected above the constructor when base is used.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enums are generally simple data classes and I found it a little strange to subtype data classes. Are there benefits for opening up the type for extending, over a final class with a public constructor?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For federated plugins specifically, yes. The main benefit is discoverability and readability. For example, the camera plugin supports 7 distinct platforms and contains an ImageFormat enum. If each platform wanted to add there own ImageFormats, it would be harder for the code writer/reader to know which formats are platform specific. e.g.

Future<void> aMethod(ImageFormat format) async {
  // Works on all platforms.
  await cameraController.takePicture(ImageFormat.jpg);
  await cameraController.takePicture(ImageFormat.png);

  // Only works on iOS.
  await cameraController.takePicture(ImageFormat.heic);

  // Only works on Android.
  await cameraController.takePicture(ImageFormat.nv12);
}

From experience, this with support for 7+ platforms doesn't scale very well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized you can get around this with extension

final class SystemUiOverlayStyle {
  const SystemUiOverlayStyle();
}
// In library B:
extension CustomOverlayStyles on SystemUiOverlayStyle {
  static const SystemUiOverlayStyle brandDark = SystemUiOverlayStyle();
  bool get isBrandDark => identical(this, brandDark);
}
void main() {
  const SystemUiOverlayStyle style = CustomOverlayStyles.brandDark;
  print(style.isBrandDark);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized you can get around this with extension

final class SystemUiOverlayStyle {
  const SystemUiOverlayStyle();
}
// In library B:
extension CustomOverlayStyles on SystemUiOverlayStyle {
  static const SystemUiOverlayStyle brandDark = SystemUiOverlayStyle();
  bool get isBrandDark => identical(this, brandDark);
}
void main() {
  const SystemUiOverlayStyle style = CustomOverlayStyles.brandDark;
  print(style.isBrandDark);
}

Actually I think that option is better than extending. The only thing I think you lose out on is

void aMethod(SystemUiOverlayStyle style) {
  // The name 'CustomOverlayStyles' isn't a type and can't be used in an 'is' expression. 
  if (style is CustomOverlayStyles) {
  }
} 

Though I don't think this is a common use case and I think there a benefit to not have to worry about full external implementations.

Comment thread rfc/710.0000-style-guide-enums.md Outdated
static const MyEnum foo = MyEnum._(0);
static const MyEnum bar = MyEnum._(1);

static const List<String> _names = <String>[

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very easy to mess up since we have to write this by hand. Instead of an int index, can we not just make the String name the value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely remembered there is a reason we went with int, but I couldn't remember now. cc @loic-sharma, do you remember why?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dart enums can carry types that are not ints, similarly I think we should not limit what the underlying type is, for instance if I want to define an AutofillHint non-exhaustive enum it makes a lot more sense to use string as the underlying type.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think main benefit is comparison, also gives enum orders which is useful in some cases. For example, _RouteLifecycle in navigator.dart.

I updated to prefer string by default

Comment thread rfc/710.0000-style-guide-enums.md Outdated
final int index;

static const MyEnum foo = MyEnum(0);
static const MyEnum bar = MyEnum(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If MyEnum(...) is public, what prevents external APIs from creating indexes that overlap?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, we can't do anything about it. I think the best We can do is to write a custom lint for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's one reason why using a String that matches the field name is more future proof. Collisions with a String is less likely for external APIs and if they do collide, then it's more likely they were representing the same value anyways.

@justinmc justinmc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Dash Forum on this RFC, @reidbaker was talking about using an int as an enum, similar to what Android does in some of the APIs it expects to add values to in the future. I think this is effectively the same thing as using static const values on a class as proposed by this RFC.

// A user's code.
void main(List<String> arguments) {
  final MyIntEnum myIntEnum = MyIntEnum.foo();

  final bool isCool = switch (myIntEnum) {
    MyIntEnum.foo => true,
    MyIntEnum.bar => false,
    // Or instead of throwing, I could gracefully handle an unknown future value.
    _ => throw Exception('Uknown value of MyIntEnum!'),
  };

  print('Is myIntEnum cool? $isCool.');
}

// Part of the Flutter SDK for the sake of this example.
class MyIntEnum {
  final int _index;

  const MyIntEnum.foo() : _index = 0;

  const MyIntEnum.bar() : _index = 1;

  const MyIntEnum.baz() : _index = 2;

  // Could add more in the future!
  // It's up to users whether they want to be broken by a new value or not.

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is MyIntEnum &&
        other.runtimeType == runtimeType &&
        other._index == _index;
  }

  @override
  int get hashCode => _index;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

⌂ Proxy