neodart 0.1.1 neodart: ^0.1.1 copied to clipboard
Extensions and principles for modern Dart development.
neodart #
Neo-Dart, or "new" Dart, is a series of recommended packages and principles that break out of classic conventions ("we've always done it that way") and have a bias towards change versus stability for stability sake.
TIP: Importantly, nobody should feel forced to use neo-Dart conventions.
We've contributed these to help others understand some of the trade-offs involved in building Dart/Flutter apps and libraries, but you can feel free to have your own opinion.
Additionally, some or all of these guidelines may not apply to a short-lived prototype, weekend warrior project, or something that just doesn't need to be particularly robust or contributed to by others.
Usage #
Neo-Dart is a convention, and you can "use" it just by reading (and optionally, contributing) to this repository.
However, if you'd like to use a very opiniated set of lints that represent the conventions being used here:
# Add neodart as a development dependency.
dart pub add --dev neodart
# analysis_options.yaml
include: package:neodart/neodart.yaml
These rules contain include: package:lints/core.yaml
, and then include
additional stricter lints on top of them, always with a reason/rationale.
Contributing #
To contribute, simply open an issue (major discussion) or a pull request (minor or no discussion needed, such as a spelling fix). There is no guarantee your request will be fulfilled, but changes to the guidelines are welcome in general.
As of 2022-07-05, github.com/matanlurey is the only decision maker, with hopes to expand.
Requirements #
- Your request must have a well-documented reason beyond personal preference.
- Your request must be communictable in written English.
- Requests of the form "but X language/framework/library ..." are not enough.
Principles #
The following are principles of neo-Dart packages and code:
- Do not use: Should never occur in any code that is non-experimental.
- Avoid: Should rarely occur in non-experimental code; is well documented.
- Consider: Should be considered when appropriate.
- Prefer: Should be the default unless well-documented otherwise.
Do not use dart:mirrors
#
Enforced: None. Ideally there would be a lint, so TBD.
Runtime reflection was never truly fully baked within Dart 1.x, and in modern Dart (2.x) it is relegated as third-class citizen, no longer supported in either Dart AOT (i.e. most Flutter binaries) or any Dart for the Web program.
In addition, even for command-line Dart JIT programs, mirrors is intentionally no longer being supported or updated to keep up with new language paradigms or primitives.
As a result do not use dart:mirrors
, at all, even for testing. Runtime
reflection often makes it difficult for new users and maintainers of a package
and becomes overly clever. There is always a better way.
Do not use or override noSuchMethod
#
Enforced: None. Ideally there would be a lint, so TBD.
Somewhat similar to dart:mirrors
, is noSuchMethod
, a user-implementable
"catch-all" for an unimplemented member on a Dart class. In modern Dart, it is
mostly used for either stubbing (this class is not yet or intentionally not
implemented) or mocking (i.e. with something like package:mockito
).
Like mirrors, noSuchMethod
has largely not caught up in the language and has
both performance (runtime and code size) costs that aren't apparent to most
developers.
There is almost always a better way than overriding noSuchMethod
.
Do not make dynamic calls #
Enforced: (via optional lint)
avoid_dynamic_calls
.
In Dart 1.x, every call was a dynamic call, and type signatures were only
used for (quite limited) static analysis. Starting in Dart 2.x, most calls are
static, and dynamic calls are limited only to objects (implicitly or explicitly)
typed dynamic
.
Dynamic calls are completely avoidable technical debt, disabling any sort of useful static analysis, making compilers work harder (and in most cases generate slower and less compact code), and are increasingly by accident.
Either use:
Object?
with type checks (i.e.is
) and casting.- Type inference, including with method generics.
- Strongly typed custom objects instead of relying on
foo.someMethod()
.
NOTE: Calls to type
Function
orFunction.apply
are also dynamic calls.
Do not use mocks #
Enforced: (partially via lint)
avoid_implementing_value_types
, but requires more; TBD.
Mocking, or creating short-lived proxy objects that pretend to implement a
type can be useful when testing certain contracts between related objects;
for example, you may want to ensure that pet.eat(bowl)
only calls the
bowl.grab(...)
method once with specific arguments (the pet's mouth size?).
However, mocking is generally extremely over-used. Typical apps and libraries usually have no need for mocking, and would be better served either by using the real object (especially for value-type immutable objects), and/or using or contributing to fakes - intentional sub-types intended for testing.
Avoid code generation #
Enforced: None; not a hard rule and easy to catch in code review.
As a follow-up to not using dart:mirrors
, also do not use code generation.
Code generation can be a powerful tool, but is almost always overused and is not
well supported in the Dart ecosystem (as of 2022-07-05).
Why to avoid code generation:
-
There is not strong support of tooling necessary; the
build
ecosystem is considered "best effort" and is not widely used by the Dart team itself; tools such as the analyzer do not understand the concept of code generation. -
Code generation often produces patterns that would never be permitted within a codebase, but are otherwise hidden or obscured behind "generated code". Just because it's generated doesn't mean the code shouldn't be well understood and of good quality.
-
Code generation is too slow to be part of most serious developer cycles; often taking much longer than running Dart code otherwise, particularly within the VM.
However, this rule is avoid, not do not use; targeted use of code generation (i.e. to generate API clients or for JSON serialization) is permitted, but should be used judiciously; try developing your feature or library without code generation and narrow down the patterns before deciding on code generation.
NOTE: There is some promising upcoming language work in the form of macros that might allow this rule to be softened in the future, but even with macros you'll want to be careful around overuse.
Avoid heavy-weight dependencies when developing a library #
Enforced: None; not a hard rule and easy to catch in code review.
Dependencies are not evil, they are what lets you create any non-trivial app without spending time re-inventing the wheel (particularly important when it comes to things like security, privacy, performance, or low-level computing).
However, be judicious when adding non-development dependencies to a library
package, as each dependency you add will make it harder and harder for users of
your library to upgrade and maintain over time. You should even consider
using show
when importing symbols from other packages into your library:
import 'package:other/other.dart' show ExplicitThingIAmUsing;
For example, when creating a hypothetical number_parser
class, it might be
tempting to use existing libraries for number formatting, string parsing, or
anything in-between. However, ask yourself if you truly need many of the
exact features in your dependencies, or if you could get away with a smaller
(private) bespoke implementation.
TL;DR: In general, try to abide by the YAGNI policy.
Avoid creating classes that can or should be extended or implemented #
Enforced: Partially through the analyzer (@sealed
and factory
).
This is shamelessly copied from Effective Java (Item 17):
Design and document for inheritance or else prohibit it.
In Dart, there are limited mechanisms for accomplishing this outside of
documentation (i.e. /// Do not extend this class
), but some suggestions are:
-
Use
@sealed
frompackage:meta
; prohibits most kinds of inheritance:import 'package:meta/meta.dart'; @sealed class Animal { final String name; Animal(this.name); }
-
Ensure all your public constructors are
factory
constructors:class Animal { final String name; // Factory constructors cannot be called/used by super-classes. factory Animal(String name) = Animal._; // Having a private constructor prevents `extends` and `with` (mixins). Animal._(this.name); }
Avoid mutable objects in public APIs #
Enforced: None. Ideally there would be a lint, so TBD.
It's not desirable to completely avoid mutable state and APIs; at the end of the day all immutability is an abstraction.
However, for public APIs, design for immutability unless mutability is required (i.e. for performance or correctness reasons). Avoid mutating objects that provided to you (unless explicitly documenting you will mutate them):
/// Removes elements from [names] that match our explicit name filter.
void removeExplicit(Set<String> names) {
// OK: Documented in the public API that "names" will be mutated.
}