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.
I understand that feature flags are an exception to the "no relative imports across libraries" rule, but is this exception a problem in practice? This exception seems to be OK so far?
There was a problem hiding this comment.
yup, I think that is ok to keep it as exception, though we should make sure this doesn't encourage more exceptions
|
Thanks 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.
A guidance we should bubble up: your enum-like class must have at least one non-private constructor.
If you forget this, Dart's recommended lint exhaustive_cases will enforce exhaustive checks on your enum-like class: 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?
|
|
||
| #### 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).
| 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.
There was a problem hiding this comment.
There is a big grey area when the customer_test is not broken. It is that we have to use our judgement to decide whether we "think" this will break people. People don't want to treat new enum as breakage, but when combining with exhaustive_cases it almost guaranteed someone will break.
|
|
||
| ### 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?
|
enum rfc has been separated out to #32 |
loic-sharma
left a comment
There was a problem hiding this comment.
This looks good to me overall!
(It looks like the RFC review process asks for a design review meeting before GitHub approval)
|
|
||
| ## Context | ||
|
|
||
| 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`. When an implementation detail is needed across multiple files within a layer, contributors are left with two poor options: |
There was a problem hiding this comment.
Do we have examples where implementation details / secret states have to be exposed within a layer but not to other layers? The examples listed in the next section don't seem super convincing IMO (see the comment below).
There was a problem hiding this comment.
Also flutter/flutter#193960 seems to say another motivation for using @internal is to indicate an API is experimental.
|
Looks like people are generally ok with private files that are under feature flags or cross platform code to use I think we still need to decide whether it is ok to have package private to hide internal facing class that are not of the above two categories. Looks like in flutter/packages does this a lot, but flutter/flutter is still following the rule as best as it can. Do we want to go ahead and make lift this restriction? I vote for yes, as is in this proposal, WDYT @loic-sharma @justinmc @dkwingsmt @LongCatIsLooong |
justinmc
left a comment
There was a problem hiding this comment.
Do you have any specific reasons in mind where you want to start using @internal, or is the motivation for this just the existing violations of the styleguide?
Looks like people are generally ok with private files that are under feature flags or cross platform code to use @internal and not exported in barrel export.
What do you have in mind when you say "cross platform code" here? Or do you mean cross-layer?
Looks like in flutter/packages does this a lot
One example is NoOpPage in go_router, which is probably a good usage.
| #### Enforcement | ||
|
|
||
| * Accidental public exports of `@internal` declarations are caught by the analyzer's [`invalid_export_of_internal_element`](https://dart.dev/tools/diagnostics/invalid_export_of_internal_element) diagnostic, and external usage is caught by [`invalid_use_of_internal_member`](https://dart.dev/tools/diagnostics/invalid_use_of_internal_member) and `implementation_imports`. | ||
| * Repository-specific rules (requiring unexported `lib/src/` files to use a `_` prefix and annotate public top-level declarations with `@internal`, and enforcing instance-member placement rules) will be enforced via custom analyzer lint rules. |
There was a problem hiding this comment.
+1 to automatic enforcement via analyzer.
Overall I think the recommendation in the styleguide is useful as a guideline, but clearly we don't always follow it. I'm supportive of keeping the general guideline but being open to using @internal when it's worth it, especially if we have automated enforcement.
yes, NavigatorState.finalizeRoute and FocusTraversalPolicy. invalidateScopeData are some example that I would have use
I meant cases like conditional import for web vs io, there are a bunch for example like this one https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/_web_image_web.dart |
LongCatIsLooong
left a comment
There was a problem hiding this comment.
Is there a different motivating example for introducing @internal? I don't feel the 2 focus related examples are particularly good candidates for @internal.
|
|
||
| * In any file in `lib/src/` not exported by a barrel file, all public top-level declarations must be annotated with `@internal`, subject to the cross-platform API policy below. | ||
|
|
||
| #### Cross-platform APIs |
There was a problem hiding this comment.
cc @loic-sharma and @justinmc , I added this section let me know what you think
|
|
||
| ### Package-private APIs (`@internal`) | ||
|
|
||
| Package-private APIs remain discouraged in general, rather than banned. Unexported files and package-private declarations in `lib/src/` are permitted only in the following cases: |
There was a problem hiding this comment.
I updated the wording to discourage use of package private instead of banning.
justinmc
left a comment
There was a problem hiding this comment.
A couple questions and ideas but overall LGTM 👍 . You should try to get approval from the other commenters on this file too before merging (I think that's @LongCatIsLooong, @loic-sharma, @dkwingsmt).
| @internal | ||
| extension RouteStateInternal on RouteState { | ||
| void finalizeRoute() => _finalizeRoute(); | ||
| } |
There was a problem hiding this comment.
I would try to avoid this kind of thing unless it's really necessary. It's probably a sign that finalizeRoute should just be public. But I guess I imagine there could be a situation where we do need it.
There was a problem hiding this comment.
yup, therefore I phrase it as discourage to use @internal as general.
For this particular case, it is for separating navigator and route files, the alternative will be merging them navigator.dart and route.dart.
There was a problem hiding this comment.
I agree with not allowing @internal on instance members of exported classes, but the @internal extension isn't a good workaround for letting other files in the package use a library-private (_) member of an exported class. A same-file @internal top-level function does the same job, since Dart privacy is library-level. Your Helper example in this thread relies on the same mechanism. Compared with the extension, it doesn't need an exception to the style guide rule against extensions, and it actually triggers invalid_use_of_internal_member for external callers. The extension doesn't, because the analyzer resolves the call to the unannotated extension member rather than to the extension.
Repro (external package importing src/ directly):
// package:lib_pkg/src/route_state.dart
import 'package:meta/meta.dart';
class RouteState {
void _finalizeRoute() {}
}
@internal
extension RouteStateInternal on RouteState {
void finalizeRoute() => _finalizeRoute();
}
extension RouteStateInternal2 on RouteState {
@internal
void finalizeRoute2() => _finalizeRoute();
}
@internal
void finalizeRoute3(RouteState s) => s._finalizeRoute();// package:app/main.dart
import 'package:lib_pkg/src/route_state.dart';
void main() {
final s = RouteState();
s.finalizeRoute(); // no diagnostic
RouteStateInternal(s).finalizeRoute(); // no diagnostic
s.finalizeRoute2(); // invalid_use_of_internal_member
finalizeRoute3(s); // invalid_use_of_internal_member
}Suggestion: recommend the top-level function as the workaround. Whether to allow @internal extensions for other cases (e.g. helpers on types we don't own) is your call, but if they are allowed, @internal must go on each extension member, not only on the extension declaration.
There was a problem hiding this comment.
This comment is not answered yet. There is no need of making an exception of using extension for package privates, as they can be completely replaced by top-level functions. There is zero benefit of using extension except for a beautiful .foo() syntax, which has no value for internal functions.
| #### Barrel exports and file naming | ||
|
|
||
| * Drop the requirement that every file in `lib/src/<layer>/` be exported by `lib/<layer>.dart` (superseding [flutter/flutter#108632](https://github.com/flutter/flutter/issues/108632)). | ||
| * Any file in `lib/src/<layer>/` not exported by `lib/<layer>.dart` must have a `_` filename prefix (such as `lib/src/widgets/_accessibility_evaluations.dart`), and have all public top-level declarations annotated with `@internal`, subject to the cross-platform API policy above. |
dkwingsmt
left a comment
There was a problem hiding this comment.
I'm generally ok with the current direction, although I found a few non-trivial issues.
| @internal | ||
| extension RouteStateInternal on RouteState { | ||
| void finalizeRoute() => _finalizeRoute(); | ||
| } |
There was a problem hiding this comment.
I agree with not allowing @internal on instance members of exported classes, but the @internal extension isn't a good workaround for letting other files in the package use a library-private (_) member of an exported class. A same-file @internal top-level function does the same job, since Dart privacy is library-level. Your Helper example in this thread relies on the same mechanism. Compared with the extension, it doesn't need an exception to the style guide rule against extensions, and it actually triggers invalid_use_of_internal_member for external callers. The extension doesn't, because the analyzer resolves the call to the unannotated extension member rather than to the extension.
Repro (external package importing src/ directly):
// package:lib_pkg/src/route_state.dart
import 'package:meta/meta.dart';
class RouteState {
void _finalizeRoute() {}
}
@internal
extension RouteStateInternal on RouteState {
void finalizeRoute() => _finalizeRoute();
}
extension RouteStateInternal2 on RouteState {
@internal
void finalizeRoute2() => _finalizeRoute();
}
@internal
void finalizeRoute3(RouteState s) => s._finalizeRoute();// package:app/main.dart
import 'package:lib_pkg/src/route_state.dart';
void main() {
final s = RouteState();
s.finalizeRoute(); // no diagnostic
RouteStateInternal(s).finalizeRoute(); // no diagnostic
s.finalizeRoute2(); // invalid_use_of_internal_member
finalizeRoute3(s); // invalid_use_of_internal_member
}Suggestion: recommend the top-level function as the workaround. Whether to allow @internal extensions for other cases (e.g. helpers on types we don't own) is your call, but if they are allowed, @internal must go on each extension member, not only on the extension declaration.
|
|
||
| Package-private APIs remain discouraged in general, rather than banned. Unexported files and package-private declarations in `lib/src/` are permitted only in the following cases: | ||
|
|
||
| * Experimental APIs that are not yet ready for public stability guarantees. These APIs must be both annotated with `@internal` and gated behind a feature flag. |
There was a problem hiding this comment.
I again suggest keeping experimental APIs out of this RFC, as I mentioned earlier. How experimental APIs should work is more than just a tag, and hasn't been settled in this thread: @internal + feature flag vs. @experimental, how users opt in, how APIs graduate, and whether SDK packages may use them (flutter_test/lib/src/binding.dart already uses the windowing APIs via a file-wide ignore_for_file). I'd suggest removing the experimental category here, and treating the existing windowing APIs and the _features.dart exception as out of scope until a separate RFC covers them.
There was a problem hiding this comment.
@experimental is a weaker guard than @internal + feature flag. We also already have leaned toward package private + feature flag in our current code, this guideline change only makes it easier to follow for new experimental API. I view it as a net benefit.
There was a problem hiding this comment.
I agree that @internal + feature flag is a stronger guard than @experimental; I'm not arguing for @experimental. My concern is codifying the pattern before its rules are worked out.
The only existing case, windowing, doesn't follow the other rules in this RFC: WidgetsBinding.windowingOwner is an @internal instance member on an exported mixin, and flutter_test uses these APIs through a file-wide ignore_for_file. Once this lands, those become violations to fix or exceptions to carve out. Open questions like how users opt in (importing src/ plus an ignore?) and how an API graduates would get answered by whatever the first few cases happen to do, and changing a published guideline later is harder than getting it right once.
Since windowing is the only experimental API using this pattern today, deferring the category to a separate RFC costs little. If you'd rather keep it here, I'd at least like the RFC to cover how windowing conforms, how users are expected to opt in, and what graduation looks like.
There was a problem hiding this comment.
I think the current rule in this rfc should have covered this? that the windowingOwner should have been internal extension and should be fixed.
Open questions like how users opt in (importing src/ plus an ignore?)
External package or app should not use it so there is no opt in for them even in their tests. Their only channel is to file an issue to open the API to public
If you meant the test in flutter/flutter, I am less concern about how they get around the @internal decorator. Either they direct import or use ignore_for_file.
There was a problem hiding this comment.
Moving windowingOwner to an internal extension doesn't work technically: WidgetsBinding.createWindowingOwner() has to be overridden by TestWidgetsFlutterBinding in flutter_test (a separate package), and extension methods can't be overridden. And because @internal is strictly package-scoped, flutter_test has no way to touch windowing types without ignore_for_file.
More importantly, "external apps should not use it" contradicts why the API is behind a user-facing --enable-windowing flag in the first place. In fact, the windowingOwner error message, _window.dart doc comments, and examples/multiple_windows all explicitly instruct developers to enable the flag and import src/ (with ignore_for_file) to try it out before it stabilizes (flutter/flutter#177586).
Therefore applying @internal to windowingOwner, even with it converted to an extension, will not solve its problem.
Speaking more broadly, both issues come from the same root cause: @internal is for symbols that are never accessed outside the package, whereas an experimental API is meant to be tried across package boundaries before becoming public. Having an experimental API mechanism is valuable and worth discussing, but it shouldn't be built on @internal, and it involves process questions beyond syntax—such as the criteria and lifecycle for introducing and graduating an experimental API. For this RFC, we should remove the experimental category (leaving windowing as an existing exception for now) and address experimental APIs in a dedicated proposal.
There was a problem hiding this comment.
let's keep this to a separate discussion of whether we should use @experimental, for now let's keep createWindowingOwner as an exception.
justinmc
left a comment
There was a problem hiding this comment.
I wanted to capture two points from the Dash Forum on this RFC:
- We should be clear in the styleguide that we still encourage APIs that are public and production-quality at all layers, and package private should only be used in rare cases. Ideally we continue to allow users to peel back layers and hack on things, while keeping our maintenance burden and velocity reasonable.
- As asked by @reidbaker, should we consider breaking up some of our large files? For example basic.dart and framework.dart both are 7,000+ lines. It might be worthwhile to take a look at what it would look like if we did try breaking them up, just for argument's sake.
Piinks
left a comment
There was a problem hiding this comment.
should we consider breaking up some of our large files? For example basic.dart and framework.dart both are 7,000+ lines. It might be worthwhile to take a look at what it would look like if we did try breaking them up, just for argument's sake.
I wonder if shorter files could also improve agent performance, maybe worth an eval to see for sure.
I think lines may not be the best indicator but more of a correlation of a file that may be mixing too much class or concept. but to that point these two file may be a good example to break it apart because they are a lump sum of widgets that doesn't relate to the other, and a lot of them can probably be separated without the need of I will do a throughly audit on the file once this proposal is approved. |
| The style guide bans package-private APIs beyond library-private (`_`) declarations and requires every file in `lib/src/<layer>/` to be exported by `lib/<layer>.dart`. When an implementation detail is needed across multiple files within a layer, contributors are left with two poor options: | ||
|
|
||
| * Keep declarations library-private (`_`), which forces all related code into a single large file. | ||
| * Expose implementation details as public APIs (such as `NavigatorState.finalizeRoute` shared between `navigator.dart` and `routes.dart`, or `HardwareKeyboard.syncKeyboardState` shared between `hardware_keyboard.dart` and `binding.dart` in `services`), which commits the framework to backward compatibility and discourages refactoring. |
There was a problem hiding this comment.
which commits the framework to backward compatibility and discourages refactoring.
I feel this would encourage the framework to sit on tech debts instead, if @internal is not counted as tech debts (IIRC there's some sort of tracker for framework tech debts?)
If the API design is leaky and it's a real framework tech debt (the more common case), wouldn't improving the API and deprecating the existing leaky API be the more correct thing to do (especially if you're already refactoring it)?
If the API design is leaky because it has to be leaky (e.g., because of a language limitation), then I feel it's justified to tell API users that the API isn't really a part of the public contract and should not be used (and prevent dartdoc from generating a reference for that API).
NavigatorState.finalizeRoute
I think it's still part of the Route lifecycle contract? A custom Route subclass would still need to call this method in didPop. So this shouldn't really qualify for @internal?
HardwareKeyboard.syncKeyboardState
Not familiar with this one, but Gemini suggested the leak is avoidable if we redesign the API if we pass the initial key presses synchronously. So technically a tech debt?
There was a problem hiding this comment.
Should we require a TODO comment for @internal usages for this category, since in most cases they are tech debts?
There was a problem hiding this comment.
A custom Route subclass would still need to call this method in didPop. So this shouldn't really qualify for @internal?
they can do that by calling super, I think the reason is didpop can have animation, but that is self contained in TransitionRoute. One arguement is someone implement route interface, but it won't work without significant hack in navigator because there is internal logic in base route class.
The only use case I can think of that can't be solve by inheritance is that they implement on top of Route but below TransitionRoute, It will break a bunch of other stuff like gesture pop and predicative back. So I don't think that is something we should support.
I agree this may be able to be prevented through less leaky design, but that will mean we will build transition and animation in mind when designing navigator, or just move TransitionRoute to navigator.dart.
but that back to the question whether we are going to merge classes and different concepts in a single file or not.
Should we require a TODO comment for @internal usages for this category, since in most cases they are tech debts?
I think sometimes this is just unavoidable, and the alternative is big files. but I think requiring a comment reasoning may be a good practice
| } | ||
| ``` | ||
|
|
||
| * When a package-private constructor or class member is needed for inheritance or construction on a publicly exported class, all such constructors and members must be annotated with `@internal`, and the class must be marked `final` or `sealed` so external classes cannot `extends` or `implements` it. |
There was a problem hiding this comment.
This rule contradicts itself on inheritance. A final or sealed class cannot be extended or implemented in any other file. Within the same file, _ declarations already work, so @internal is never needed there; across files, final and sealed forbid the inheritance this bullet tries to permit.
Assuming this rule is an answer to #30 (comment) : Constructors and static members do not need final or sealed. Because they aren't part of the class interface, changing them can't break external extends or implements, and requiring final or sealed needlessly prevents adding them to existing public classes. To allow in-package subclasses across files while blocking external subtypes, use base with a named @internal generative constructor (@internal Foo.internal(); unnamed @internal Foo() misses implicit super() calls).
There was a problem hiding this comment.
I ended up disallow all class property including constructor and static member.
primary constructor will have api leak
class Foo {
@internal
Foo()
}
class Bar extends Foo{} // this is ok but leak the private API
class Bar extends Foo{
Bar(): super() // This will raise error
}unless we say things like only allow on name parameter, but I felt trying to patch corner cases are not worth it
for static property is replaceable with top level variable.
Just to keep it simple for now.
| @internal | ||
| extension RouteStateInternal on RouteState { | ||
| void finalizeRoute() => _finalizeRoute(); | ||
| } |
There was a problem hiding this comment.
This comment is not answered yet. There is no need of making an exception of using extension for package privates, as they can be completely replaced by top-level functions. There is zero benefit of using extension except for a beautiful .foo() syntax, which has no value for internal functions.
| * Files in `lib/src/<layer>/` must not import another layer's `lib/src/` files directly, except for `lib/src/foundation/_features.dart`. | ||
| 3. Add CI checks for cross-file and barrel rules: | ||
| * Any file in `lib/src/<layer>/` not exported by `lib/<layer>.dart` must have a `_` filename prefix, and `_`-prefixed files must not be exported by `lib/<layer>.dart`. | ||
| * Every `@internal` declaration must be referenced in the package's non-test code; references from tests do not count, and unused `@internal` declarations must be removed. |
There was a problem hiding this comment.
nit: I wonder if we need to make an exception to generated code, such as those in pigeon, which generates functions for all APIs regardless of whether they're used. cc @tarrinneal
There was a problem hiding this comment.
pigeon shouldn't need an exception, but I don't know about the jnigen/ffigen output.
| * `@internal` types must not appear in the public signatures or supertypes (`extends`, `implements`, `with`, `on`) of publicly exported declarations. | ||
| * Public members of an `@internal` `extension` must also be annotated with `@internal`. | ||
| * Any publicly exported class with `@internal` constructors or members must be marked `final` or `sealed`. | ||
| * Files in `lib/src/<layer>/` must not import another layer's `lib/src/` files directly, except for `lib/src/foundation/_features.dart`. |
There was a problem hiding this comment.
with
lib/src/foundation/_features.dartas the only permitted exception
_features.dart isn't the only cross-layer import of an unexported file today. There are at least two more:
lib/src/web.dart: a hand-copied subset ofpackage:web(DOM and JS interop types such asHTMLImageElementanddocument), kept in the framework so that Flutter doesn't depend onpackage:web. It sits directly inlib/src/, outside any layer directory, and isn't exported by any barrel. It's imported via../web.dartby web implementations in three layers:foundation/_error_dumper_web.dart,painting/_web_image_info_web.dartandpainting/_network_image_web.dart, andwidgets/_web_image_web.dartandwidgets/_platform_selectable_region_context_menu_web.dart.lib/src/painting/_web_image_info_{io,web}.dart: definesWebImageInfo. It isn't exported bypainting.dart, but the widgets layer imports it directly:widgets/image.dart(as a conditional import) andwidgets/_web_image_{io,web}.dart. It can't simply move to the widgets layer, because it's created in the painting layer (painting/_network_image_web.dart, behindNetworkImage) and consumed in the widgets layer (Imagechecks for it to switch to an<img>platform view). Making it public isn't trivial either, since itshtmlImagegetter returns theHTMLImageElementtype from theweb.dartshim above.
I think this rule is still valuable, since keeping cross-layer "private imports" to a minimum is crucial for layer isolation, and for letting external code build on the lower layers (such as rendering) the same way the framework's own higher layers do. I suggest adding these two as exceptions for now. web.dart should probably be a permanent exception. WebImageInfo might be worth another look in the future, to see whether we can factor it out or turn it into a meaningful public class: today, a third-party image widget built on NetworkImage receives a WebImageInfo on the web but has no way to recognize it, which is exactly the kind of gap this rule is meant to prevent.
|
|
||
| Package-private APIs remain discouraged in general, rather than banned. Unexported files and package-private declarations in `lib/src/` are permitted only in the following cases: | ||
|
|
||
| * Experimental APIs that are not yet ready for public stability guarantees. These APIs must be both annotated with `@internal` and gated behind a feature flag. |
There was a problem hiding this comment.
Moving windowingOwner to an internal extension doesn't work technically: WidgetsBinding.createWindowingOwner() has to be overridden by TestWidgetsFlutterBinding in flutter_test (a separate package), and extension methods can't be overridden. And because @internal is strictly package-scoped, flutter_test has no way to touch windowing types without ignore_for_file.
More importantly, "external apps should not use it" contradicts why the API is behind a user-facing --enable-windowing flag in the first place. In fact, the windowingOwner error message, _window.dart doc comments, and examples/multiple_windows all explicitly instruct developers to enable the flag and import src/ (with ignore_for_file) to try it out before it stabilizes (flutter/flutter#177586).
Therefore applying @internal to windowingOwner, even with it converted to an extension, will not solve its problem.
Speaking more broadly, both issues come from the same root cause: @internal is for symbols that are never accessed outside the package, whereas an experimental API is meant to be tried across package boundaries before becoming public. Having an experimental API mechanism is valuable and worth discussing, but it shouldn't be built on @internal, and it involves process questions beyond syntax—such as the criteria and lifecycle for introducing and graduating an experimental API. For this RFC, we should remove the experimental category (leaving windowing as an existing exception for now) and address experimental APIs in a dedicated proposal.
f40f447 to
f6eda15
Compare
| 2. Add custom analyzer lint rules for single-library repository rules: | ||
| * `@internal` may only be applied to top-level declarations inside `lib/src/` (not class members or constructors). | ||
| * Files in `lib/src/<layer>/` must not import another layer's `lib/src/` files directly, except for `lib/src/foundation/_features.dart`. | ||
| 3. Add CI checks (using `package:analyzer`) for cross-file and barrel rules, honoring `// ignore:` comments for feature-flagged experimental APIs: |
There was a problem hiding this comment.
cc @loic-sharma @dkwingsmt I added an exception clause for experimental api. This can be fixed if we decide to use @experimental, but I felt that needs more discussion. Keep them as exception for now
Proposing change to style guide around package private 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.