This document describes heuristics and rules for writing Dart libraries that are published in the Fuchsia SDK.
Unless otherwise specified, Fuchsia library authors should adhere to all the heuristics and rules recommended by the Dart team itself under Effective Dart. Author’s should familiarize themselves with all sections, Style, Documentation, Usage and Design prior to reading this rubric.
There are some terms of art that Dart uses which conflict with Fuchsia’s terminology.
BUILD.gn
file.import 'package:enchilada/enchilada.dart';
imports the enchilada library. Note that Dart libraries have a privacy boundary, e.g. private implementation details are not visible or accessible outside of the library. A Dart package can contain multiple Dart Libraries.When writing Dart code it is important to understand the distinction in terminology in order to remain clear when communicating with team members whose primary language might be one of the other supported languages (C++, Rust, etc.).
Public classes should expose a clean user interface which clearly describes the API surface and is free from internal implementation details. Classes which contain more than a minimal amount of functionality should expose their API in an abstract class with the implementation inside a private implementation file. Doing so allows for the users of the classes to focus on the public methods and forces the implementer to think about the usage of the class before implementation.
When designing the API consider how it will fit into the larger Dart ecosystem of libraries. For example, if writing an API which delivers events consider using Streams instead of callbacks because they compose better with libraries like Flutter.
Dart code written against the Fuchsia SDK should pass all the lint rules specified by the analysis_options.yaml file which lives in the topaz repository. These lint rules will help to automate the review API review process. There are situations where a lint rule may be in conflict with a specific API and may need to be explicitly ignored. If a file is opting out of a lint rule the developer must provide a comment explaining the reasoning for opting out of the lint rule.
When organizing the structure of a Dart package it is important to follow the recommendations laid out by the Effective Dart style guide. Additionally, developers should consider how their code is exported. For a more complicated package, developers should avoid a singular catch all top-level export file and rather expose a top level file per logical grouping of classes that make sense to be pulled under one import line. This allows users of the library the ability to have finer grained control over which sections of the library they import. An example is a package which contains functionality for both agents and modules. In this scenario, we could have one import for agents and one for modules but they could be in the same package.
All comments should adhere to Effective Dart: Documentation as well as the Fuchsia Documentation guide.
Packages written for the Dart Fuchsia SDK should not take on third party dependencies that are not themselves also in the Fuchsia SDK. Exceptions will be made for the following, well established, dependencies which are likely to be present in all environments. Any packages which should be added to this list must be approved by the API Council.
Packages which do take on external dependencies should consider whether they want to reexport those symbols. If the dependency is reexported then the generated documentation will generate documentation for the external dependency. However, reexporting the dependency will create a tight coupling between package versions.
Code should be formatted using the dartfmt
tool. This is an opinionated tool that cannot be configured. Formatting all of our code with this tool will ensure consistency. In Fuchsia, you can use fx format-code
will run dartfmt
on all staged dart files.
part of
directive to avoid tight coupling of classes.void updateAndCommit();
but prefer explicit naming./// Constructs a [Car] object /// /// If [id] is not provided one will be /// generated with a UUID4 format. Car({ @required this.make, @required this.model, this.id, }) : assert(make != null), assert(model != null);
If a method will use a cached object, or create it if it doesn’t exist, avoid introducing or into the name.
class Node { //BAD Node getOrCreateChild(String name); //GOOD Node child(String name); }
When adding a function or interface which will have methods invoked in response to another action, name the methods addListener() and removeListener(). The objects which implement the Listener interface should name the invoked methods on.
class MediaController { void addMediaListener(MediaListener listener) {} void removeMediaListener(MediaListener listener) {} } abstract class MediaListener { void onPause(); void onPlay(); }
When appending an item to your object prefer the name add instead of append to follow the dart list naming.
When deciding between using a single member abstract or a plain Function
as a Listener
object consider how your API might evolve over time. If you expect that you may add more methods to the listener use a single member abstract to allow for the evolution but if the API is not likely to change use a plain function.
// This could logically grow to include an onDoubleTap() // method so it makes sense to use a single member abstract. abstract class TapListener { void onTap(); } void addTapListener(TapListener listener) { ... } // This will likely never need more methods so it can // clearly take a function type. void addOnCloseListener(void Function() listener) { ... }
Concrete data types should be used instead of lower level primitives. The following types should be used when possible:
If there is not a concrete type which can be used to represent your object at a higher level your API should expose one. For example, if we had an API which dealt with currency we would create a Currency
data type instead of working with num
types.
// BAD int getCash() { ... } // GOOD Currency getCash() { ... }
Your API should avoid returning unstructured JSON data but rather transform any JSON into a typed value.
// BAD Map<String, dynamic> getCar() => { 'make': 'Toyota', 'year': 2019, } // Good Car getCar() => Car(make: 'Toyota', year: 2019);
If a package exposes a user visible string the string should be internationalized. In the absence of an ability to internationalize a user visible string the API should return data in which a user of a library can construct an internationalized string.
Exceptions and log messages do not need to be internationalized if they are not intended to be user visible.
All error handling should adhere to Effective Dart: Error handling.
Error and its subclasses are for programmatic errors that shouldn’t be explicitly caught. An Error indicates a bug in your code, it should unwind the entire call stack, halt the program, and print a stack trace so you can locate and fix the bug.
Non-Error exception classes are for runtime errors. If your API implementation throws an exception, it should be documented as part of the public API and it’s expected behavior. This will facilitate programmatic handling of the exception by API clients.
Except in a few special circumstances, idiomatic Dart should throw Errors, but never catch them. They exist specifically to not be caught so that they take down the app and alert the programmer to the location of the bug.
Note: often times people refer to Error when they mean Exception and vice versa. Especially developers that are coming from a different language. Apply your knowledge of their difference when developing your Dart API.
Your public API should throw well defined and typed exceptions so that users can catch them and react appropriately. If you are not in control of all the code that is being called by your package, maybe because you are using a third party library, you may not be able to know exactly which exceptions may be thrown. If this is the case, you can either attempt to catch the exception and wrap it in a type that you create or clearly document that an exception of unknown type may be thrown.
If your API can fail in more than one way the exception should clearly indicate the failure method. Consider throwing different types of exceptions or adding a code to the exception so the caller can respond appropriately. Also, don’t forget to publicly document all the exceptions that are potentially thrown by a given method.
enum ErrorCode { foo, bar } class MyException implements Exception { final ErrorCode code; MyException(this.code); } /// Throws MyException(ErrorCode.foo) if condition is true or /// throws MyException(ErrorCode.bar) if not void baz(bool condition) { If (condition) { throw MyException(ErrorCode.foo); } else { throw MyException(ErrorCode.bar); } }
Assertions should only be used to verify conditions that should be logically impossible to be false due to programmer error, not user or data input. These conditions should only be based on inputs generated by your own code. Any checks based on external inputs should use exceptions.
Use asserts when you are in full control of the inputs. For example verify private functions' arguments with asserts, and using exceptions for public functions arguments.
In Dart all assertions are compiled out from the production/release builds. Therefore, your program must work just as well when all assertions are removed. Do not directly assert on a value returned directly from a function as this can cause the code to not be included in release build since the entire body of the assert is removed in release builds.
// BAD assert(foo()); // foo is not executed // GOOD final success = foo(); assert(success);
In Fuchsia, the generated Dart FIDL bindings are always asynchronous, thus all methods return a Future
even if there is no return value (Future<void>
is used). Also, when connecting to a particular service the connection is assumed to be successful even though it can fail to connect or disconnect in the future. For these reasons, the caller of any FIDL api should always assume that a specific call can fail and handle that appropriately when needed.
final _proxy = fidl_myService.MyServiceProxy(); connectToAgentService('fuchsia-pkg://fuchsia.com/my_service#meta/my_service.cmx', _proxy); _proxy .doSomething() .catchError((e, s) { // handle the error if needed });
Please review Dart and Flutter testing guides.
Future<T>
when disambiguating a FutureOr<T>
whose type argument could be Object.@visibleForTesting
on public API.The API surface of your package should be well tested. However, the public API should not need to leak internal details for the class to be testable. Consider the following example:
class Foo { // services is exposed for testing Foo({GlobalServices services = GlobalServices()}) { … } // Connects to the global service with the given name. Connection connectToGlobalService(String name) { return services.connect(name); } }
Rather, consider writing your class as an abstract class so the user does not need to know about the injection of global services but tests can directly inject the global services into the implementation. These avoids leaking implementation details to the user and provides an API that the user cannot abuse or mess up. This has the added advantage of allowing the API to evolve if the GlobalServices class evolves without having to change the callers of the method.
// foo.dart abstract class Foo { factory Foo() => FooImpl(services: GlobalServices); connectToGlobalService(String name); } // internal/foo_impl.dart class FooImpl implements Foo { FooImpl({GlobalServices services}) { … } // Connects to the global service with the given name. Connection connectToGlobalService(String name) { return services.connect(name); } }
Dart does not allow a private class/function to be accessed from within a test. This has the effect that any private classes cannot be tested. This may be ok if there is a corresponding public class/function that can exercise the private members but this may not always be the case. In these situations it is best to move the private class into its own file which does not get exported by the top-level export and make it public. The tests can now access your private members.
/// BAD - this code does not make _Taco directly testable // dinner.dart class Dinner { final _taco = _Taco(); void eat() => _taco.consume(); } // We have no way to directly test this class class _Taco { void consume() {} } /// GOOD - this code makes Taco directly testable by moving it to its own private file // dinner.dart import '_taco.dart'; class Dinner { final _taco = Taco(); void eat() => _taco.consume(); } // _taco.dart class Taco { void consume() {} } // _taco_test.dart import 'package:dinner/src/_taco.dart' // ignore: implementation_imports void main() { test('taco consumption', () { expect(_Taco().consume(), runsNormally); }); }
It can be useful for a library to declare a common base class without allowing developers to extend the common base class. The common pattern for supporting this is to declare a private constructor on your public base class. This has the effect of allowing subclasses within the same file to extend the base class while not allowing users of your library to subclass the base class.
/// Base class abstract class A { // private constructor disallows instantiation outside of this file ._(); /// Concrete implementation of foo void foo() {} } /// A concrete implementation of [A] class B extends A { () : super._(); @override void foo() { // B implementation super.foo(); } }
It is important to note that this pattern does not restrict users from subclassing the child class since it has a public constructor. If this restriction is required see the factory constructors pattern below.
This pattern is useful if the implementation surface is small since the pattern requires all of the subclasses to live in the same file as the base class or to use the part of directive which is discouraged. If the surface area is too large for a single file consider an alternate pattern.
There are times when a user only needs to interact with a single interface but which may have a different implementation depending on how the object was constructed. Requiring the user to know about the different implementations can add extra API which is not needed and only serves to confuse the user. In this situation you can define an abstract base class which defines the API surface and create factory constructors which vends the appropriate private class.
// Publicly exported class abstract class Foo { factory Foo() => FooImpl(); factory Foo.withNamespace(String namespace) => NamespacedFoo(namespace); void update(String value); void revert(); } // Private implementations not exported in public API class FooImpl implements Foo { final _values = <String>[]; @override void update(String name) => _values.add(name); @override void revert() => _values.removeLast(); } class NamespacedFoo extends FooImpl { final String namespace; NamespacedFoo(this.namespace); @override void update(String name) => super.update('$namespace/$name'); }
Note: If you need to add the restriction that the base class cannot be extended you can implement the pattern defined in Disallowing Subclassing which adds a private constructor to the public base class
Try to make a clear distinction between regular object types and FIDL types. This makes it easier for the maintainers of the code to identify FIDL types from other types and take the necessary precautions when needed. Consider using the as when importing a FIDL service and prefixing it with “fidl_”, this makes it very to identify FIDL types across the entire file.
import 'package:fidl_fuchsia_foo/fidl_async.dart' as fidl_foo; // now it is clear that the return type bar comes from fidl_foo fidl_foo.Bar myMethod(String baz) {...}
When subclassing FIDL types extend them so they can be interchanged with the generated FIDL files. Usually, wrappers decorate the existing type with additional functionality that compliments the original object. However, by extending it from the original FIDL it allows the existing and new API to work with original FIDL types instead of the more concrete types which is useful when interacting with other FIDLs or when developers are not using your wrapper.
Try to avoid interfaces which cover multiple areas of concerns. By breaking down the concerns users can have more flexibility with how they choose to combine the interfaces and allows composed objects to be passed to methods with specific concerns.
void main() { final restaurant = lookupRestaurant(); map.display(restaurant); phone.call(restaurant); } abstract class Callable { String get phoneNumber; } abstract class Location { String get address; String get displayName; } class Restaurant implements Callable, Location { final String name; final String phoneNumber; final String address; String get displayName => name; Restaurant(this.name, this.phoneNumber, this.address); } class Map { void display(Location descriptor) {} } class Phone { void call(Callable callable) {} }
When exposing an API that can modify some sort of collection it is important to protect against modifying the collection during iteration. When iterating over an internal collection consider making a copy of the backing collection to iterate. This will protect from exceptions being thrown for concurrent modification of the underlying collection.
class Controller { final _listeners = <void Function()>[]; void addListener(void Function() f) => _listeners.add(f); bool removeListener(void Function() f) => _listeners.remove(f); void notify() { // Make a copy to avoid modification of _listeners during iteration. for (final f in List.of(_listeners)) { // This method can safely call add/remove listeners f(); } } }
The following patterns should be avoided when writing Dart libraries for the Fuchsia Dart SDK. Exposing Internal Details for Testing It may be tempting to expose certain aspects of your API for testing concerns. However, doing so can clutter your public interface and leak implementation details which the user does not need to know about or may come to rely on. See the Testing section for more details
Dart provides a dynamic type which the compiler will allow any type to be passed to a function and returned from a function. This can be useful in some situations like json encoding/decoding but in the general case it should be avoided. Using dynamic types prevents the compiler from performing static type checking at compile time and introduces hard to debug run-time errors.
In situations where an API might need to accept/return multiple input types consider using generics or defining an interface which the object implements instead. In situations where this will not work, consider defining multiple methods which call through to the private dynamic accepting function.
Dart distinguishes private members from public members by prefixing them with the underscore. This creates isolation between files reduces coupling. This can be overridden by using the part of
directive at the top of a file. This directive has the effect of combining multiple files and allowing them to access each others private members. Doing this makes it hard to rationalize about what is public and what is private and creates tight coupling between classes. Rather than using this directive, it is recommended to only interact with another object via its public interfaces. If classes must interact via private interfaces it is recommended to keep them in the same file to clearly indicate their relationship.
Global static variables can be useful in sharing state across a library but they can easily introduce race conditions and hard to debug code. Global variables can also be accessed by users of your library which may introduce unexpected side effects. It is strongly recommended that you avoid global static variables in public libraries.
If there is a reason that your package does need to use a global static variable it is recommended to use zone-local static variables instead to isolate the variable from users of your library.
void startComputation() { runZoned(() async { await collectScores(getValues()); print('Scores: ${Zone.current[#scores]}'); }, zoneValues: {#scores: <int>[]}); } Future<void> collectScores(Stream<int> scores) async { await for (int value in scores) { Zone.current[#scores].add(value); } } // scores will not be affected by the call to startComputation. final scores = <int>[1, 2, 3]; void main() { startComputation(); }