diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml deleted file mode 100644 index 1b025c6..0000000 --- a/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: github pages - -on: - push: - branches: - - main # Set a branch to deploy - pull_request: - -jobs: - deploy: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - with: - submodules: true # Fetch Hugo themes (true OR recursive) - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - - - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 - with: - hugo-version: 'latest' - # extended: true - - - name: Build - run: hugo --gc --minify - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./public \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b716b80..0000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store - -node_modules/ -resources/ -public/ -footage/ - -*.mov \ No newline at end of file diff --git a/README.md b/.nojekyll similarity index 100% rename from README.md rename to .nojekyll diff --git a/layouts/404.html b/404.html similarity index 100% rename from layouts/404.html rename to 404.html diff --git a/static/CNAME b/CNAME similarity index 100% rename from static/CNAME rename to CNAME diff --git a/archetypes/default.md b/archetypes/default.md deleted file mode 100644 index 00e77bd..0000000 --- a/archetypes/default.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "{{ replace .Name "-" " " | title }}" -date: {{ .Date }} -draft: true ---- - diff --git a/articles/build-widget-with-async-method-call/index.html b/articles/build-widget-with-async-method-call/index.html new file mode 100644 index 0000000..98bbe5e --- /dev/null +++ b/articles/build-widget-with-async-method-call/index.html @@ -0,0 +1,94 @@ +
You want to return a widget in a build method…
But your data comes from an async function!
class MyWidget extends StatelessWidget {
+ @override
+ Widget build(context) {
+ callAsyncFetch().then((data) {
+ return Text(data); // doesn't work
+ });
+ }
+}
+The callAsyncFetch function could be an HTTP call, a Firebase call, or a call to SharedPreferences or SQLite, etc. Anything that returns a Future 🔮.
So, can we make the build method async? 🤔
class MyWidget extends StatelessWidget {
+ @override
+ Future<Widget> build(context) async {
+ var data = await callAsyncFetch();
+ return Text(data); // doesn't work either
+ }
+}
+Not possible! A widget’s build “sync” method will NOT wait for you while you fetch data 🙁
(You might even get a type 'Future' is not a subtype of type kind of error.)
Meet FutureBuilder:
class MyWidget extends StatelessWidget {
+ @override
+ Widget build(context) {
+ return FutureBuilder<String>(
+ future: callAsyncFetch(),
+ builder: (context, AsyncSnapshot<String> snapshot) {
+ if (snapshot.hasData) {
+ return Text(snapshot.data);
+ } else {
+ return CircularProgressIndicator();
+ }
+ }
+ );
+ }
+}
+It takes our Future as argument, as well as a builder (it’s basically a delegate called by the widget’s build method). The builder will be called immediately, and again when our future resolves with either data or an error.
An AsyncSnapshot<T> is simply a representation of that data/error state. This is actually a useful API!
If we get a new snapshot with:
StatefulWidget? Yes, it’s a possible solution but not an ideal one. Keep on reading and we’ll see why.Click Run and see it for yourself!
It will show a circular progress indicator while the future resolves (about 2 seconds) and then display data. Problem solved!
FutureBuilder itself is built on top of StatefulWidget! Attempting to solve this problem with a StatefulWidget is not wrong but simply lower-level and more tedious.
Check out the simplified and commented-by-me source code:
(I removed bits and pieces for illustration purposes)
// FutureBuilder *is* a stateful widget
+class FutureBuilder<T> extends StatefulWidget {
+
+ // it takes in a `future` and a `builder`
+ const FutureBuilder({
+ this.future,
+ this.builder
+ });
+
+ final Future<T> future;
+
+ // the AsyncWidgetBuilder<T> type is a function(BuildContext, AsyncSnapshot<T>) which returns Widget
+ final AsyncWidgetBuilder<T> builder;
+
+ @override
+ State<FutureBuilder<T>> createState() => _FutureBuilderState<T>();
+}
+
+class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
+ // keeps state in a local variable (so far there's no data)
+ AsyncSnapshot<T> _snapshot = null;
+
+ @override
+ void initState() {
+ super.initState();
+
+ // wait for the future to resolve:
+ // - if it succeeds, create a new snapshot with the data
+ // - if it fails, create a new snapshot with the error
+ // in both cases `setState` will trigger a new build!
+ widget.future.then<void>((T data) {
+ setState(() { _snapshot = AsyncSnapshot<T>(data); });
+ }, onError: (Object error) {
+ setState(() { _snapshot = AsyncSnapshot<T>(error); });
+ });
+ }
+
+ // builder is called with every `setState` (so it reacts to any event from the `future`)
+ @override
+ Widget build(BuildContext context) => widget.builder(context, _snapshot);
+
+ @override
+ void didUpdateWidget(FutureBuilder<T> oldWidget) {
+ // compares old and new futures!
+ }
+
+ @override
+ void dispose() {
+ // ...
+ super.dispose();
+ }
+}
+Very simple, right? This is likely similar to what you tried when using a StatefulWidget. Of course, for the real, battle-tested source code see FutureBuilder.
Before wrapping up… 🎁
From the docs
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder’s parent is rebuilt, the asynchronous task will be restarted.
Widget build(context) {
+ return FutureBuilder<String>(
+ future: callAsyncFetch(),
+Does this mean callAsyncFetch() will be called many times?
In this small example, there is no reason for the parent to rebuild (nothing changes) but in general you should assume it does. See Why is my Future/Async Called Multiple Times?.
What is the best practice for checking nulls in Dart?
var value = maybeSomeNumber();
+
+if (value != null) {
+ doSomething();
+}
+That’s right. There is no shortcut like if (value) and truthy/falsey values in Javascript. Conditionals in Dart only accept bool values.
However! There are some very interesting null-aware operators.
??In other languages we can use the logical-or shortcut. If maybeSomeNumber() returns null, assign a default value of 2:
value = maybeSomeNumber() || 2
+In Dart we can’t do this because the expression needs to be a boolean (“the operands of the || operator must be assignable to bool”).
That’s why the ?? operator exists:
var value = maybeSomeNumber() ?? 2;
+Similarly, if we wanted to ensure a value argument was not-null we’d do:
value = value ?? 2;
+But there’s an even simpler way.
??=value ??= 2;
+Much like Ruby’s ||=, it assigns a value if the variable is null.
Here’s an example of a very concise cache-based factory constructor using this operator:
class Robot {
+ final double height;
+
+ static final _cache = <double, Robot>{};
+
+ Robot._(this.height);
+
+ factory Robot(height) {
+ return _cache[height] ??= Robot._(height);
+ }
+}
+More generally, ??= is useful when defining computed properties:
get value => _value ??= _computeValue();
+?.Otherwise known as the Elvis operator. I first saw this in the Groovy language.
def value = person?.address?.street?.value
+If any of person, address or street are null, the whole expression returns null. Otherwise, value is called and returned.
In Dart it’s exactly the same!
final value = person?.address?.street?.value;
+If address was a method instead of a getter, it would work just the same:
final value = person?.getAddress()?.street?.value;
+
...?Lastly, this one only inserts a list into another only if it’s not-null.
List<int> additionalZipCodes = [33110, 33121, 33320];
+List<int> optionalZipCodes = fetchZipCodes();
+final zips = [10001, ...additionalZipCodes, ...?optionalZipCodes];
+print(zips); /* [10001, 33110, 33121, 33320] if fetchZipCodes() returns null */
+Right now, null can be assigned to any assignable variable.
There are plans to improve the Dart language and include NNBD (non-nullable by default).
For a type to allow null values, a special syntax will be required.
The following will throw an error:
int value = someNumber();
+value = null;
+And fixed by specifying the int? type:
int? value = someNumber();
+value = null;
+This is an example of how we can configure Flutter Data to use GetIt as a dependency injection framework.
Important: Make sure to replicate ProxyProviders for other models than Todo.
class GetItTodoApp extends StatelessWidget {
+ @override
+ Widget build(context) {
+ GetIt.instance.registerRepositories();
+ return MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: FutureBuilder(
+ future: GetIt.instance.allReady(),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return const CircularProgressIndicator();
+ }
+ final repository = GetIt.instance.get<Repository<Todo>>();
+ return GestureDetector(
+ onDoubleTap: () async {
+ print((await repository.findOne(1, remote: false))?.title);
+ final todo = await Todo(id: 1, title: 'blah')
+ .save(remote: false);
+ print(keyFor(todo));
+ },
+ child: Text('Hello Flutter Data with GetIt! $repository'),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// we can do this as this function will never be called
+T _<T>(ProviderBase<T> provider) => null as T;
+
+extension GetItFlutterDataX on GetIt {
+ void registerRepositories(
+ {FutureFn<String>? baseDirFn,
+ List<int>? encryptionKey,
+ bool clear = false,
+ bool? remote,
+ bool? verbose}) {
+ final i = GetIt.instance;
+
+ final _container = ProviderContainer(
+ overrides: [
+ configureRepositoryLocalStorage(
+ baseDirFn: baseDirFn, encryptionKey: encryptionKey, clear: clear),
+ ],
+ );
+
+ if (i.isRegistered<RepositoryInitializer>()) {
+ return;
+ }
+
+ i.registerSingletonAsync<RepositoryInitializer>(() async {
+ final init = _container.read(
+ repositoryInitializerProvider(remote: remote, verbose: remote)
+ .future);
+ internalLocatorFn =
+ <T extends DataModel<T>>(Provider<Repository<T>> provider, _) =>
+ _container.read(provider);
+ return init;
+ });
+ i.registerSingletonWithDependencies<Repository<Todo>>(
+ () => _container.read(todosRepositoryProvider),
+ dependsOn: [RepositoryInitializer]);
+ }
+}
+See this in action with the Flutter Data setup app!
This is an example of how we can configure Flutter Data to use Provider as a dependency injection framework.
Important: Make sure to replicate ProxyProviders for other models than Todo.
class ProviderTodoApp extends StatelessWidget {
+ @override
+ Widget build(context) {
+ return MultiProvider(
+ providers: [
+ ...providers(clear: true),
+ ProxyProvider<Repository<Todo>?, SessionService?>(
+ lazy: false,
+ create: (_) => SessionService(),
+ update: (context, repository, service) {
+ if (service != null && repository != null) {
+ return service..initialize(repository);
+ }
+ return service;
+ },
+ ),
+ ],
+ child: MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: Builder(
+ builder: (context) {
+ if (context.watch<RepositoryInitializer?>() == null) {
+ // optionally also check
+ // context.watch<SessionService>.repository != null
+ return const CircularProgressIndicator();
+ }
+ final repository = context.watch<Repository<Todo>?>();
+ return GestureDetector(
+ onDoubleTap: () async {
+ print((await repository!.findOne(1, remote: false))?.title);
+ final todo = await Todo(id: 1, title: 'blah')
+ .save(remote: false);
+ print(keyFor(todo));
+ },
+ child: Text('Hello Flutter Data with Provider! $repository'),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+List<SingleChildWidget> providers(
+ {FutureFn<String>? baseDirFn,
+ List<int>? encryptionKey,
+ bool? clear,
+ bool? remote,
+ bool? verbose}) {
+ return [
+ Provider(
+ create: (_) => ProviderContainer(
+ overrides: [
+ configureRepositoryLocalStorage(
+ baseDirFn: baseDirFn, encryptionKey: encryptionKey, clear: clear),
+ ],
+ ),
+ ),
+ FutureProvider<RepositoryInitializer?>(
+ initialData: null,
+ create: (context) async {
+ return await Provider.of<ProviderContainer>(context, listen: false)
+ .read(
+ repositoryInitializerProvider(remote: remote, verbose: verbose)
+ .future,
+ );
+ },
+ ),
+ ProxyProvider<RepositoryInitializer?, Repository<Todo>?>(
+ lazy: false,
+ update: (context, i, __) => i == null
+ ? null
+ : Provider.of<ProviderContainer>(context, listen: false)
+ .read(todosRepositoryProvider),
+ dispose: (_, r) => r?.dispose(),
+ ),
+ ];
+}
+See this in action with the Flutter Data setup app!
Example:
mixin AuthAdapter on RemoteAdapter<User> {
+ Future<String> login(String email, String password) async {
+ return sendRequest(
+ baseUrl.asUri / 'token',
+ method: DataRequestMethod.POST,
+ body: json.encode({'email': email, 'password': password}),
+ onSuccess: (data) => data['token'] as String,
+ );
+ }
+}
+and use it:
final token = await userRepository.authAdapter.login('e@mail, p*ssword');
+Also see JSONAPIAdapter for inspiration.
What’s the difference between final and const in Dart?
Easy!
Final means single-assignment.
Const means immutable.
Let’s see an example:
final _final = [2, 3];
+const _const = [2, 3];
+_final = [4,5]; // ERROR: can't re-assign
+_final.add(6); // OK: can mutate
+_const.add(6); // ERROR: can't mutate
+An elegant Dart getter shorthand used to cache computed properties:
T get foo => _foo ??= _computeFoo();
+
+// which depends on having
+T _foo;
+T _computeFoo() => /** ... **/;
+It makes use of the fallback assignment operator ??=.
nulls in Dart!Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories…
Read this post and you will become an expert!

When we want an instance of a certain class we call a constructor, right?
var robot = new Robot();
+In Dart 2 we can leave out the new:
var robot = Robot();
+A constructor is used to ensure instances are created in a coherent state. This is the definition in a class:
class Robot {
+ Robot();
+}
+This constructor has no arguments so we can leave it out and write:
class Robot {
+}
+The default constructor is implicitly defined.
Most times we need to configure our instances. For example, pass in the height of a robot:
var r = Robot(5);
+r is now a 5-feet tall Robot.
To write that constructor we include the height field after the colon :
class Robot {
+ double height;
+ Robot(height) : this.height = height;
+}
+or even
class Robot {
+ double height;
+ Robot(data) : this.height = data.physics.raw['heightInFt'];
+}
+This is called an initializer. It accepts a comma-separated list of expressions that initialize fields with arguments.
Fortunately, Dart gives us a shortcut. If the field name and type are the same as the argument in the constructor, we can do:
class Robot {
+ double height;
+ Robot(this.height);
+}
+Imagine that the height field is expressed in feet and we want clients to supply the height in meters. Dart also allows us to initialize fields with computations from static methods (as they don’t depend on an instance of the class):
class Robot {
+ static mToFt(m) => m * 3.281;
+ double height; // in ft
+ Robot(height) : this.height = mToFt(height);
+}
+Sometimes we must call super constructors when initializing:
class Machine {
+ String name;
+ Machine(this.name);
+}
+
+class Robot extends Machine {
+ static mToFt(m) => m * 3.281;
+ double height;
+ Robot(height, name) : this.height = mToFt(height), super(name);
+}
+Notice that super(...) must always be the last call in the initializer.
And if we needed to add more complex guards (than types) against a malformed robot, we can use assert:
class Robot {
+ final double height;
+ Robot(height) : this.height = height, assert(height > 4.2);
+}
+Back to our earlier robot definition:
class Robot {
+ double height;
+ Robot(this.height);
+}
+
+void main() {
+ var r = Robot(5);
+ print(r.height); // 5
+}
+Let’s make it taller:
void main() {
+ var r = Robot(5);
+ r.height = 6;
+ print(r.height); // 6
+}
+But robots don’t grow, their height is constant! Let’s prevent anyone from modifying the height by making the field private.
In Dart, there is no private keyword. Instead, we use a convention: field names starting with _ are private (library-private, actually).
class Robot {
+ double _height;
+ Robot(this._height);
+}
+Great! But now there is no way to access r.height. We can make the height property read-only by adding a getter:
class Robot {
+ double _height;
+ Robot(this._height);
+
+ get height {
+ return this._height;
+ }
+}
+Getters are functions that take no arguments and conform to the uniform access principle.
We can simplify our getter by using two shortcuts: single expression syntax (fat arrow) and implicit this:
class Robot {
+ double _height;
+ Robot(this._height);
+
+ get height => _height;
+}
+Actually, we can think of public fields as private fields with getters and setters. That is:
class Robot {
+ double height;
+ Robot(this.height);
+}
+is equivalent to:
class Robot {
+ double _height;
+ Robot(this._height);
+
+ get height => _height;
+ set height(value) => _height = value;
+}
+Keep in mind initializers only assign values to fields and it is therefore not possible to use a setter in an initializer:
class Robot {
+ double _height;
+ Robot(this.height); // ERROR: 'height' isn't a field in the enclosing class
+
+ get height => _height;
+ set height(value) => _height = value;
+}
+
If a setter needs to be called, we’ll have to do that in a constructor body:
class Robot {
+ double _height;
+
+ Robot(h) {
+ height = h;
+ }
+
+ get height => _height;
+ set height(value) => _height = value;
+}
+We can do all sorts of things in constructor bodies, but we can’t return a value!
class Robot {
+ double height;
+ Robot(this.height) {
+ return this; // ERROR: Constructors can't return values
+ }
+}
+Final fields are fields that can only be assigned once.
final r = Robot(5);
+r = Robot(7); /* ERROR */
+Inside our class, we won’t be able to use the setter:
class Robot {
+ final double _height;
+ Robot(this._height);
+
+ get height => _height;
+ set height(value) => _height = value; // ERROR
+}
+Just like with var, we can use final before any type definition:
var r;
+var Robot r;
+
+final r;
+final Robot r;
+The following won’t work because height, being final, must be initialized. And initialization happens before the constructor body is run:
class Robot {
+ final double height;
+
+ Robot(double height) {
+ this.height = height; // ERROR: The final variable 'height' must be initialized
+ }
+}
+Let’s fix it:
class Robot {
+ final double height;
+ Robot(this.height);
+}
+If most robots are 5-feet tall then we can avoid specifying the height each time. We can make an argument optional and provide a default value:
class Robot {
+ final double height;
+ Robot([this.height = 5]);
+}
+So we can just call:
void main() {
+ var r = Robot();
+ print(r.height); // 5
+
+ var r2d2 = Robot(3.576);
+ print(r2d2.height); // 3.576
+}
+
Our robots clearly have more attributes than a height. Let’s add some more!
class Robot {
+ final double height;
+ final double weight;
+ final String name;
+
+ Robot(this.height, this.weight, this.name);
+}
+
+void main() {
+ final r = Robot(5, 170, "Walter");
+ r.name = "Steve"; // ERROR
+}
+As all fields are final, our robots are immutable! Once they are initialized, their attributes can’t be changed.
Now let’s imagine that robots respond to many different names:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ Robot(this.height, this.weight, this.names);
+}
+
+void main() {
+ final r = Robot(5, 170, ["Walter"]);
+ print(r.names..add("Steve")); // [Walter, Steve]
+}
+Dang, using a List made our robot mutable again!
We can solve this with a const constructor:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ const Robot(this.height, this.weight, this.names);
+}
+
+void main() {
+ final r = const Robot(5, 170, ["Walter"]);
+ print(r.names..add("Steve")); // ERROR: Unsupported operation: add
+}
+const can only be used with expressions that can be computed at compile time. Take the following example:
import 'dart:math';
+
+class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ const Robot(this.height, this.weight, this.names);
+}
+
+void main() {
+ final r = const Robot(5, 170, ["Walter", Random().nextDouble().toString()]); // ERROR: Invalid constant value
+}
+const instances are canonicalized which means that equal instances point to the same object in memory space when running.
For example this is a “cheap” operation:
void main() {
+ [for(var i = 0; i < 20000; i += 1) Robot(5, 170, ["Walter"])];
+}
+And yes, using const constructors can improve performance in Flutter applications.
If we wanted the weight argument to be optional we’d have to declare it at the end:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ const Robot(this.height, this.names, [this.weight = 170]);
+}
+
+void main() {
+ final r = Robot(5, ["Walter"]);
+ print(r.weight); // 170
+}
+Having to construct a robot like Robot(5, ["Walter"]) is not very explicit.
Dart has named arguments! Naturally, they can be provided in any order and are all optional by default:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ Robot({ this.height, this.weight, this.names });
+}
+
+void main() {
+ final r = Robot(height: 5, names: ["Walter"]);
+ print(r.height); // 5
+}
+But we can annotate a field with @required:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ Robot({ this.height, @required this.weight, this.names });
+}
+(or use assert(weight != null) in the initializer for a runtime check!)
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ Robot({ this.height = 7, this.weight = 100, this.names = const [] });
+}
+
+void main() {
+ print(Robot().height); // 7
+ print(Robot().weight); // 100
+ print(Robot().names); // []
+}
+It’s important to note that these default values must be constant!
Alternatively, we can use the ?? (“if-null”) operator in the assignment to provide any constant or static computation:
class Robot {
+ final double height;
+ final double weight;
+ final List<String> names;
+
+ Robot({ height, weight, this.names = const [] }) : height = height ?? 7, weight = weight ?? int.parse("100");
+}
+
+void main() {
+ print(Robot().height); // 7
+ print(Robot().weight); // 100
+}
+How about making the attributes private?
class Robot {
+ final double _height;
+ final double _weight;
+ final List<String> _names;
+
+ Robot({ this._height, this._weight, this._names }); // ERROR: Named optional parameters can't start with an underscore
+}
+It fails! Unlike with positional arguments, we need to specify the mappings in the initializer:
class Robot {
+ final double _height;
+ final double _weight;
+ final List<String> _names;
+
+ Robot({ height, weight, names }) : _height = height, _weight = weight, _names = names;
+
+ get height => _height;
+ get weight => _weight;
+ get names => _names;
+}
+
+void main() {
+ print(Robot(height: 5).height); // 5
+}
+Both positional and named argument styles can be used together:
class Robot {
+ final double _height;
+ final double _weight;
+ final List<String> _names;
+
+ Robot(height, { weight, names }) :
+ _height = height,
+ _weight = weight,
+ _names = names;
+
+ get height => _height;
+ get weight => _weight;
+}
+
+void main() {
+ var r = Robot(7, weight: 120);
+ print(r.height); // 7
+ print(r.weight); // 120
+}
+Not only can arguments be named. We can give names to any number of constructors:
class Robot {
+ final double height;
+ Robot(this.height);
+
+ Robot.fromPlanet(String planet) : height = (planet == 'geonosis') ? 2 : 7;
+ Robot.copy(Robot other) : this(other.height);
+}
+
+void main() {
+ print(Robot.copy(Robot(7)).height); // 7
+ print(new Robot.fromPlanet('geonosis').height); // 2
+ print(new Robot.fromPlanet('earth').height); // 7
+}
+What happened in copy? We used this to call the default constructor, effectively “redirecting” the instantiation.
(new is optional but I sometimes like to use it, since it clearly states the intent.)
Invoking named super constructors works as expected:
class Machine {
+ String name;
+ Machine();
+ Machine.named(this.name);
+}
+
+class Robot extends Machine {
+ final double height;
+ Robot(this.height);
+
+ Robot.named({ height, name }) : this.height = height, super.named(name);
+}
+
+void main() {
+ print(Robot.named(height: 7, name: "Walter").name); // Walter
+}
+Note that named constructors require an unnamed constructor to be defined!
But what if we didn’t want to expose a public constructor? Only named?
We can make a constructor private by prefixing it with an underscore:
class Robot {
+ Robot._();
+}
+Applying this knowledge to our previous example:
class Machine {
+ String name;
+ Machine._();
+ Machine.named(this.name);
+}
+
+class Robot extends Machine {
+ final double height;
+ Robot._(this.height, name) : super.named(name);
+
+ Robot.named({ height, name }) : this._(height, name);
+}
+
+void main() {
+ print(Robot.named(height: 7, name: "Walter").name); // Walter
+}
+The named constructor is “redirecting” to the private default constructor (which in turn delegates part of the creation to its Machine ancestor).
Consumers of this API only see Robot.named() as a way to get robot instances.

We said constructors were not allowed to return. Guess what?
Factory constructors can!
class Robot {
+ final double height;
+
+ Robot._(this.height);
+
+ factory Robot() {
+ return Robot._(7);
+ }
+}
+
+void main() {
+ print(Robot().height); // 7
+}
+Factory constructors are syntactic sugar for the “factory pattern”, usually implemented with static functions.
They appear like a constructor from the outside (useful for example to avoid breaking API contracts), but internally they can delegate instance creation invoking a “normal” constructor. This explains why factory constructors do not have initializers.
Since factory constructors can return other instances (so long as they satisfy the interface of the current class), we can do very useful things like:
They work with both normal and named constructors!
Here’s our robot warehouse, that only supplies one robot per height:
class Robot {
+ final double height;
+
+ static final _cache = <double, Robot>{};
+
+ Robot._(this.height);
+
+ factory Robot(height) {
+ return _cache[height] ??= Robot._(height);
+ }
+}
+
+void main() {
+ final r1 = Robot(7);
+ final r2 = Robot(7);
+ final r3 = Robot(9);
+
+ print(r1.height); // 7
+ print(r2.height); // 7
+ print(identical(r1, r2)); // true
+ print(r3.height); // 9
+ print(identical(r2, r3)); // false
+}
+Finally, to demonstrate how a factory would instantiate subclasses, let’s create different robot brands that calculate prices as a function of height:
abstract class Robot {
+ factory Robot(String brand) {
+ if (brand == 'fanuc') return Fanuc(2);
+ if (brand == 'yaskawa') return Yaskawa(9);
+ if (brand == 'abb') return ABB(7);
+ throw "no brand found";
+ }
+ double get price;
+}
+
+class Fanuc implements Robot {
+ final double height;
+ Fanuc(this.height);
+ double get price => height * 2922.21;
+}
+
+class Yaskawa implements Robot {
+ final double height;
+ Yaskawa(this.height);
+ double get price => height * 1315 + 8992;
+}
+
+class ABB implements Robot {
+ final double height;
+ ABB(this.height);
+ double get price => height * 2900 - 7000;
+}
+
+void main() {
+ try {
+ print(Robot('fanuc').price); // 5844.42
+ print(Robot('abb').price); // 13300
+ print(Robot('flutter').price);
+ } catch (err) {
+ print(err); // no brand found
+ }
+}
+Singletons are classes that only ever create one instance. We think of this as a specific case of caching!
Let’s implement the singleton pattern in Dart:
class Robot {
+ static final Robot _instance = new Robot._(7);
+ final double height;
+
+ factory Robot() {
+ return _instance;
+ }
+
+ Robot._(this.height);
+}
+
+void main() {
+ var r1 = Robot();
+ var r2 = Robot();
+ print(identical(r1, r2)); // true
+ print(r1 == r2); // true
+}
+The factory constructor Robot(height) simply always returns the one and only instance that was created when loading the Robot class. (So in this case, I prefer not to use new before Robot.)
Dart defines implicit interfaces. What does this mean?
In your app you’d have:
class Session {
+ authenticate() { // impl }
+}
+or
abstract class Session {
+ authenticate();
+}
+And for example in tests:
class MockSession implements Session {
+ authenticate() { // mock impl }
+}
+No need to define a separate interface, just use regular or abstract classes!
Why is FutureBuilder firing multiple times? My future should be called just once!
It appears that this build method is rebuilding unnecessarily:
@override
+Widget build(context) {
+ return FutureBuilder<String>(
+ future: callAsyncFetch(), // called all the time!!! 😡
+ builder: (context, snapshot) {
+ // rebuilding all the time!!! 😡
+ }
+ );
+}
+This causes unintentional network refetches, recomputes and rebuilds – which can also be an expensive problem if using Firebase, for example.
Well, let me tell you something…
This is not a bug 🐞, it’s a feature ✅!
Let’s quickly see why… and how to fix it!
Imagine the FutureBuilder’s parent is a ListView. This is what happens:
build fires many times per second to update the screencallAsyncFetch() gets invoked once per build returning new Futures every timedidUpdateWidget in the FutureBuilder compares old and new Futures; if different it calls the builder againbuilder refires once for every call to the parent’s build… that is, A LOT(Remember: Flutter is a declarative framework. This means it will paint the screen as many times as needed to reflect the UI you declared, based on the latest state)
We clearly must take the Future out of this build method!
A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:
class MyWidget extends StatefulWidget {
+ @override
+ _MyWidgetState createState() => _MyWidgetState();
+}
+
+class _MyWidgetState extends State<MyWidget> {
+ Future<String> _future;
+
+ @override
+ void initState() {
+ _future = callAsyncFetch();
+ super.initState();
+ }
+
+ @override
+ Widget build(context) {
+ return FutureBuilder<String>(
+ future: _future,
+ builder: (context, snapshot) {
+ // ...
+ }
+ );
+ }
+}
+We’re caching a value (in other words, memoizing) such that the build method can now call our code a million times without problems.
Here we have a sample parent widget that rebuilds every 3 seconds. It’s meant to represent any widget that triggers rebuilds like, for example, a user scrolling a ListView.
The screen is split in two:
StatelessWidget containing a FutureBuilder. It’s fed a new Future that resolves to the current date in secondsStatefulWidget containing a FutureBuilder. A new Future (that also resolves to the current date in seconds) is cached in the State object. This cached Future is passed into the FutureBuilderHit Run and see the difference (wait at least 3 seconds). Rebuilds are also logged to the console.
The top future (stateless) gets called and triggered all the time (every 3 seconds in this example).
The bottom (stateful) can be called any amount of times without changing.
Are you using Provider by any chance? You can simply use a FutureProvider instead of the StatefulWidget above:
class MyWidget extends StatelessWidget {
+ // Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
+ @override
+ Widget build(BuildContext context) {
+ // print('building widget');
+ return FutureProvider<String>(
+ create: (_) {
+ // print('calling future');
+ return callAsyncFetch();
+ },
+ child: Consumer<String>(
+ builder: (_, value, __) => Text(value ?? 'Loading...'),
+ ),
+ );
+ }
+}
+Much nicer, if you ask me.
Another option is using the fantastic Flutter Hooks library with the useMemoized hook for the memoization (caching):
class MyWidget extends HookWidget {
+ @override
+ Widget build(BuildContext context) {
+ final future = useMemoized(() {
+ // Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
+ callAsyncFetch(); // or your own async function
+ });
+ return FutureBuilder<String>(
+ future: future,
+ builder: (context, snapshot) {
+ return Text(snapshot.hasData ? snapshot.data : 'Loading...');
+ }
+ );
+ }
+}
+Your build methods should always be pure, that is, never have side-effects (like updating state, calling async functions).
Remember that builders are ultimately called by build!
The shortest, most elegant and reliable way to get HH:mm:ss from a Duration is doing:
format(Duration d) => d.toString().split('.').first.padLeft(8, "0");
+Example usage:
main() {
+ final d1 = Duration(hours: 17, minutes: 3);
+ final d2 = Duration(hours: 9, minutes: 2, seconds: 26);
+ final d3 = Duration(milliseconds: 0);
+ print(format(d1)); // 17:03:00
+ print(format(d2)); // 09:02:26
+ print(format(d3)); // 00:00:00
+}
+If we are dealing with smaller durations and needed only minutes and seconds:
format(Duration d) => d.toString().substring(2, 7);
+By calling repositoryInitializerProvider again with Riverpod’s refresh we can reinitialize Flutter Data.
class TasksApp extends HookConsumerWidget {
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ return MaterialApp(
+ home: RefreshIndicator(
+ onRefresh: () async => ref.container.refresh(repositoryInitializerProvider.future),
+ child: Scaffold(
+ body: Center(
+ child: ref.watch(repositoryInitializerProvider).when(
+ error: (error, _) => Text(error.toString()),
+ loading: () => const CircularProgressIndicator(),
+ data: (_) => TasksScreen(),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+The global onError handler will call logout if certain conditions are met:
mixin BaseAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ @override
+ FutureOr<Null?> onError<Null>(DataException e) async {
+ // Automatically logout user if a 401/403 is returned from any API response.
+ if (e.statusCode == 401 || e.statusCode == 403) {
+ await read(sessionProvider).logOut();
+ return null;
+ }
+
+ throw e;
+ }
+}
+mixin AppointmentAdapter on RemoteAdapter<Appointment> {
+ Future<Appointment?> fetchNext() async {
+ return await sendRequest(
+ baseUrl.asUri / type / 'next',
+ onSuccess: (data) => deserialize(data).model,
+ );
+ }
+}
+Using sendRequest we have both fine-grained control over our request while leveraging existing adapter features such as type, baseUrl, deserialize and any other customizations.
Adapters are applied on RemoteAdapter but Flutter Data will automatically create shortcuts to call these custom methods.
final nextAppointment = await appointmentRepository.appointmentAdapter.fetchNext();
+Every time I do a flutter create project I get the default “counter” sample app full of comments.
While it’s great for the very first time, I now want to get up and running with a minimal base app that fits in my screen.
Here are a few options to copy-paste into lib/main.dart.
// lib/main.dart
+
+import 'package:flutter/widgets.dart';
+
+main() => runApp(MyApp());
+
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(context) => Center(
+ child: Text('Hello Flutter!', textDirection: TextDirection.ltr)
+ );
+}
+Can’t get smaller than this!
See it live:
// lib/main.dart
+
+import 'package:flutter/material.dart';
+
+main() => runApp(MyApp());
+
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(context) {
+ return MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: Text('Hello World'),
+ ),
+ ),
+ );
+ }
+}
+See it live:
import 'package:flutter/material.dart';
+
+main() => runApp(MinimalStatefulApp());
+
+class MinimalStatefulApp extends StatefulWidget {
+ @override
+ _MinimalState createState() => _MinimalState();
+}
+
+class _MinimalState extends State<MinimalStatefulApp> {
+
+ int _counter = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onDoubleTap: () => setState(() => _counter++),
+ child: Center(
+ child: Text(
+ 'Counter: $_counter',
+ textDirection: TextDirection.ltr,
+ ),
+ ),
+ );
+ }
+}
+See it live (double tap to increment):
flutter_hooks)// lib/main.dart
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+main() => runApp(MyApp());
+
+class MyApp extends HookWidget {
+ @override
+ Widget build(context) {
+ final counter = useState(0);
+ return GestureDetector(
+ onDoubleTap: () => counter.value++,
+ child: Center(
+ child: Text(
+ 'Counter: ${counter.value}',
+ textDirection: TextDirection.ltr,
+ ),
+ ),
+ );
+ }
+}
+It uses hooks which remove the boilerplate of a classic StatefulWidget. Make sure you add the flutter_hooks dependency to pubspec.yaml!
Here’s how you could access nested resources such as: /posts/1/comments
mixin NestedURLAdapter on RemoteAdapter<Comment> {
+ // ...
+ @override
+ String urlForFindAll(params) => '/posts/${params['postId']}/comments';
+
+ // or even
+ @override
+ String urlForFindAll(params) {
+ final postId = params['postId'];
+ if (postId != null) {
+ return '/posts/${params['postId']}/comments';
+ }
+ return super.urlForFindAll(params);
+ }
+}
+and call it like:
final comments = await commentRepository.findAll(params: {'postId': post.id });
+Flutter Data is extended via adapters.
mixin UserURLAdapter on RemoteAdapter<User> {
+ @override
+ String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo';
+}
+Need to apply the adapter to all your models? Make it generic:
mixin UserURLAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ @override
+ String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo';
+}
+In this example we completely override findAll to return random models:
mixin FindAllAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ @override
+ Future<List<T>> findAll({
+ bool? remote,
+ Map<String, dynamic>? params,
+ Map<String, String>? headers,
+ bool? syncLocal,
+ OnDataError<List<T>>? onError,
+ }) async {
+ // could use: super.findAll();
+ return _generateRandomModels<T>();
+ }
+}
+In this example we override URLs to hit finder endpoints with snake case, and for save to always use HTTP PUT:
mixin URLAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ @override
+ String urlForFindAll(Map<String, dynamic> params) => type.snakeCase;
+
+ @override
+ String urlForFindOne(id, Map<String, dynamic> params) =>
+ '${type.snakeCase}/$id';
+
+ @override
+ DataRequestMethod methodForSave(id, Map<String, dynamic> params) {
+ return DataRequestMethod.PUT;
+ }
+}
+Custom headers and query parameters can be passed into all finders and watchers (findAll, findOne, save, watchOne etc) but sometimes defaults are necessary.
Here is how:
mixin BaseAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ final _localStorageService = read(localStorageProvider);
+
+ @override
+ String get baseUrl => "http://my.remote.url:8080/";
+
+ @override
+ FutureOr<Map<String, String>> get defaultHeaders async {
+ final token = _localStorageService.getToken();
+ return await super.defaultHeaders & {'Authorization': token};
+ }
+
+ @override
+ FutureOr<Map<String, dynamic>> get defaultParams async {
+ return await super.defaultParams & {'v': 1};
+ }
+}
+An example on how to override and use a more advanced HTTP client.
Here the connectionTimeout is increased, and an HTTP proxy enabled.
mixin HttpProxyAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+ HttpClient? _httpClient;
+ IOClient? _ioClient;
+
+ @override
+ http.Client get httpClient {
+ _httpClient ??= HttpClient();
+ _ioClient ??= IOClient(_httpClient);
+
+ // increasing the timeout
+ _httpClient!.connectionTimeout = const Duration(seconds: 5);
+
+ // using a proxy
+ _httpClient!.badCertificateCallback =
+ ((X509Certificate cert, String host, int port) => true);
+ _httpClient!.findProxy = (uri) => 'PROXY (proxy url)';
+
+ return _ioClient!;
+ }
+
+ @override
+ Future<void> dispose() async {
+ _ioClient?.close();
+ _ioClient = null;
+ _httpClient = null;
+ super.dispose();
+ }
+}
+
Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart’s syntax.
(Pros and cons of choosing Flutter/Dart is outside the scope of this article.)
So if you have a JS background and want to build apps with this awesome framework, read on. Let’s see how these two puppies fair against each other!
// js
+
+var dog1 = "Lucy"; // variable
+let dog2 = "Milo"; // block scoped variable
+
+const maleDogs = ["Max", "Bella"]; // mutable single-assignment variable
+maleDogs.push("Cooper"); // ✅
+maleDogs = ["Cooper"]; // ❌
+
+const femaleDogs = Object.freeze(["Luna", "Bella"]); // runtime constant
+femaleDogs.push("Winona"); // ❌
+femaleDogs = ["Winona"]; // ❌
+And now in Dart:
// dart
+
+main() {
+ var dog1 = "Max"; // variable
+
+ final maleDogs = ["Milo"]; // mutable single-assignment variable
+ maleDogs.add("Cooper"); // ✅
+ maleDogs = ["Cooper"]; // ❌
+
+ const femaleDogs = ["Luna", "Bella"]; // compile time constant
+ femaleDogs.add("Winona"); // ❌
+ femaleDogs = ["Winona"]; // ❌
+
+ // alternative const syntax without assignment
+ walkingTimes(const [7, 9, 11]); // ✅
+ walkingTimes(const [DateTime.now()]); // ❌
+}
+Unlike Javascript, const in Dart lives up to its meaning. The whole object is checked at compile time to ensure it’s completely immutable.
Therefore any element inside femaleDogs has to be a const too. Not the case for the elements inside maleDogs, which are not necessarily final.
Dart doesn’t need let because lexical scope works correctly.
Trailing semicolons are required in Dart. In Javascript you can omit the ; (you have to be careful, though!)
Let’s set a default value of 1 if bones is falsey (in Javascript) or null (in Dart).
// js
+
+var bones;
+bones = bones || 1;
+console.log(bones); // 1
+// dart
+
+main() {
+ var bones;
+ bones ??= 1; // OR: bones = bones ?? 1
+ print(bones); // 1
+}
+This is a great Javascript-only feature.
// js
+
+var [dog, owner] = ["Max", "Frank"];
+console.log(dog); // Max
+[owner, dog] = [dog, owner];
+console.log(dog); // Frank
+Not possible in Dart yet.
Let’s go ahead and have a look at falsey values that only exist in Javascript.
// js
+
+var collar = false,
+ toys = null,
+ amountOfMeals = 0 / 0, // NaN
+ owner = "",
+ age = 0,
+ breed;
+
+if (!collar) console.log("bark"); // bark
+if (!toys) console.log("bark"); // bark
+if (!amountOfMeals) console.log("bark"); // bark
+if (!owner) console.log("bark"); // bark
+if (!age) console.log("bark"); // bark
+if (!breed) console.log("bark"); // bark
+In Dart, undefined values are null. Expressions in conditionals may only be boolean.
// dart
+
+main() {
+ var collar = false,
+ toys = null,
+ amountOfMeals = 0 / 0, // NaN
+ owner = "",
+ age = 0,
+ breed;
+
+ if (!collar) print('bark'); // bark
+ if (toys == null) print('bark'); // bark
+ if (amountOfMeals.isNaN) print('bark'); // bark
+ if (owner.isEmpty) print('bark'); // bark
+ if (age == 0) print('bark'); // bark
+ if (breed == null) print('bark'); // bark
+}
+In Dart, 'Rocky' - 2 is an error – not NaN 🤔 Fortunately Dart didn’t pick up Javascript’s 💩
// js
+
+function bark() {
+ return "WOOF";
+}
+
+var bday = (age) => age + 1;
+// dart
+
+bark() {
+ return "WOOF";
+}
+
+var bday = (age) => age + 1;
+One-liner function syntax looks exactly the same in both languages! In JS, however, parenthesis are optional.
// js
+
+var greet = (name = "Milo") => `Woof! My name is ${name}`;
+console.log(greet()); // Woof! My name is Milo
+// dart
+
+main() {
+ var greet = ({ name = 'Rocky' }) => "Woof! My name is ${name}";
+ print(greet()); // Woof! My name is Rocky
+}
+Dart requires curly braces for optional arguments. String interpolation is practically the same.
// js
+
+const sum = (...meals) => meals.reduce((sum, next) => sum + next, 0);
+console.log(sum(1, 2, 3)); // 6
+Not supported because a Dart function can’t have a variable amount of positional arguments. The alternative is simply:
// dart
+
+main() {
+ final sum = (List<int> meals) => meals.reduce((sum, next) => sum + next);
+ print(sum([1, 2, 3])); // 6
+}
+name should be returned unless address or street are null, in that case the whole expression should return null.
// js
+var name =
+ person.address || person.address.street || person.address.street.name;
+In Dart we have the safe navigation operator:
// dart
+var name = address?.street?.name;
+An Array in Javascript is a List in Dart. An Object in Javascript is a Map in Dart.
// js
+
+var dogArray = ["Lucy", "Cooper", "Zeus"];
+var dogObj = { first: "Lucy", second: "Cooper" };
+var dogSet = new Set(["Lucy", "Cooper", "Zeus"]);
+
+console.log(dogArray.length); // 3
+console.log(Object.keys(dogObj).length); // 2
+console.log(dogSet.size); // 3
+// dart
+
+main() {
+ var dogList = ["Lucy", "Cooper", "Zeus"];
+ var dogMap = { 'first': "Lucy", 'second': "Cooper" }; // could use #first symbol instead
+ var dogSet = { "Lucy", "Cooper", "Zeus" };
+
+ print(dogList.length); // 3
+ print(dogMap.length); // 2
+ print(dogSet.length); // 3
+}
+The value of the array.push(element) expression is always the value of push(element). This is standard behavior.
In Javascript, the array push function returns the length of the array (go figure!). So we can’t possibly have console.log([1, 2, 3].push(4, 5)) result in [1, 2, 3, 4, 5].
// js
+
+var parks = [1, 2, 3];
+parks.push(4, 5);
+console.log(parks); // [1, 2, 3, 4, 5]
+
+var shelters = [1, 2, 3];
+shelters[1] = 4;
+shelters[2] = 5;
+console.log(shelters); // [1, 4, 5]
+In Dart we have the cascade operator list..add(), which allows us to return the list.
// dart
+
+main() {
+ print([1, 2, 3]..add(4)..add(5)); // [1, 2, 3, 4, 5]
+ print([1, 2, 3]..[1]=4..[2]=5); // [1, 4, 5]
+}
+A fluent API is one that allows chaining. jQuery is a great example: $('a').css("underline", "none").html("link!"); as every jQuery function call returns this.
This approach greatly reduces intermediate variables. However, not all APIs are designed this way. The cascade operator allows us to take a regular API and turn it into a fluid API, like what we did above with the list.

// js
+
+var parks = [1, 2, 3];
+parks = parks.concat([4, 5], [6, 7]);
+console.log(parks); // [1, 2, 3, 4, 5, 6, 7]
+To push or concatenate other arrays we can use addAll in the same fashion:
// dart
+
+main() {
+ print([1, 2, 3]..addAll([4, 5])..addAll([6, 7])); // [1, 2, 3, 4, 5, 6, 7]
+}
+But there’s a cleaner way! Using spreads…
// js
+
+console.log([1, 2, 3, ...[4, 5], ...[6, 7]]); // [1, 2, 3, 4, 5, 6, 7]
+// dart
+
+main() {
+ print([1, 2, 3, ...[4, 5], ...[6, 7]]); // [1, 2, 3, 4, 5, 6, 7]
+}
+Same same. Also for objects/maps:
// js
+
+const name = { name: "Luna" };
+const age = { age: 7 };
+console.log({ ...name, ...age }); // { name: "Luna", age: 7 }
+(Notice that we have to use let or const in Javascript.)
// dart
+
+main() {
+ var name = { 'name': "Luna" };
+ var age = { 'age': 7 };
+ print({ ...name, ...age }); // { 'name': "Luna", 'age': 7 }
+}
+But what if P2 has a value sometimes?
// js
+
+const P1 = [4, 5];
+var P2 = Math.random() < 0.5 ? [6, 7] : null;
+
+P2 = P2 || [];
+console.log([1, 2, 3, ...P1, ...P2]); // [1, 2, 3, 4, 5] or [1, 2, 3, 4, 5, 6, 7]
+// dart
+
+import 'dart:math';
+
+const P1 = [4, 5];
+final P2 = Random().nextBool() ? [6, 7] : null;
+
+main() {
+ print([1, 2, 3, ...P1, ...?P2]); // [1, 2, 3, 4, 5] or [1, 2, 3, 4, 5, 6, 7]
+}
+The optional spread operator ...? will only insert the array if it’s not null.
Let’s consider now this example:
const A = 2;
+
+var ages = [1];
+if (Math.random() < 0.5) {
+ ages.push(A);
+}
+console.log(ages); // [1] or [1, 2]
+There is yet another way in Dart of including logic inside arrays:
import 'dart:math';
+const A = 2;
+
+main() {
+ print([1, if (Random().nextBool()) A]); // [1] or [1, 2]
+}
+It’s called a “collection-if”. There’s also “collection-for”:
main() {
+ var ages = [1, 2, 3];
+ print([
+ 1,
+ for(int i in ages) i + 1,
+ 5
+ ]); // [1, 2, 3, 4, 5]
+}
+Extremely elegant! I can’t really think of a Javascript equivalent 🤔
// js
+
+var first = { age: 7 };
+console.log(first.age); // 7
+// dart
+
+main() {
+ var first = { 'age': 7 };
+ print(first['age']); // 7
+}
+// js
+
+// module file
+export const dog = "Luna";
+
+export default function clean(dog) {
+ return doCleaning(dog);
+}
+
+// import
+import { dog } from "module";
+
+import clean from "module";
+Dart, on the other hand, does not need to specify the imports: everything is imported by default. Imports can have prefixes (as) and can “whitelist” (show) and “blacklist” (hide). Ultimately, through static analysis and tree-shaking, whatever is not used will be discarded.
// dart
+
+// module file
+final dog = "Luna";
+
+clean(dog) => _doCleaning(dog);
+
+// import
+import 'module.dart';
+
+// alternatively
+import 'module.dart' as module;
+
Dart is a statically-typed language with strong type inference.
As we’ve seen so far, we almost never need to declare type annotations:
// dart
+
+main() {
+ var age = 1;
+ var pets = ["Cooper", "Luna"];
+ print(age.runtimeType); // int
+ print(pets.runtimeType); // Array<String>
+}
+This means we leverage the power of types without stuffing our code with declarations! But of course we may:
// dart
+
+main() {
+ int age = 5;
+ List<String> pets = ["Cooper", "Luna"];
+ var pets2 = <String>["Cooper", "Luna"];
+ List<String> pets3 = <String>["Cooper", "Luna"];
+}
+Specifying types can bring clarity to code. In our example above declarations are redundant (especially pets3).
Imagine a walk method with no typed arguments, assuming callers will pass an argument of type Distance:
// dart
+
+walk(distance) {
+ print('Walking ${distance.length} miles');
+}
+
+main() {
+ print(walk("86")); // 2
+ print(walk(86)); // ERROR
+ // ...
+}
+Gives all kind of weird behavior. The analyzer doesn’t have enough information to infer a specific type for distance so it uses the dynamic type. It’s equivalent to:
walk(dynamic distance) {
+ print('Walking ${distance.length} miles');
+}
+In short: argument types are very important!
This is recommended, idiomatic Dart:
void walk(Distance distance) {
+ print('Walking ${distance.length} miles');
+}
+
+String walk(int distance) => 'Walking $distance miles';
+Type checking, however, can be explicitly “turned off” at a variable-level by declaring it as dynamic.
main() {
+ dynamic dog = "Charlie";
+ dog = ["char", "lie"]; // compiler NOT type checking!
+ print(dog); // [char, lie]
+}
+Classes are relatively new in Javascript:
// js
+
+class Dog {
+ constructor(name, phone) {
+ this.name = name;
+ this.phone = phone;
+ }
+
+ tag = () => `${this.name}\nIf you found me please call ${this.phone}!`;
+}
+
+console.log(new Dog("Luna", 6198887421).tag());
+// Luna
+// If you found me please call 6198887421!
+In Dart:
// dart
+
+class Dog {
+ final String name;
+ final int phone;
+ Dog(this.name, { this.phone });
+
+ String tag() => "${name}\nIf you found me please call ${phone}!";
+}
+
+main() {
+ print(Dog('Luna', phone: 6198887421).tag());
+ // Luna
+ // If you found me please call 6198887421!
+}
+A few things to note about Dart classes & constructors!
new when calling constructors – that is why I used Dog() (vs new Dog())this to reference fields: it is only used to define constructorsWe use instanceof in Javascript:
// js
+
+class Dog extends Animal {
+ // ...
+}
+
+var animal = getAnimal();
+if (animal instanceof Dog) {
+ console.log("🐶");
+}
+And is in Dart:
// dart
+
+class Dog extends Animal {
+ // ...
+}
+
+main() {
+ var animal = getAnimal();
+ if (animal is Dog) {
+ console.log('🐶');
+ }
+}
+These are methods that extend existing types. In Javascript a function can be added to a prototype:
// js
+
+Object.defineProperties(String.prototype, {
+ kebab: {
+ get: function () {
+ return this.replace(/\s+/g, "-").toLowerCase();
+ },
+ },
+});
+
+console.log("This is Luna".kebab); // this-is-luna
+In Dart:
// dart
+
+extension on String {
+ String get kebab => this.replaceAll(RegExp(r'\s+'), '-').toLowerCase();
+}
+
+main() {
+ print("This is Luna".kebab); // this-is-luna
+}
+Static extension members are available since Dart 2.6 and open up very interesting possibilities for API design, like the fantastic time.dart ⏰. Now we can do stuff like:
Duration timeOfSleep = 7.hours + 32.minutes + 8.seconds;
+DateTime medicated = 5.minutes.ago;
+// js
+
+var dog = JSON.parse(
+ '{ "name": "Willy", "medications": { "doxycycline": true } }'
+);
+
+console.log(Object.keys(dog.medications).lnegth); // undefined
+Javascript is a dynamic language. Misspelling length just returns undefined.
list.isEmpty, in Javascript we must use the length for this: !array.length.In Dart:
// dart
+
+import 'dart:convert';
+
+main() {
+ var dog = jsonDecode('{ "name": "Willy", "medications": { "doxycycline": true } }');
+ print(dog.runtimeType); // _InternalLinkedHashMap<String, dynamic>
+ print(dog['medications'].lnegth); // NoSuchMethodError: Class '_InternalLinkedHashMap<String, dynamic>' has no instance getter 'lnegth'.
+}
+It is known that keys of a JSON object are strings, but values can be of many different types. Hence the resulting map is of type <String, dynamic>.
When we misspell length on a dynamic variable there is no type checking, so the error we get is at runtime.
Another gigantic chaos in the world of Javascript. We won’t get into it – just say that for equality we only use === to tell if both objects are strictly the same.
If we need to verify equivalence of two different objects, we’d use a deep comparison like _.isEqual in Lodash.
// js
+
+class DogTag {
+ constructor(id) {
+ this.id = id;
+ }
+}
+
+var tag1 = new DogTag(9);
+var tag2 = new DogTag(9);
+
+console.log(_.isEqual(tag1, tag2)); // true (same ID, same tag)
+console.log(tag1 === tag2); // false (not the same object in memory)
+In Dart, === is identical and isEqual is ==. You can override the == operator to check for equality between two objects 🙌
// dart
+
+class DogTag {
+ int id;
+ DogTag(this.id);
+ operator ==(other) => this.id == other.id;
+}
+
+main() {
+ var tag1 = DogTag(9);
+ var tag2 = DogTag(9);
+
+ print(tag1 == tag2); // true (same ID, same tag)
+ print(identical(tag1, tag2)); // false (not the same object in memory)
+}
+
While a solution is being worked on for ESNext, there is currently no proper way of defining private properties in Javascript.
Dart uses a _ prefix which makes the variable private. And we can use a standard getter to expose it to the outside world:
// dart
+
+class Dog {
+ String name;
+ int _age;
+
+ Dog(this.name, this._age);
+
+ get age => _age;
+}
+
+main() {
+ var zeus = new Dog("Zeus", 7);
+ print(zeus.age); // 7
+
+ zeus.age = 8; // ERROR: No setter named 'age' in class 'Dog'
+ zeus._age = 8;
+ print(zeus.age); // 8
+}
+Makes sense?
Uhhmmm… we are setting the private variable and it actually works? 🤔
Private in Dart means library-private. If we placed the Dog class in models.dart:
// dart
+
+import 'models.dart';
+
+main() {
+ var zeus = new Dog("Zeus", 7);
+ print(zeus.age); // 7
+
+ zeus.age = 8; // ERROR: No setter named 'age' in class 'Dog'
+ zeus._age = 8; // ERROR: The setter '_age' isn't defined for the class 'Dog'.
+ print(zeus.age); // 7
+}
+Setters work in a similar way.
The Promise API in Javascript is analogous to the Future API in Dart.
Both languages support then() and async/await.
Let’s appreciate the differences through a food dispenser that will pour out dog chow in 4 seconds.
// js
+
+function dispenseFood() {
+ return new Promise((resolve) => setTimeout(resolve, 4000)).then(
+ () => "DOG CHOW"
+ );
+}
+
+async function main() {
+ console.log("Idle.");
+ var food = await dispenseFood();
+ console.log(food); // DOG CHOW
+}
+
+main();
+
+// or
+dispenseFood().then(console.log); // .catch();
+Very similar in Dart:
// dart
+
+Future<String> dispenseFood() {
+ return Future.delayed(Duration(seconds: 4), () => 'DOG CHOW');
+}
+
+main() async {
+ print('Idle.');
+ String food = await dispenseFood();
+ print(food); // DOG CHOW
+
+ // or
+ dispenseFood().then(print); // .catchError();
+}
+Well… maybe 🤪 Pending for a next revision:
As you may have noticed we simply highlighted differences between syntaxes. Not comparing their merits, popularity, available libraries, and many other considerations. There will be another opinionated article discussing which is the best tool for which job.
Type in your terminal:
flutter upgrade
+This will update Flutter to the latest version in the current channel. Most likely you have it set in stable.
flutter channel
+# Flutter channels:
+# beta
+# dev
+# master
+# * stable
+Do you want to live in the cutting edge? Switching channels is easy:
flutter channel dev
+# Switching to flutter channel 'dev'...
+# ...
+And run upgrade again:
flutter upgrade
+