Repository navigation
Conversation
justinmc
left a comment
There was a problem hiding this comment.
I'm definitely on board with having recommendations for when to use enum vs. static const.
My main concern is that allowing non-exported code will result in a proliferation of private files, and that users will import them by path regardless of what the styleguide says. This could result in even more time spent dealing with breaking changes than before. Even though we state that we don't consider these to be breaking changes, usage by g3 customers and customer_tests can still disrupt us.
I think that's the key question for me, do we think this could create more problems than it solves?
Some other questions:
- Are these rules going to be enforced by our linter or analyzer? Like enforcing that things marked
@internalare not imported from outside the library. - Are we using any files that are not exported today? Besides feature-flagged experimental APIs like desktop multiwindow.
- Do you have anything in mind for where you want to start using internal code?
|
|
||
| * Declarations marked `@internal` are outside the public API contract; modifying or removing them is not a breaking change. | ||
|
|
||
| * For public APIs, a standard `enum` is used when consumers must handle all values exhaustively (making new values a breaking change), and a class with `static const` fields is used when consumers need a fallback (making new values non-breaking). |
There was a problem hiding this comment.
I agree with this. It's good to have an official recommendation for when to use these. This is even more important now that adding an enum will break the flutter/packages autoroller; we can't accidentally sneak in a new enum value under the radar any more.
| #### Placement rules | ||
|
|
||
| * `@internal` may be applied to top-level declarations and to `static` members or constructors of public classes inside `lib/src/`. | ||
| * In any file in `lib/src/` not exported by a barrel file, all public top-level declarations must be annotated with `@internal`. |
| ### Removing discouragement of `@visibleForTesting` | ||
|
|
||
| The style guide section "Avoid using `@visibleForTesting`" advises against `@visibleForTesting` on public declarations, preferring APIs testable through their public interfaces, though contributors still use it when necessary. Should the style guide remove its discouragement of `@visibleForTesting` or keep the current stance? |
There was a problem hiding this comment.
I can't use an API marked @internal in a test, right?
This styleguide rule is one I disagreed with when I first started on the team, but I've since gotten used to it. I want to say that I agree with the recommendation to not kid ourselves about visibleForTesting being effectively public, and so we should keep the rule as-is.
There was a problem hiding this comment.
I can't use an API marked @internal in a test, right?
Yes they serve different purposes. so I don't think we can replace visibleForTesting with internal
I want to say that I agree with the recommendation to not kid ourselves about visibleForTesting being effectively public, and so we should keep the rule as-is.
did you mean we should state that changing API that is visibleForTesting is not breaking change, and keep the discouragement as is?
| ### Adopting `@experimental` for cutting-edge development | ||
|
|
||
| New public APIs immediately fall under the breaking change policy, requiring an RFC, deprecation cycle, and migration guide to modify or remove. Should Flutter adopt [`@experimental`](https://pub.dev/documentation/meta/latest/meta/experimental-constant.html) from `package:meta` to exempt cutting-edge APIs from the breaking change policy? |
There was a problem hiding this comment.
We already have feature flags for this, I think no change is needed.
There was a problem hiding this comment.
The reason I bring this up is that should we abandon feature flag approach and just mark something as @Experiemental and treat the change to the API as non breaking change?
On the other hand, we can also just use @internal here as well.
Also, using feature flag requires a lot of works as well, is it really worth it if we can just document our rule and lint warning against using these cutting-edge API
There was a problem hiding this comment.
I'd lean towards avoiding @experimental in the Flutter SDK. While @experimental declares that we reserve the right to change an API, @experimental doesn't actually make the change any less impactful. I'd argue the Flutter SDK is so foundational that the impact of changing an @experimental API would be too high. Furthermore, @experimental APIs are listed during code completions, making them much more visible than @internal APIs.
... should we abandon feature flag approach and just mark something as @Experiemental and treat the change to the API as non breaking change?
In my mind, these serve different use cases:
- API that should only be used by the framework: Use
@internal. - Public API that reserves the right to change: Use
@experimental. (Though again, I don't think the Flutter SDK should use@experimental) - APIs that aren't ready to be used in production: Gate behind a feature flag that cannot be enabled on stable and use
@internal.
On the other hand, we can also just use @internal here as well.
Agreed 👍
| ### Handling feature flag files and the cross-layer import rule | ||
|
|
||
| `lib/src/foundation/_features.dart` defines `@internal` feature flags consumed by higher layers (for example, `lib/src/widgets/binding.dart` imports `../foundation/_features.dart`). Because `@internal` symbols cannot be exported by `lib/foundation.dart`. What should be done about feature flags? |
There was a problem hiding this comment.
I can think of a few approach.
- abandon the feature flags and just use internal or experiemental
- move this to a share library that is not part of the exported package. (will need to look into whether this is possible).
There was a problem hiding this comment.
Is this a problem in practice? You need a funky relative import like import '../foundation/_features.dart';, but that seems OK?
Review comment for #30Thanks for writing this up. I agree with the underlying problems, but I think the document needs a structural rewrite. Comments are grouped below. 1. Split into separate RFCsI'd suggest splitting this into three RFCs:
I see the common theme, which is keeping APIs evolvable without going through the breaking change process. But the decisions can be made separately. The enum conventions are independent of the other two. The experimental RFC would build on Experimental APIs deserve their own RFC because they raise questions the other two don't: exempting public APIs from the breaking change policy; how 2.
|
I consider more private files are probably better, if we uses it when it fits. If we force everything to be public, we run into two problems
At least with "@internal" people will be warned to stay away from them.
I don't think so, for g3, they will get warning when they try to use the class, it is not any worse than if anything is forced to be public. for customer_tests, we can reject tests that use direct import. I think we will deal with breaking changes less.
I don't think so , they are currently all feature flagged based and desktop window related.
we can add lint to flutter_lint if they are not already in there.
system like Focus Management exposed a bunch of stuff like FocusAttachment |
|
Hi @dkwingsmt I only talked about visibleForTesting and experimental in open question because I think they serve a slightly different purposes and I am not too sure whether we want to adopt/change them in styleguide. just put it out in open question to see if the discussion lead us toward that direction. will revise the doc a bit based on suggestion |
| * Provides compile-time exhaustiveness checking. | ||
| * Adding a value to a public `enum` is a breaking change and requires to go through the breaking change process. | ||
|
|
||
| ##### Enum-like class with `static const` values (non-exhaustive) |
There was a problem hiding this comment.
FYI, Dart has a recommended lint exhaustive_cases that will enforce exhaustive checks on an enum-like class if it has no public constructor: https://dart.dev/tools/linter-rules/exhaustive_cases
| - https://github.com/chunhtai | ||
| --- | ||
|
|
||
| # RFC 710.0000: Style guide changes for package private and enums |
There was a problem hiding this comment.
nit: sealed classes are as closed / frozen as enums, what about them?
|
|
||
| ### Breaking change policy exemption | ||
|
|
||
| Modifying, renaming, or removing any declaration annotated with `@internal` is not a breaking change and requires no deprecation period or announcement. |
There was a problem hiding this comment.
Contributors making changes to @internal APIs can still be blocked even with this exemption, if a registered customer test is consuming @internal APIs (or worse if google testing is depending on that API), as they are still responsible for fixing the customer testing breakage even if not considered breaking?
There was a problem hiding this comment.
... if a registered customer test is consuming
@internalAPIs ...
I'd update the customer test guidance to state that customer tests must not use @internal framework APIs. If/when a contributor is blocked from changing an @internal API due to a customer test, the contributor should be allowed to disable that customer test.
|
|
||
| #### Placement rules | ||
|
|
||
| * `@internal` may be applied to top-level declarations and to `static` members or constructors of public classes inside `lib/src/`. |
There was a problem hiding this comment.
Loic talked about keeping the rules simple / easy to digest and keeping the complicated details in lint rules. It looks pretty doable to turn these rules into an analyzer plugin lint, instead of adding to the style guide (and/or using a simpler version of this in the style guide).
|
|
||
| The style guide discourages package-private APIs beyond library-private (`_`) declarations and requires every file in `lib/src/<layer>/` to be exported by `lib/<layer>.dart`. In practice, when file-scoped `_` privacy is too narrow, the framework already omits `_`-prefixed files in `lib/src/` from barrel exports (such as `lib/src/widgets/_window.dart` and `lib/src/foundation/_features.dart`). | ||
|
|
||
| Relying on filename conventions alone risks accidental leaks. Because `lib/src/` files are re-exported by default, moving code or sharing a non-private class within a layer can inadvertently expose it to external consumers, making future edits breaking changes. Marking these declarations `@internal` triggers an `invalid_internal_annotation` analyzer warning if exported, prompting contributors to move them to an unexported `_` file or hide them in the barrel export. |
There was a problem hiding this comment.
Is the motivation of introducing @internal being able to get an analyzer warning when incorrectly exported? So the updated style guide will still strongly discourage package-private APIs, but if you have to introduce one as there's no viable alternatives (which the reviewers must double check), mark them as @internal?
| Use a standard `enum` when consumers are expected to handle every case without a `default` or `_` branch (such as `Axis`, `AxisDirection`, `VerticalDirection`, and `GrowthDirection`). | ||
|
|
||
| * Provides compile-time exhaustiveness checking. | ||
| * Adding a value to a public `enum` is a breaking change and requires to go through the breaking change process. |
There was a problem hiding this comment.
nit: is a breaking change -> can be a breaking change, as those changes are not necessarily breaking according to our breaking change policy.
|
|
||
| ### Removing discouragement of `@visibleForTesting` | ||
|
|
||
| The style guide section "Avoid using `@visibleForTesting`" advises against `@visibleForTesting` on public declarations, preferring APIs testable through their public interfaces, though contributors still use it when necessary. Should the style guide remove its discouragement of `@visibleForTesting` or keep the current stance? |
There was a problem hiding this comment.
The
@visibleForTestingannotation marks a public API so that developers that have not disabled theinvalid_use_of_visible_for_testing_memberanalyzer error get a warning when they use this API outside of atestdirectory.This means that the API has to be treated as being public (nothing prevents a developer from using the API even in non-test code), meaning it must be designed to be a public API, it must be documented, it must be tested, etc. At which point, there's really no reason not to just make it a public API. If anything, the use of
@visibleForTestingbecomes merely a crutch to convince ourselves that it's ok that we're making something public that we should really not have made public.So rather than rely on
@visibleForTesting, consider designing your APIs so that they are directly testable using the public API, without exposing any sensitive internals.(One exception is combining
@visibleForTestingwith@protected. The@protectedannotation marks a member as one that is intended for subclasses, so it is already a public API and considered as such. The@visibleForTestingannotation in that case merely enables the member to be called directly in tests without having to create a fake subclass and without having to add//ignorepragmas.)
The existing guide seems pretty reasonable especially with the @protected exception (the member probably should be @nonVirtual too), and it doesn't say it's strictly forbidden?
| Use a `final` class with a private `const` constructor and `static const` fields when consumers handle a subset of values with a `default` or `_` fallback (such as `TextInputType` and `LogicalKeyboardKey`; existing enums like `TargetPlatform` and `AppLifecycleState` also fit this category). | ||
|
|
||
| * Adding a `static const` field is not a breaking change. | ||
| * Does not provide compile-time exhaustiveness checking. |
There was a problem hiding this comment.
For clarity, could we add an example of a non-exhaustive enum-like class that follows best practices? FYI there's some good prior art in this doc: flutter.dev/go/extensible-enums-plugins
There was a problem hiding this comment.
Maybe for non-exhaustive closed enums:
@immutable
class MyEnum {
@protected
const MyEnum(this.index);
final int index;
static const MyEnum foo = MyEnum._(0);
static const MyEnum bar = MyEnum._(1);
static const List<MyEnum> values = <MyEnum>[
foo,
bar,
];
static const List<String> _names = <String>[
'foo',
'bar',
];
String get _name => 'MyEnum.${_names[index]}';
@override
String toString() {
return '${objectRuntimeType(this, 'MyEnum')}(name: $_name)';
}
@override
bool operator ==(Object other) {
return other is MyEnum && other.index == index;
}
@override
int get hashCode => index.hashCode;
}Maybe for non-exhaustive open enums:
@immutable
class MyEnum {
@protected
const MyEnum(this.name);
final int name;
static const MyEnum foo = MyEnum._('foo');
static const MyEnum bar = MyEnum._('bar');
@override
String toString() {
return '${objectRuntimeType(this, 'MyEnum')}(name: $name)';
}
@override
bool operator ==(Object other) {
return other is MyEnum && other.name == name;
}
@override
int get hashCode => name.hashCode;
}
Proposing change to style guide around package private and enums in public APIs
Tracking issue: flutter/flutter#193960
This proposal directly contradicts flutter/flutter#108632
Pre-launch Checklist
design doc.dart run bin/assign_rfc_number.dartlocally when instructed).If you need help, consider asking for advice on the #hackers channel on Discord.