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 @@ +How to Build Widgets with an Async Method Call - Flutter Data

How to Build Widgets with an Async Method Call

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.)

🛠 How do we fix this with Flutter best practices?

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:

  • 📭 no data… we show a progress indicator
  • data from our future… we use it to feed any widgets for display!
  • error from our future… we show an appropriate message
Do you think the answer to this problem is a 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!

🎩 Under the hood: FutureBuilder

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?.

\ No newline at end of file diff --git a/articles/checking-null-aware-operators-dart/index.html b/articles/checking-null-aware-operators-dart/index.html new file mode 100644 index 0000000..1ca834a --- /dev/null +++ b/articles/checking-null-aware-operators-dart/index.html @@ -0,0 +1,41 @@ +Checking Nulls and Null-Aware Operators in Dart - Flutter Data

Checking Nulls and Null-Aware Operators in Dart

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.

Default operator: ??

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.

Fallback assignment operator: ??=

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();
+

Safe navigation operator: ?.

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;
+

groovy

Optional spread operator: ...?

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 */
+

Non-nullable types

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;
+
\ No newline at end of file diff --git a/articles/configure-get-it/index.html b/articles/configure-get-it/index.html new file mode 100644 index 0000000..541bc62 --- /dev/null +++ b/articles/configure-get-it/index.html @@ -0,0 +1,77 @@ +Configure Flutter Data to Work with GetIt - Flutter Data

Configure Flutter Data to Work with GetIt

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!

\ No newline at end of file diff --git a/articles/configure-provider/index.html b/articles/configure-provider/index.html new file mode 100644 index 0000000..b7307f5 --- /dev/null +++ b/articles/configure-provider/index.html @@ -0,0 +1,90 @@ +Configure Flutter Data to Work with Provider - Flutter Data

Configure Flutter Data to Work with Provider

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!

\ No newline at end of file diff --git a/articles/custom-deserialization-adapter/index.html b/articles/custom-deserialization-adapter/index.html new file mode 100644 index 0000000..70f7603 --- /dev/null +++ b/articles/custom-deserialization-adapter/index.html @@ -0,0 +1,20 @@ +Custom Deserialization Adapter - Flutter Data

Custom Deserialization Adapter

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.

\ No newline at end of file diff --git a/articles/dart-final-const-difference/index.html b/articles/dart-final-const-difference/index.html new file mode 100644 index 0000000..03d0f41 --- /dev/null +++ b/articles/dart-final-const-difference/index.html @@ -0,0 +1,14 @@ +Final vs const in Dart - Flutter Data

Final vs const in Dart

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
+
Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!
\ No newline at end of file diff --git a/articles/dart-getter-cache-computed-properties/index.html b/articles/dart-getter-cache-computed-properties/index.html new file mode 100644 index 0000000..ffbb0e3 --- /dev/null +++ b/articles/dart-getter-cache-computed-properties/index.html @@ -0,0 +1,14 @@ +Dart Getter Shorthand to Cache Computed Properties - Flutter Data

Dart Getter Shorthand to Cache Computed Properties

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 ??=.

Check out Null-Aware Operators in Dart for a complete guide on dealing with nulls in Dart!
\ No newline at end of file diff --git a/content/articles/deconstructing-dart-constructors/featured-mini.jpg b/articles/deconstructing-dart-constructors/featured-mini.jpg similarity index 100% rename from content/articles/deconstructing-dart-constructors/featured-mini.jpg rename to articles/deconstructing-dart-constructors/featured-mini.jpg diff --git a/content/articles/deconstructing-dart-constructors/featured.jpg b/articles/deconstructing-dart-constructors/featured.jpg similarity index 100% rename from content/articles/deconstructing-dart-constructors/featured.jpg rename to articles/deconstructing-dart-constructors/featured.jpg diff --git a/articles/deconstructing-dart-constructors/index.html b/articles/deconstructing-dart-constructors/index.html new file mode 100644 index 0000000..e5ddb63 --- /dev/null +++ b/articles/deconstructing-dart-constructors/index.html @@ -0,0 +1,437 @@ +Deconstructing Dart Constructors - Flutter Data

Deconstructing Dart Constructors

Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories…

Read this post and you will become an expert!

Photo by Arseny Togulev on Unsplash

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.

Did you know you can try out Dart and Flutter code in DartPad?

Initializing…

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);
+}
+

Accessors and mutators

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;
+}
+

Constructor bodies

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

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);
+}
+

Default values

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
+}
+

Immutable robots

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.

Optional arguments always last!

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
+}
+

Naming things

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!)

Naming things with defaults

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
+}
+

Mixing it up

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
+}
+

Named constructors

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!

Keeping it private

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.

A robot factory

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:

  • caching: conditionally returning existing objects (they might be expensive to create)
  • subclasses: returning other instances such as subclasses

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

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.)

\ No newline at end of file diff --git a/articles/define-interface-dart/index.html b/articles/define-interface-dart/index.html new file mode 100644 index 0000000..70060d4 --- /dev/null +++ b/articles/define-interface-dart/index.html @@ -0,0 +1,18 @@ +How To Define an Interface in Dart - Flutter Data

How To Define an Interface in Dart

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!

Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!
\ No newline at end of file diff --git a/articles/future-async-called-multiple-times/index.html b/articles/future-async-called-multiple-times/index.html new file mode 100644 index 0000000..6e36512 --- /dev/null +++ b/articles/future-async-called-multiple-times/index.html @@ -0,0 +1,73 @@ +Why Is My Future/Async Called Multiple Times? - Flutter Data

Why Is My Future/Async Called Multiple Times?

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!

Understanding the problem

Imagine the FutureBuilder’s parent is a ListView. This is what happens:

  • 🧻 User scrolls list
  • 🔥 build fires many times per second to update the screen
  • callAsyncFetch() gets invoked once per build returning new Futures every time
  • = didUpdateWidget in the FutureBuilder compares old and new Futures; if different it calls the builder again
  • 😩 Since instances are always new (always different to the old one) the builder 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)

A quick fix 🔧

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.

Live example

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:

  • Top: a StatelessWidget containing a FutureBuilder. It’s fed a new Future that resolves to the current date in seconds
  • Bottom: a StatefulWidget 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 FutureBuilder

Hit 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.

Cleaner ways 🛀

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.

Tip! It’s a fully functional example. Comment out those lines and try it out in your own editor!

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...');
+      }
+    );
+  }
+}
+

Takeaway

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!

\ No newline at end of file diff --git a/articles/how-to-format-duration/index.html b/articles/how-to-format-duration/index.html new file mode 100644 index 0000000..f60bd50 --- /dev/null +++ b/articles/how-to-format-duration/index.html @@ -0,0 +1,19 @@ +How to Format a Duration as a HH:MM:SS String - Flutter Data

How to Format a Duration as a HH:MM:SS String

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);
+
\ No newline at end of file diff --git a/articles/how-to-reinitialize-flutter-data/index.html b/articles/how-to-reinitialize-flutter-data/index.html new file mode 100644 index 0000000..7253d0a --- /dev/null +++ b/articles/how-to-reinitialize-flutter-data/index.html @@ -0,0 +1,28 @@ +How to Reinitialize Flutter Data - Flutter Data

How to Reinitialize Flutter Data

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(),
+                ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
\ No newline at end of file diff --git a/articles/index.html b/articles/index.html new file mode 100644 index 0000000..304644b --- /dev/null +++ b/articles/index.html @@ -0,0 +1,9 @@ +Flutter Data
\ No newline at end of file diff --git a/articles/index.xml b/articles/index.xml new file mode 100644 index 0000000..2a573c2 --- /dev/null +++ b/articles/index.xml @@ -0,0 +1,58 @@ +Flutter Data/articles/Recent content on Flutter DataHugo -- gohugo.ioen-usSat, 18 Dec 2021 17:08:28 -0300How to Reinitialize Flutter Data/articles/how-to-reinitialize-flutter-data/Sat, 18 Dec 2021 17:08:28 -0300/articles/how-to-reinitialize-flutter-data/By calling repositoryInitializerProvider again with Riverpod&rsquo;s refresh we can reinitialize Flutter Data. +class TasksApp extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return MaterialApp( home: RefreshIndicator( onRefresh: () async =&gt; ref.container.refresh(repositoryInitializerProvider.future), child: Scaffold( body: Center( child: ref.watch(repositoryInitializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) =&gt; TasksScreen(), ), ), ), ), ); } }Nested Resources Adapter/articles/nested-resources-adapter/Thu, 09 Dec 2021 23:17:30 -0300/articles/nested-resources-adapter/Here&rsquo;s how you could access nested resources such as: /posts/1/comments +mixin NestedURLAdapter on RemoteAdapter&lt;Comment&gt; { // ... @override String urlForFindAll(params) =&gt; &#39;/posts/${params[&#39;postId&#39;]}/comments&#39;; // or even @override String urlForFindAll(params) { final postId = params[&#39;postId&#39;]; if (postId != null) { return &#39;/posts/${params[&#39;postId&#39;]}/comments&#39;; } return super.urlForFindAll(params); } } and call it like: +final comments = await commentRepository.findAll(params: {&#39;postId&#39;: post.id });Custom Deserialization Adapter/articles/custom-deserialization-adapter/Thu, 09 Dec 2021 23:15:44 -0300/articles/custom-deserialization-adapter/Example: +mixin AuthAdapter on RemoteAdapter&lt;User&gt; { Future&lt;String&gt; login(String email, String password) async { return sendRequest( baseUrl.asUri / &#39;token&#39;, method: DataRequestMethod.POST, body: json.encode({&#39;email&#39;: email, &#39;password&#39;: password}), onSuccess: (data) =&gt; data[&#39;token&#39;] as String, ); } } and use it: +final token = await userRepository.authAdapter.login(&#39;e@mail, p*ssword&#39;); Also see JSONAPIAdapter for inspiration.Intercept Logout Adapter/articles/intercept-logout-adapter/Thu, 09 Dec 2021 23:15:11 -0300/articles/intercept-logout-adapter/The global onError handler will call logout if certain conditions are met: +mixin BaseAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override FutureOr&lt;Null?&gt; onError&lt;Null&gt;(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; } }Override findAll Adapter/articles/override-findall-adapter/Thu, 09 Dec 2021 23:14:28 -0300/articles/override-findall-adapter/In this example we completely override findAll to return random models: +mixin FindAllAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override Future&lt;List&lt;T&gt;&gt; findAll({ bool? remote, Map&lt;String, dynamic&gt;? params, Map&lt;String, String&gt;? headers, bool? syncLocal, OnDataError&lt;List&lt;T&gt;&gt;? onError, }) async { // could use: super.findAll(); return _generateRandomModels&lt;T&gt;(); } }Override findOne URL Adapter/articles/override-findone-url-method/Thu, 09 Dec 2021 23:14:28 -0300/articles/override-findone-url-method/In this example we override URLs to hit finder endpoints with snake case, and for save to always use HTTP PUT: +mixin URLAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override String urlForFindAll(Map&lt;String, dynamic&gt; params) =&gt; type.snakeCase; @override String urlForFindOne(id, Map&lt;String, dynamic&gt; params) =&gt; &#39;${type.snakeCase}/$id&#39;; @override DataRequestMethod methodForSave(id, Map&lt;String, dynamic&gt; params) { return DataRequestMethod.PUT; } }Iterator Style Adapter/articles/iterator-style-adapter/Thu, 09 Dec 2021 23:13:36 -0300/articles/iterator-style-adapter/mixin AppointmentAdapter on RemoteAdapter&lt;Appointment&gt; { Future&lt;Appointment?&gt; fetchNext() async { return await sendRequest( baseUrl.asUri / type / &#39;next&#39;, onSuccess: (data) =&gt; 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();Override HTTP Client Adapter/articles/override-http-client-adapter/Thu, 09 Dec 2021 23:10:10 -0300/articles/override-http-client-adapter/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&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { 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) =&gt; true); _httpClient!.findProxy = (uri) =&gt; &#39;PROXY (proxy url)&#39;; return _ioClient!Override Default Headers and Query Parameters/articles/override-headers-query-parameters/Thu, 09 Dec 2021 23:07:40 -0300/articles/override-headers-query-parameters/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&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { final _localStorageService = read(localStorageProvider); @override String get baseUrl =&gt; &#34;http://my.remote.url:8080/&#34;; @override FutureOr&lt;Map&lt;String, String&gt;&gt; get defaultHeaders async { final token = _localStorageService.getToken(); return await super.defaultHeaders &amp; {&#39;Authorization&#39;: token}; } @override FutureOr&lt;Map&lt;String, dynamic&gt;&gt; get defaultParams async { return await super.Configure Flutter Data to Work with GetIt/articles/configure-get-it/Sun, 05 Dec 2021 23:12:05 -0300/articles/configure-get-it/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&lt;Repository&lt;Todo&gt;&gt;(); return GestureDetector( onDoubleTap: () async { print((await repository.Configure Flutter Data to Work with Provider/articles/configure-provider/Sun, 05 Dec 2021 23:12:05 -0300/articles/configure-provider/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&lt;Repository&lt;Todo&gt;?, SessionService?&gt;( lazy: false, create: (_) =&gt; SessionService(), update: (context, repository, service) { if (service != null &amp;&amp; repository != null) { return service..initialize(repository); } return service; }, ), ], child: MaterialApp( home: Scaffold( body: Center( child: Builder( builder: (context) { if (context.Override Base URL Adapter/articles/override-base-url/Fri, 03 Dec 2021 18:45:45 -0300/articles/override-base-url/Flutter Data is extended via adapters. +mixin UserURLAdapter on RemoteAdapter&lt;User&gt; { @override String get baseUrl =&gt; &#39;https://my-json-server.typicode.com/flutterdata/demo&#39;; } Need to apply the adapter to all your models? Make it generic: +mixin UserURLAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override String get baseUrl =&gt; &#39;https://my-json-server.typicode.com/flutterdata/demo&#39;; }Deconstructing Dart Constructors/articles/deconstructing-dart-constructors/Wed, 12 Feb 2020 13:43:48 -0500/articles/deconstructing-dart-constructors/Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories&hellip; +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:Dart Getter Shorthand to Cache Computed Properties/articles/dart-getter-cache-computed-properties/Sat, 04 Jan 2020 13:43:48 -0500/articles/dart-getter-cache-computed-properties/An elegant Dart getter shorthand used to cache computed properties: +T get foo =&gt; _foo ??= _computeFoo(); // which depends on having T _foo; T _computeFoo() =&gt; /** ... **/; It makes use of the fallback assignment operator ??=. +Check out Null-Aware Operators in Dart for a complete guide on dealing with nulls in Dart!Final vs const in Dart/articles/dart-final-const-difference/Sat, 04 Jan 2020 13:43:48 -0500/articles/dart-final-const-difference/What&rsquo;s the difference between final and const in Dart? +Easy! +Final means single-assignment. +Const means immutable. +Let&rsquo;s see an example: +final _final = [2, 3]; const _const = [2, 3]; _final = [4,5]; // ERROR: can&#39;t re-assign _final.add(6); // OK: can mutate _const.add(6); // ERROR: can&#39;t mutate Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!How To Define an Interface in Dart/articles/define-interface-dart/Sat, 04 Jan 2020 13:43:48 -0500/articles/define-interface-dart/Dart defines implicit interfaces. What does this mean? +In your app you&rsquo;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! +Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!How to Build Widgets with an Async Method Call/articles/build-widget-with-async-method-call/Wed, 18 Dec 2019 00:00:00 +0000/articles/build-widget-with-async-method-call/You want to return a widget in a build method&hellip; +But your data comes from an async function! +class MyWidget extends StatelessWidget { @override Widget build(context) { callAsyncFetch().then((data) { return Text(data); // doesn&#39;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? 🤔Why Is My Future/Async Called Multiple Times?/articles/future-async-called-multiple-times/Wed, 18 Dec 2019 00:00:00 +0000/articles/future-async-called-multiple-times/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&lt;String&gt;( 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.The Ultimate Javascript vs Dart Syntax Guide/articles/ultimate-javascript-dart-syntax-guide/Tue, 15 Oct 2019 13:43:48 -0500/articles/ultimate-javascript-dart-syntax-guide/Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart&rsquo;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! +Variables and constants // js var dog1 = &#34;Lucy&#34;; // variable let dog2 = &#34;Milo&#34;; // block scoped variable const maleDogs = [&#34;Max&#34;, &#34;Bella&#34;]; // mutable single-assignment variable maleDogs.Checking Nulls and Null-Aware Operators in Dart/articles/checking-null-aware-operators-dart/Wed, 18 Sep 2019 00:00:00 +0000/articles/checking-null-aware-operators-dart/What is the best practice for checking nulls in Dart? +var value = maybeSomeNumber(); if (value != null) { doSomething(); } That&rsquo;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. +Default operator: ?? In other languages we can use the logical-or shortcut. If maybeSomeNumber() returns null, assign a default value of 2:How to Format a Duration as a HH:MM:SS String/articles/how-to-format-duration/Tue, 10 Sep 2019 23:43:48 -0500/articles/how-to-format-duration/The shortest, most elegant and reliable way to get HH:mm:ss from a Duration is doing: +format(Duration d) =&gt; d.toString().split(&#39;.&#39;).first.padLeft(8, &#34;0&#34;); 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) =&gt; d.How to Upgrade Flutter/articles/upgrade-flutter-sdk/Tue, 27 Aug 2019 12:43:48 -0500/articles/upgrade-flutter-sdk/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 &#39;dev&#39;... # ... And run upgrade again: +flutter upgradeMinimal Flutter Apps to Get Started/articles/minimal-hello-world-flutter-app/Tue, 30 Jul 2019 23:43:48 -0500/articles/minimal-hello-world-flutter-app/Every time I do a flutter create project I get the default &ldquo;counter&rdquo; sample app full of comments. +While it&rsquo;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. +Bare bones app // lib/main.dart import &#39;package:flutter/widgets.dart&#39;; main() =&gt; runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) =&gt; Center( child: Text(&#39;Hello Flutter! \ No newline at end of file diff --git a/articles/intercept-logout-adapter/index.html b/articles/intercept-logout-adapter/index.html new file mode 100644 index 0000000..c6609a3 --- /dev/null +++ b/articles/intercept-logout-adapter/index.html @@ -0,0 +1,21 @@ +Intercept Logout Adapter - Flutter Data

Intercept Logout Adapter

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;
+  }
+}
+
\ No newline at end of file diff --git a/articles/iterator-style-adapter/index.html b/articles/iterator-style-adapter/index.html new file mode 100644 index 0000000..9d910f9 --- /dev/null +++ b/articles/iterator-style-adapter/index.html @@ -0,0 +1,18 @@ +Iterator Style Adapter - Flutter Data

Iterator Style Adapter

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();
+
\ No newline at end of file diff --git a/articles/minimal-hello-world-flutter-app/index.html b/articles/minimal-hello-world-flutter-app/index.html new file mode 100644 index 0000000..1fc99b3 --- /dev/null +++ b/articles/minimal-hello-world-flutter-app/index.html @@ -0,0 +1,87 @@ +Minimal Flutter Apps to Get Started - Flutter Data

Minimal Flutter Apps to Get Started

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.

Bare bones app

// 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:



Material-style minimal app

// 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:



Minimal stateful app

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):



Minimal stateful app (with 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!

\ No newline at end of file diff --git a/articles/nested-resources-adapter/index.html b/articles/nested-resources-adapter/index.html new file mode 100644 index 0000000..d89fdae --- /dev/null +++ b/articles/nested-resources-adapter/index.html @@ -0,0 +1,25 @@ +Nested Resources Adapter - Flutter Data

Nested Resources Adapter

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 });
+
\ No newline at end of file diff --git a/articles/override-base-url/index.html b/articles/override-base-url/index.html new file mode 100644 index 0000000..35416d7 --- /dev/null +++ b/articles/override-base-url/index.html @@ -0,0 +1,17 @@ +Override Base URL Adapter - Flutter Data

Override Base URL Adapter

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';
+}
+
\ No newline at end of file diff --git a/articles/override-findall-adapter/index.html b/articles/override-findall-adapter/index.html new file mode 100644 index 0000000..8e2143d --- /dev/null +++ b/articles/override-findall-adapter/index.html @@ -0,0 +1,22 @@ +Override findAll Adapter - Flutter Data

Override findAll Adapter

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>();
+  }
+}
+
\ No newline at end of file diff --git a/articles/override-findone-url-method/index.html b/articles/override-findone-url-method/index.html new file mode 100644 index 0000000..c5eef8a --- /dev/null +++ b/articles/override-findone-url-method/index.html @@ -0,0 +1,22 @@ +Override findOne URL Adapter - Flutter Data

Override findOne URL Adapter

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;
+  }
+}
+
\ No newline at end of file diff --git a/articles/override-headers-query-parameters/index.html b/articles/override-headers-query-parameters/index.html new file mode 100644 index 0000000..216c455 --- /dev/null +++ b/articles/override-headers-query-parameters/index.html @@ -0,0 +1,26 @@ +Override Default Headers and Query Parameters - Flutter Data

Override Default Headers and Query Parameters

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};
+  }
+}
+
\ No newline at end of file diff --git a/articles/override-http-client-adapter/index.html b/articles/override-http-client-adapter/index.html new file mode 100644 index 0000000..8d1c895 --- /dev/null +++ b/articles/override-http-client-adapter/index.html @@ -0,0 +1,37 @@ +Override HTTP Client Adapter - Flutter Data

Override HTTP Client Adapter

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();
+  }
+}
+
\ No newline at end of file diff --git a/content/articles/ultimate-javascript-dart-syntax-guide/featured-mini.jpg b/articles/ultimate-javascript-dart-syntax-guide/featured-mini.jpg similarity index 100% rename from content/articles/ultimate-javascript-dart-syntax-guide/featured-mini.jpg rename to articles/ultimate-javascript-dart-syntax-guide/featured-mini.jpg diff --git a/content/articles/ultimate-javascript-dart-syntax-guide/featured.jpg b/articles/ultimate-javascript-dart-syntax-guide/featured.jpg similarity index 100% rename from content/articles/ultimate-javascript-dart-syntax-guide/featured.jpg rename to articles/ultimate-javascript-dart-syntax-guide/featured.jpg diff --git a/articles/ultimate-javascript-dart-syntax-guide/index.html b/articles/ultimate-javascript-dart-syntax-guide/index.html new file mode 100644 index 0000000..b0d79a0 --- /dev/null +++ b/articles/ultimate-javascript-dart-syntax-guide/index.html @@ -0,0 +1,486 @@ +The Ultimate Javascript vs Dart Syntax Guide - Flutter Data

The Ultimate Javascript vs Dart Syntax Guide

Photo by ipet photo on Unsplash

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!

Variables and constants

// 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!)

Default assignment

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
+}
+

Destructuring assignment

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.

Falsey vs null

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 💩

Function literals

// 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.

Function defaults

// 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.

Spreading arguments

// 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
+}
+

Safe navigation

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;
+
Interested in Dart’s amazing capabilities to deal with nulls? Read Checking Nulls and Null-Aware Operators in Dart.

Collection literals

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
+}
+

Cascade operator

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.

Array concatenation

// 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 🤔

Accessing properties in objects/maps

// js
+
+var first = { age: 7 };
+console.log(first.age); // 7
+
// dart
+
+main() {
+  var first = { 'age': 7 };
+  print(first['age']);  // 7
+}
+

Imports and exports

// 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;
+

The Great Dane in the Room

Dart is a statically-typed language with strong type inference.

A comparison with Typescript would probably be fairer, but I’ll leave that for next time. 😄

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]
+}
+

Object oriented breeds 🐩

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!

  • We can avoid using new when calling constructors – that is why I used Dog() (vs new Dog())
  • No need to use this to reference fields: it is only used to define constructors
  • Factory and named constructors are a thing
  • Dart supports mixins!
Wanna know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors.

Checking types

We 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('🐶');
+  }
+}
+

Class & prototype extensions

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;
+

Parsing JSON 🐶 style

// 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.

Checking for an empty list is easy in Dart: 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.

Equality to the bone 🦴

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)
+}
+

Doggy privates

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.

Futuristic hounds 🐕

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();
+}
+

Is this really the definitive syntax guide?

Well… maybe 🤪 Pending for a next revision:

  • Enums
  • Annotations
  • Streams & sync/async generators
  • Workers vs isolates
  • and more!

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.

\ No newline at end of file diff --git a/articles/upgrade-flutter-sdk/index.html b/articles/upgrade-flutter-sdk/index.html new file mode 100644 index 0000000..4f840d6 --- /dev/null +++ b/articles/upgrade-flutter-sdk/index.html @@ -0,0 +1,20 @@ +How to Upgrade Flutter - Flutter Data

How to Upgrade Flutter

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
+
\ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000..7aab969 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,9 @@ +Categories - Flutter Data

Categories

\ No newline at end of file diff --git a/categories/index.xml b/categories/index.xml new file mode 100644 index 0000000..ff09308 --- /dev/null +++ b/categories/index.xml @@ -0,0 +1 @@ +Categories on Flutter Data/categories/Recent content in Categories on Flutter DataHugo -- gohugo.ioen-us \ No newline at end of file diff --git a/config/_default/config.toml b/config/_default/config.toml deleted file mode 100644 index f2c5ebf..0000000 --- a/config/_default/config.toml +++ /dev/null @@ -1,20 +0,0 @@ -baseURL = "/" -languageCode = "en-us" -title = "Flutter Data" - -pygmentsUseClassic = false -pygmentsUseClasses = true -pygmentsCodeFences = true - -[markup] - [markup.goldmark] - [markup.goldmark.renderer] - unsafe = true - [markup.tableOfContents] - endLevel = 2 - startLevel = 2 - -[caches] -[caches.getjson] -dir = ":cacheDir/:project" -maxAge = "10s" \ No newline at end of file diff --git a/config/production/config.toml b/config/production/config.toml deleted file mode 100644 index ec661d5..0000000 --- a/config/production/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -baseURL = "/" -publishdir = "public" diff --git a/content/articles/_index.md b/content/articles/_index.md deleted file mode 100644 index b645635..0000000 --- a/content/articles/_index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -type: article ---- - -{{< archive >}} \ No newline at end of file diff --git a/content/articles/build-widget-with-async-method-call.md b/content/articles/build-widget-with-async-method-call.md deleted file mode 100644 index b73445d..0000000 --- a/content/articles/build-widget-with-async-method-call.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -date: "2019-12-18" -draft: false -title: "How to Build Widgets with an Async Method Call" -author: frank06 -versions: "1.12.13" ---- - -You want to return a widget in a `build` method... - -**But your data comes from an async function!** - -```dart -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? 🤔** - -```dart -class MyWidget extends StatelessWidget { - @override - Future 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.) - -## 🛠 How do we fix this with Flutter best practices? - -Meet `FutureBuilder`: - -```dart -class MyWidget extends StatelessWidget { - @override - Widget build(context) { - return FutureBuilder( - future: callAsyncFetch(), - builder: (context, AsyncSnapshot 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` is simply a representation of that data/error state. This is actually a useful API! - -If we get a new snapshot with: - -- 📭 **no data**... we show a progress indicator -- ✅ **data from our future**... we use it to feed any widgets for display! -- ❌ **error from our future**... we show an appropriate message - -{{< notice >}} -Do you think the answer to this problem is a `StatefulWidget`? Yes, it's a possible solution but not an ideal one. Keep on reading and we'll see why. -{{< /notice >}} - -Click _Run_ and see it for yourself! - -{{< dartpad f85195d247d9854446a4a736d67debf2 70 500 >}} - -It will show a circular progress indicator while the future resolves (about 2 seconds) and then display data. **Problem solved!** - -## 🎩 Under the hood: FutureBuilder - -`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) - -```dart {hl_lines=[2 6 7 21 "31-35"]} -// FutureBuilder *is* a stateful widget -class FutureBuilder extends StatefulWidget { - - // it takes in a `future` and a `builder` - const FutureBuilder({ - this.future, - this.builder - }); - - final Future future; - - // the AsyncWidgetBuilder type is a function(BuildContext, AsyncSnapshot) which returns Widget - final AsyncWidgetBuilder builder; - - @override - State> createState() => _FutureBuilderState(); -} - -class _FutureBuilderState extends State> { - // keeps state in a local variable (so far there's no data) - AsyncSnapshot _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((T data) { - setState(() { _snapshot = AsyncSnapshot(data); }); - }, onError: (Object error) { - setState(() { _snapshot = AsyncSnapshot(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 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](https://github.com/flutter/flutter/blob/27321ebbad/packages/flutter/lib/src/widgets/async.dart#L566). - -Before wrapping up... 🎁 - -From the [docs](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html) - -> 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. - -```dart {hl_lines=[3]} -Widget build(context) { - return FutureBuilder( - 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?](/articles/future-async-called-multiple-times). - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/checking-null-aware-operators-dart.md b/content/articles/checking-null-aware-operators-dart.md deleted file mode 100644 index e6afcff..0000000 --- a/content/articles/checking-null-aware-operators-dart.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -date: "2019-09-18" -draft: false -title: "Checking Nulls and Null-Aware Operators in Dart" -author: frank06 -versions: "1.9.1" -tags: ["dart"] ---- - -What is the best practice for checking nulls in Dart? - -```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. - -## Default operator: `??` - -In other languages we can use the logical-or shortcut. If `maybeSomeNumber()` returns null, assign a default value of `2`: - -```ruby -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: - -```dart -var value = maybeSomeNumber() ?? 2; -``` - -Similarly, if we wanted to ensure a `value` argument was not-null we'd do: - -```dart -value = value ?? 2; -``` - -But there's an even simpler way. - -## Fallback assignment operator: `??=` - -```dart -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](/articles/deconstructing-dart-constructors) using this operator: - -```dart {hl_lines=[9]} -class Robot { - final double height; - - static final _cache = {}; - - Robot._(this.height); - - factory Robot(height) { - return _cache[height] ??= Robot._(height); - } -} -``` - -More generally, `??=` is useful when defining computed properties: - -```dart -get value => _value ??= _computeValue(); -``` - -## Safe navigation operator: `?.` - -Otherwise known as the Elvis operator. I first saw this in the Groovy language. - -```groovy -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! - -```dart -final value = person?.address?.street?.value; -``` - -If `address` was a method instead of a getter, it would work just the same: - -```dart -final value = person?.getAddress()?.street?.value; -``` - -![groovy](https://media.giphy.com/media/1Bg8omsmc0ZXEsc67W/giphy.gif) - -## Optional spread operator: `...?` - -Lastly, this one only inserts a list into another only if it's not-null. - -```dart -List additionalZipCodes = [33110, 33121, 33320]; -List optionalZipCodes = fetchZipCodes(); -final zips = [10001, ...additionalZipCodes, ...?optionalZipCodes]; -print(zips); /* [10001, 33110, 33121, 33320] if fetchZipCodes() returns null */ -``` - -{{< contact >}} - -## Non-nullable types - -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](https://github.com/dart-lang/language/issues/110)). - -For a type to allow null values, a special syntax will be required. - -The following will throw an error: - -```dart -int value = someNumber(); -value = null; -``` - -And fixed by specifying the `int?` type: - -```dart -int? value = someNumber(); -value = null; -``` diff --git a/content/articles/configure-get-it.md b/content/articles/configure-get-it.md deleted file mode 100644 index 3516fd0..0000000 --- a/content/articles/configure-get-it.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: "Configure Flutter Data to Work with GetIt" -date: 2021-12-05T23:12:05-03:00 ---- - -This is an example of how we can configure Flutter Data to use [GetIt](https://pub.dev/packages/get_it) as a dependency injection framework. - -**Important**: Make sure to replicate `ProxyProvider`s for other models than `Todo`. - -```dart -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>(); - 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 _(ProviderBase provider) => null as T; - -extension GetItFlutterDataX on GetIt { - void registerRepositories( - {FutureFn? baseDirFn, - List? 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()) { - return; - } - - i.registerSingletonAsync(() async { - final init = _container.read( - repositoryInitializerProvider(remote: remote, verbose: remote) - .future); - internalLocatorFn = - >(Provider> provider, _) => - _container.read(provider); - return init; - }); - i.registerSingletonWithDependencies>( - () => _container.read(todosRepositoryProvider), - dependsOn: [RepositoryInitializer]); - } -} -``` - -See this in action with the [Flutter Data setup app](https://github.com/flutterdata/flutter_data_setup_app)! - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/configure-provider.md b/content/articles/configure-provider.md deleted file mode 100644 index b828384..0000000 --- a/content/articles/configure-provider.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: "Configure Flutter Data to Work with Provider" -date: 2021-12-05T23:12:05-03:00 ---- - -This is an example of how we can configure Flutter Data to use [Provider](https://pub.dev/packages/provider) as a dependency injection framework. - -**Important**: Make sure to replicate `ProxyProvider`s for other models than `Todo`. - -```dart -class ProviderTodoApp extends StatelessWidget { - @override - Widget build(context) { - return MultiProvider( - providers: [ - ...providers(clear: true), - ProxyProvider?, 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() == null) { - // optionally also check - // context.watch.repository != null - return const CircularProgressIndicator(); - } - final repository = context.watch?>(); - 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 providers( - {FutureFn? baseDirFn, - List? encryptionKey, - bool? clear, - bool? remote, - bool? verbose}) { - return [ - Provider( - create: (_) => ProviderContainer( - overrides: [ - configureRepositoryLocalStorage( - baseDirFn: baseDirFn, encryptionKey: encryptionKey, clear: clear), - ], - ), - ), - FutureProvider( - initialData: null, - create: (context) async { - return await Provider.of(context, listen: false) - .read( - repositoryInitializerProvider(remote: remote, verbose: verbose) - .future, - ); - }, - ), - ProxyProvider?>( - lazy: false, - update: (context, i, __) => i == null - ? null - : Provider.of(context, listen: false) - .read(todosRepositoryProvider), - dispose: (_, r) => r?.dispose(), - ), - ]; -} -``` - -See this in action with the [Flutter Data setup app](https://github.com/flutterdata/flutter_data_setup_app)! - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/custom-deserialization-adapter.md b/content/articles/custom-deserialization-adapter.md deleted file mode 100644 index e0dace6..0000000 --- a/content/articles/custom-deserialization-adapter.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "Custom Deserialization Adapter" -date: 2021-12-09T23:15:44-03:00 ---- - -Example: - -```dart -mixin AuthAdapter on RemoteAdapter { - Future 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: - -```dart -final token = await userRepository.authAdapter.login('e@mail, p*ssword'); -``` - -Also see [JSONAPIAdapter](https://github.com/flutterdata/flutter_data_json_api_adapter/) for inspiration. - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/dart-final-const-difference.md b/content/articles/dart-final-const-difference.md deleted file mode 100644 index d219f00..0000000 --- a/content/articles/dart-final-const-difference.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -date: "2020-01-04T13:43:48-05:00" -draft: false -title: "Final vs const in Dart" -author: "frank06" ---- - -What's the difference between final and const in Dart? - -Easy! - -**Final means single-assignment.** - -**Const means immutable.** - -Let's see an example: - -```dart -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 -``` - -{{< notice >}} -Want to know EVERYTHING about Dart constructors? Check out [Deconstructing Dart Constructors](/articles/deconstructing-dart-constructors)! -{{< /notice >}} - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/dart-getter-cache-computed-properties.md b/content/articles/dart-getter-cache-computed-properties.md deleted file mode 100644 index 75b7514..0000000 --- a/content/articles/dart-getter-cache-computed-properties.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -date: "2020-01-04T13:43:48-05:00" -draft: false -title: "Dart Getter Shorthand to Cache Computed Properties" -author: "frank06" ---- - -An elegant Dart getter shorthand used to cache computed properties: - -```dart -T get foo => _foo ??= _computeFoo(); - -// which depends on having -T _foo; -T _computeFoo() => /** ... **/; -``` - -It makes use of the fallback assignment operator `??=`. - -{{< notice >}} -Check out [Null-Aware Operators in Dart](/articles/checking-null-aware-operators-dart) for a complete guide on dealing with `null`s in Dart! -{{< /notice >}} diff --git a/content/articles/deconstructing-dart-constructors/index.md b/content/articles/deconstructing-dart-constructors/index.md deleted file mode 100644 index 47b99bd..0000000 --- a/content/articles/deconstructing-dart-constructors/index.md +++ /dev/null @@ -1,783 +0,0 @@ ---- -date: "2020-02-12T13:43:48-05:00" -draft: false -title: "Deconstructing Dart Constructors" -author: frank06 -versions: "1.12.13" -tags: ["dart"] ---- - -Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories... - -Read this post and you will become an expert! - -![Photo by Arseny Togulev on Unsplash](featured.jpg) - -When we want an **instance** of a certain class we call a **constructor**, right? - -```dart -var robot = new Robot(); -``` - -In Dart 2 we can leave out the `new`: - -```dart -var robot = Robot(); -``` - -A constructor is used to ensure instances are created in a coherent state. This is the definition in a class: - -```dart -class Robot { - Robot(); -} -``` - -This constructor has no arguments so we can leave it out and write: - -```dart -class Robot { -} -``` - -The default constructor is implicitly defined. - -{{< notice >}} -Did you know you can try out Dart and Flutter code in [DartPad](https://dartpad.dartlang.org/)? -{{< /notice >}} - -## Initializing... - -Most times we need to configure our instances. For example, pass in the height of a robot: - -```dart -var r = Robot(5); -``` - -`r` is now a 5-feet tall `Robot`. - -To write that constructor we include the `height` field after the colon `:` - -```dart -class Robot { - double height; - Robot(height) : this.height = height; -} -``` - -or even - -```dart -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: - -```dart -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): - -```dart -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: - -```dart -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`: - -```dart -class Robot { - final double height; - Robot(height) : this.height = height, assert(height > 4.2); -} -``` - -## Accessors and mutators - -Back to our earlier robot definition: - -```dart -class Robot { - double height; - Robot(this.height); -} - -void main() { - var r = Robot(5); - print(r.height); // 5 -} -``` - -Let's make it taller: - -```dart -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). - -```dart -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**: - -```dart -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](https://en.wikipedia.org/wiki/Uniform_access_principle). - -We can simplify our getter by using two shortcuts: single expression syntax (fat arrow) and implicit `this`: - -```dart -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: - -```dart -class Robot { - double height; - Robot(this.height); -} -``` - -is equivalent to: - -```dart -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: - -```dart -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; -} -``` - -## Constructor bodies - -![](https://media.giphy.com/media/iIAYEKtLy0yG7TacbC/giphy.gif) - -If a setter needs to be called, we'll have to do that in a **constructor body**: - -```dart -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! - -```dart -class Robot { - double height; - Robot(this.height) { - return this; // ERROR: Constructors can't return values - } -} -``` - -## Final fields - -**Final** fields are fields that can only be assigned once. - -```dart -final r = Robot(5); -r = Robot(7); /* ERROR */ -``` - -Inside our class, we won't be able to use the setter: - -```dart -class Robot { - final double _height; - Robot(this._height); - - get height => _height; - set height(value) => _height = value; // ERROR -} -``` - -{{< notice >}} -Just like with `var`, we can use `final` before any type definition: - -```dart -var r; -var Robot r; - -final r; -final Robot r; -``` - -{{< /notice >}} - -The following won't work because `height`, being `final`, **must** be initialized. And initialization happens before the constructor body is run: - -```dart -class Robot { - final double height; - - Robot(double height) { - this.height = height; // ERROR: The final variable 'height' must be initialized - } -} -``` - -Let's fix it: - -```dart -class Robot { - final double height; - Robot(this.height); -} -``` - -## Default values - -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**: - -```dart -class Robot { - final double height; - Robot([this.height = 5]); -} -``` - -So we can just call: - -```dart -void main() { - var r = Robot(); - print(r.height); // 5 - - var r2d2 = Robot(3.576); - print(r2d2.height); // 3.576 -} -``` - -![](https://media.giphy.com/media/l1KsGK43cTDF4VAsg/giphy.gif) - -## Immutable robots - -Our robots clearly have more attributes than a height. Let's add some more! - -```dart -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: - -```dart -class Robot { - final double height; - final double weight; - final List 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**: - -```dart -class Robot { - final double height; - final double weight; - final List 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: - -```dart -import 'dart:math'; - -class Robot { - final double height; - final double weight; - final List 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: - -```dart -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. - -{{< contact >}} - -## Optional arguments always last! - -If we wanted the `weight` argument to be **optional** we'd have to declare it at the end: - -```dart -class Robot { - final double height; - final double weight; - final List names; - - const Robot(this.height, this.names, [this.weight = 170]); -} - -void main() { - final r = Robot(5, ["Walter"]); - print(r.weight); // 170 -} -``` - -## Naming things - -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: - -```dart -class Robot { - final double height; - final double weight; - final List 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`: - -```dart -class Robot { - final double height; - final double weight; - final List names; - - Robot({ this.height, @required this.weight, this.names }); -} -``` - -(or use `assert(weight != null)` in the initializer for a runtime check!) - -## Naming things with defaults - -```dart -class Robot { - final double height; - final double weight; - final List 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](/articles/checking-null-aware-operators-dart) in the assignment to provide any constant or static computation: - -```dart -class Robot { - final double height; - final double weight; - final List 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? - -```dart -class Robot { - final double _height; - final double _weight; - final List _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: - -```dart -class Robot { - final double _height; - final double _weight; - final List _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 -} -``` - -## Mixing it up - -Both positional and named argument styles can be used together: - -```dart -class Robot { - final double _height; - final double _weight; - final List _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 -} -``` - -## Named constructors - -Not only can arguments be named. We can give names to any number of constructors: - -```dart -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: - -```dart -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! - -## Keeping it private - -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: - -```dart -class Robot { - Robot._(); -} -``` - -Applying this knowledge to our previous example: - -```dart -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. - -## A robot factory - -![](https://media.giphy.com/media/12qq4Em3MVuwJW/giphy.gif) - -We said constructors were not allowed to return. Guess what? - -**Factory constructors** can! - -```dart -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: - -- caching: conditionally returning existing objects (they might be expensive to create) -- subclasses: returning other instances such as subclasses - -They work with both normal and named constructors! - -Here's our robot warehouse, that only supplies one robot per height: - -```dart -class Robot { - final double height; - - static final _cache = {}; - - 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: - -```dart -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 - -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: - -```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`.) diff --git a/content/articles/define-interface-dart.md b/content/articles/define-interface-dart.md deleted file mode 100644 index a73c1c1..0000000 --- a/content/articles/define-interface-dart.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -date: "2020-01-04T13:43:48-05:00" -draft: false -title: "How To Define an Interface in Dart" -author: "frank06" ---- - -Dart defines _implicit_ interfaces. What does this mean? - -In your app you'd have: - -```dart -class Session { - authenticate() { // impl } -} -``` - -or - -```dart -abstract class Session { - authenticate(); -} -``` - -And for example in tests: - -```dart -class MockSession implements Session { - authenticate() { // mock impl } -} -``` - -No need to define a separate interface, just use regular or abstract classes! - -{{< notice >}} -Want to know EVERYTHING about Dart constructors? Check out [Deconstructing Dart Constructors](/articles/deconstructing-dart-constructors)! -{{< /notice >}} - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/future-async-called-multiple-times.md b/content/articles/future-async-called-multiple-times.md deleted file mode 100644 index 91e4ed6..0000000 --- a/content/articles/future-async-called-multiple-times.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -date: "2019-12-18" -draft: false -title: "Why Is My Future/Async Called Multiple Times?" -author: frank06 -versions: "1.12.13" ---- - -Why is `FutureBuilder` firing **multiple** times? My future should be called just once! - -It appears that this `build` method is rebuilding unnecessarily: - -```dart -@override -Widget build(context) { - return FutureBuilder( - 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! - -## Understanding the problem - -Imagine the `FutureBuilder`'s parent is a `ListView`. This is what happens: - -- 🧻 User scrolls list -- 🔥 `build` fires many times per second to update the screen -- ⁑ `callAsyncFetch()` gets invoked once per `build` returning new `Future`s every time -- = `didUpdateWidget` in the `FutureBuilder` compares old and new `Future`s; if different it calls the `builder` again -- 😩 Since instances are always new (always different to the old one) the `builder` 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)_ - -## A quick fix 🔧 - -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: - -```dart -class MyWidget extends StatefulWidget { - @override - _MyWidgetState createState() => _MyWidgetState(); -} - -class _MyWidgetState extends State { - Future _future; - - @override - void initState() { - _future = callAsyncFetch(); - super.initState(); - } - - @override - Widget build(context) { - return FutureBuilder( - future: _future, - builder: (context, snapshot) { - // ... - } - ); - } -} -``` - -We're caching a value (in other words, [memoizing](https://en.wikipedia.org/wiki/Memoization)) such that the `build` method can now call our code a million times without problems. - -## Live example - -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: - -- **Top**: a `StatelessWidget` containing a `FutureBuilder`. It's fed a new `Future` that resolves to the current date in seconds -- **Bottom**: a `StatefulWidget` 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 `FutureBuilder` - -Hit _Run_ and see the difference (wait at least 3 seconds). Rebuilds are also logged to the console. - -{{< dartpad 8fff93d3dafe16a98ce203e3e3a8295d 65 500 >}} - -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. - -## Cleaner ways 🛀 - -Are you using [Provider](https://pub.dev/packages/provider) by any chance? You can simply use a `FutureProvider` instead of the `StatefulWidget` above: - -```dart -class MyWidget extends StatelessWidget { - // Future callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi"); - @override - Widget build(BuildContext context) { - // print('building widget'); - return FutureProvider( - create: (_) { - // print('calling future'); - return callAsyncFetch(); - }, - child: Consumer( - builder: (_, value, __) => Text(value ?? 'Loading...'), - ), - ); - } -} -``` - -Much nicer, if you ask me. - -{{< notice >}} -**Tip!** It's a fully functional example. Comment out those lines and try it out in your own editor! -{{< /notice >}} - -Another option is using the fantastic [Flutter Hooks](https://pub.dev/packages/flutter_hooks) library with the `useMemoized` hook for the memoization (caching): - -```dart -class MyWidget extends HookWidget { - @override - Widget build(BuildContext context) { - final future = useMemoized(() { - // Future callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi"); - callAsyncFetch(); // or your own async function - }); - return FutureBuilder( - future: future, - builder: (context, snapshot) { - return Text(snapshot.hasData ? snapshot.data : 'Loading...'); - } - ); - } -} -``` - -## Takeaway - -Your `build` methods should always be _pure_, that is, **never** have side-effects (like updating state, calling async functions). - -Remember that `builder`s are ultimately called by `build`! - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/how-to-format-duration.md b/content/articles/how-to-format-duration.md deleted file mode 100644 index fb6c78b..0000000 --- a/content/articles/how-to-format-duration.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -date: "2019-09-10T23:43:48-05:00" -draft: false -title: "How to Format a Duration as a HH:MM:SS String" -versions: "1.9.1" ---- - -The shortest, most elegant and reliable way to get `HH:mm:ss` from a `Duration` is doing: - -```dart -format(Duration d) => d.toString().split('.').first.padLeft(8, "0"); -``` - -Example usage: - -```dart -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: - -```dart -format(Duration d) => d.toString().substring(2, 7); -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/how-to-reinitialize-flutter-data.md b/content/articles/how-to-reinitialize-flutter-data.md deleted file mode 100644 index f241244..0000000 --- a/content/articles/how-to-reinitialize-flutter-data.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "How to Reinitialize Flutter Data" -date: 2021-12-18T17:08:28-03:00 ---- - -By calling `repositoryInitializerProvider` again with Riverpod's `refresh` we can reinitialize Flutter Data. - -```dart {hl_lines=[5 6]} -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(), - ), - ), - ), - ), - ); - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/intercept-logout-adapter.md b/content/articles/intercept-logout-adapter.md deleted file mode 100644 index 76f63bf..0000000 --- a/content/articles/intercept-logout-adapter.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: "Intercept Logout Adapter" -date: 2021-12-09T23:15:11-03:00 ---- - -The global `onError` handler will call `logout` if certain conditions are met: - -```dart -mixin BaseAdapter> on RemoteAdapter { - @override - FutureOr onError(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; - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/iterator-style-adapter.md b/content/articles/iterator-style-adapter.md deleted file mode 100644 index 5a2f097..0000000 --- a/content/articles/iterator-style-adapter.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: "Iterator Style Adapter" -date: 2021-12-09T23:13:36-03:00 ---- - -```dart -mixin AppointmentAdapter on RemoteAdapter { - Future 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. - -```dart -final nextAppointment = await appointmentRepository.appointmentAdapter.fetchNext(); -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/minimal-hello-world-flutter-app.md b/content/articles/minimal-hello-world-flutter-app.md deleted file mode 100644 index 917760a..0000000 --- a/content/articles/minimal-hello-world-flutter-app.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -date: "2019-07-30T23:43:48-05:00" -draft: false -title: "Minimal Flutter Apps to Get Started" -author: frank06 -versions: "1.12.13" ---- - -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`. - -### Bare bones app - -```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: - -{{< dartpad eddb6cdb56662bf037b1501b60c300da 70 500 >}} - -### *Material-style* minimal app - -```dart -// 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: - -{{< dartpad a8e34f01a4cdad18a8de100a865e30c1 70 500 >}} - - -### Minimal *stateful* app - -```dart -import 'package:flutter/material.dart'; - -main() => runApp(MinimalStatefulApp()); - -class MinimalStatefulApp extends StatefulWidget { - @override - _MinimalState createState() => _MinimalState(); -} - -class _MinimalState extends State { - - 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): - -{{< dartpad 017aa3e4b7b0ea403de1942bb5fd0472 70 500 >}} - - -### Minimal *stateful* app (with `flutter_hooks`) - -```dart -// 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](https://pub.dev/packages/flutter_hooks) which remove the boilerplate of a classic `StatefulWidget`. Make sure you add the `flutter_hooks` dependency to `pubspec.yaml`! - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/nested-resources-adapter.md b/content/articles/nested-resources-adapter.md deleted file mode 100644 index 5681075..0000000 --- a/content/articles/nested-resources-adapter.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "Nested Resources Adapter" -date: 2021-12-09T23:17:30-03:00 -draft: false ---- - -Here's how you could access nested resources such as: `/posts/1/comments` - -```dart -mixin NestedURLAdapter on RemoteAdapter { - // ... - @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: - -```dart -final comments = await commentRepository.findAll(params: {'postId': post.id }); -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/override-base-url.md b/content/articles/override-base-url.md deleted file mode 100644 index aa9a0ba..0000000 --- a/content/articles/override-base-url.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Override Base URL Adapter" -date: 2021-12-03T18:45:45-03:00 ---- - -Flutter Data is extended via [adapters](/docs/adapters). - -```dart -mixin UserURLAdapter on RemoteAdapter { - @override - String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo'; -} -``` - -Need to apply the adapter to all your models? Make it generic: - -```dart -mixin UserURLAdapter> on RemoteAdapter { - @override - String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo'; -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/override-findall-adapter.md b/content/articles/override-findall-adapter.md deleted file mode 100644 index f65304e..0000000 --- a/content/articles/override-findall-adapter.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Override findAll Adapter" -date: 2021-12-09T23:14:28-03:00 ---- - -In this example we completely override `findAll` to return random models: - -```dart -mixin FindAllAdapter> on RemoteAdapter { - @override - Future> findAll({ - bool? remote, - Map? params, - Map? headers, - bool? syncLocal, - OnDataError>? onError, - }) async { - // could use: super.findAll(); - return _generateRandomModels(); - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/override-findone-url-method.md b/content/articles/override-findone-url-method.md deleted file mode 100644 index 6c6009c..0000000 --- a/content/articles/override-findone-url-method.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Override findOne URL Adapter" -date: 2021-12-09T23:14:28-03:00 ---- - -In this example we override URLs to hit finder endpoints with snake case, and for `save` to always use `HTTP PUT`: - -```dart -mixin URLAdapter> on RemoteAdapter { - @override - String urlForFindAll(Map params) => type.snakeCase; - - @override - String urlForFindOne(id, Map params) => - '${type.snakeCase}/$id'; - - @override - DataRequestMethod methodForSave(id, Map params) { - return DataRequestMethod.PUT; - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/override-headers-query-parameters.md b/content/articles/override-headers-query-parameters.md deleted file mode 100644 index 31dfb67..0000000 --- a/content/articles/override-headers-query-parameters.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "Override Default Headers and Query Parameters" -date: 2021-12-09T23:07:40-03:00 ---- - -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: - -```dart -mixin BaseAdapter> on RemoteAdapter { - final _localStorageService = read(localStorageProvider); - - @override - String get baseUrl => "http://my.remote.url:8080/"; - - @override - FutureOr> get defaultHeaders async { - final token = _localStorageService.getToken(); - return await super.defaultHeaders & {'Authorization': token}; - } - - @override - FutureOr> get defaultParams async { - return await super.defaultParams & {'v': 1}; - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/override-http-client-adapter.md b/content/articles/override-http-client-adapter.md deleted file mode 100644 index cb0203a..0000000 --- a/content/articles/override-http-client-adapter.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "Override HTTP Client Adapter" -date: 2021-12-09T23:10:10-03:00 ---- - -An example on how to override and use a more advanced HTTP client. - -Here the `connectionTimeout` is increased, and an HTTP proxy enabled. - -```dart -mixin HttpProxyAdapter> on RemoteAdapter { - 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 dispose() async { - _ioClient?.close(); - _ioClient = null; - _httpClient = null; - super.dispose(); - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/articles/ultimate-javascript-dart-syntax-guide/index.md b/content/articles/ultimate-javascript-dart-syntax-guide/index.md deleted file mode 100644 index 15a8ead..0000000 --- a/content/articles/ultimate-javascript-dart-syntax-guide/index.md +++ /dev/null @@ -1,875 +0,0 @@ ---- -date: "2019-10-15T13:43:48-05:00" -draft: false -title: "The Ultimate Javascript vs Dart Syntax Guide" -author: frank06 -versions: "1.9.1" -tags: ["dart", "javascript", "es6"] ---- - -![Photo by ipet photo on Unsplash](featured.jpg) - -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!** - -## Variables and constants - -```js -// 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 -// 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!) - -## Default assignment - -Let's set a default value of 1 if `bones` is _falsey_ (in Javascript) or `null` (in Dart). - -```js -// js - -var bones; -bones = bones || 1; -console.log(bones); // 1 -``` - -```dart -// dart - -main() { - var bones; - bones ??= 1; // OR: bones = bones ?? 1 - print(bones); // 1 -} -``` - -## Destructuring assignment - -This is a great Javascript-only feature. - -```js -// js - -var [dog, owner] = ["Max", "Frank"]; -console.log(dog); // Max -[owner, dog] = [dog, owner]; -console.log(dog); // Frank -``` - -**Not** possible in Dart [yet](https://github.com/dart-lang/language/issues/207). - -## Falsey vs null - -Let's go ahead and have a look at _falsey_ values that only exist in Javascript. - -```js -// 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 -// 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 💩 - -## Function literals - -```js -// js - -function bark() { - return "WOOF"; -} - -var bday = (age) => age + 1; -``` - -```dart -// 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. - -## Function defaults - -```js -// js - -var greet = (name = "Milo") => `Woof! My name is ${name}`; -console.log(greet()); // Woof! My name is Milo -``` - -```dart -// 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. - -## Spreading arguments - -```js -// 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 -// dart - -main() { - final sum = (List meals) => meals.reduce((sum, next) => sum + next); - print(sum([1, 2, 3])); // 6 -} -``` - -## Safe navigation - -`name` should be returned unless `address` or `street` are `null`, in that case the whole expression should return `null`. - -```js -// js -var name = - person.address || person.address.street || person.address.street.name; -``` - -In Dart we have the safe navigation operator: - -```dart -// dart -var name = address?.street?.name; -``` - -{{< notice >}} -Interested in Dart's amazing capabilities to deal with nulls? Read [Checking Nulls and Null-Aware Operators in Dart](/articles/checking-null-aware-operators-dart). -{{< /notice >}} - -## Collection literals - -An `Array` in Javascript is a `List` in Dart. An `Object` in Javascript is a `Map` in Dart. - -```js -// 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 -// 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 -} -``` - -## Cascade operator - -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 -// 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 -// 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. - -![](https://media.giphy.com/media/JOUbijCxtRDdS/giphy.gif) - -## Array concatenation - -```js -// 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 -// 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 -// js - -console.log([1, 2, 3, ...[4, 5], ...[6, 7]]); // [1, 2, 3, 4, 5, 6, 7] -``` - -```dart -// dart - -main() { - print([1, 2, 3, ...[4, 5], ...[6, 7]]); // [1, 2, 3, 4, 5, 6, 7] -} -``` - -Same same. Also for objects/maps: - -```js -// 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 -// 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 -// 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 -// 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: - -```js -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: - -```dart -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": - -```dart -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 🤔 - -## Accessing properties in objects/maps - -```js -// js - -var first = { age: 7 }; -console.log(first.age); // 7 -``` - -```dart -// dart - -main() { - var first = { 'age': 7 }; - print(first['age']); // 7 -} -``` - -{{< contact >}} - -## Imports and exports - -```js -// 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 -// dart - -// module file -final dog = "Luna"; - -clean(dog) => _doCleaning(dog); - -// import -import 'module.dart'; - -// alternatively -import 'module.dart' as module; -``` - -## The Great Dane in the Room - -![](https://media.giphy.com/media/tNuoOEz7cigvK/giphy.gif) - -Dart is a **statically-typed language** with strong type inference. - -{{< notice >}} -A comparison with [Typescript](http://www.typescriptlang.org/) would probably be fairer, but I'll leave that for next time. 😄 -{{< /notice >}} - -As we've seen so far, we almost never need to declare type annotations: - -```dart -// dart - -main() { - var age = 1; - var pets = ["Cooper", "Luna"]; - print(age.runtimeType); // int - print(pets.runtimeType); // Array -} -``` - -This means we leverage the power of types without stuffing our code with declarations! But of course we may: - -```dart -// dart - -main() { - int age = 5; - List pets = ["Cooper", "Luna"]; - var pets2 = ["Cooper", "Luna"]; - List pets3 = ["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 -// 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: - -```dart -walk(dynamic distance) { - print('Walking ${distance.length} miles'); -} -``` - -In short: **argument types are very important!** - -This is recommended, idiomatic Dart: - -```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`. - -```dart -main() { - dynamic dog = "Charlie"; - dog = ["char", "lie"]; // compiler NOT type checking! - print(dog); // [char, lie] -} -``` - -## Object oriented breeds 🐩 - -Classes are relatively new in Javascript: - -```js -// 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 -// 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! - -- We can avoid using `new` when calling constructors – that is why I used `Dog()` (vs `new Dog()`) -- No need to use `this` to reference fields: it is only used to define constructors -- Factory and named constructors are a thing -- Dart supports mixins! - -{{< notice >}} -Wanna know EVERYTHING about Dart constructors? Check out [Deconstructing Dart Constructors](/articles/deconstructing-dart-constructors). -{{< /notice >}} - -## Checking types - -We use `instanceof` in Javascript: - -```js -// js - -class Dog extends Animal { - // ... -} - -var animal = getAnimal(); -if (animal instanceof Dog) { - console.log("🐶"); -} -``` - -And `is` in Dart: - -```dart -// dart - -class Dog extends Animal { - // ... -} - -main() { - var animal = getAnimal(); - if (animal is Dog) { - console.log('🐶'); - } -} -``` - -## Class & prototype extensions - -These are methods that extend existing types. In Javascript a function can be added to a prototype: - -```js -// 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 -// 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](https://github.com/jogboms/time.dart) ⏰. Now we can do stuff like: - -```dart -Duration timeOfSleep = 7.hours + 32.minutes + 8.seconds; -DateTime medicated = 5.minutes.ago; -``` - -## Parsing JSON 🐶 style - -```js -// 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`. - -{{< notice >}} -Checking for an empty list is easy in Dart: `list.isEmpty`, in Javascript we must use the length for this: `!array.length`. -{{< /notice >}} - -In Dart: - -```dart -// dart - -import 'dart:convert'; - -main() { - var dog = jsonDecode('{ "name": "Willy", "medications": { "doxycycline": true } }'); - print(dog.runtimeType); // _InternalLinkedHashMap - print(dog['medications'].lnegth); // NoSuchMethodError: Class '_InternalLinkedHashMap' 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 ``. - -When we misspell `length` on a `dynamic` variable there is no type checking, so the error we get is at runtime. - -## Equality to the bone 🦴 - -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 -// 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 -// 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) -} -``` - -![](https://media.giphy.com/media/pXHeBVPUTiMO4/giphy.gif) - -## Doggy privates - -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 -// 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 -// 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. - -## Futuristic hounds 🐕 - -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 -// 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 -// dart - -Future 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(); -} -``` - -## Is this really the definitive syntax guide? - -Well... maybe 🤪 Pending for a next revision: - -- Enums -- Annotations -- Streams & sync/async generators -- Workers vs isolates -- and more! - -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. diff --git a/content/articles/upgrade-flutter-sdk.md b/content/articles/upgrade-flutter-sdk.md deleted file mode 100644 index 3bea851..0000000 --- a/content/articles/upgrade-flutter-sdk.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -date: "2019-08-27T12:43:48-05:00" -draft: false -title: "How to Upgrade Flutter" -author: frank06 -versions: "1.7.8" -tags: ['pub', 'vscode'] ---- - -Type in your terminal: - -```bash -flutter upgrade -``` - -This will update Flutter to the latest version in the current channel. Most likely you have it set in `stable`. - -```bash -flutter channel -# Flutter channels: -# beta -# dev -# master -# * stable -``` - -Do you want to live in the cutting edge? Switching channels is easy: - -```bash -flutter channel dev -# Switching to flutter channel 'dev'... -# ... -``` - -And run upgrade again: - -```bash -flutter upgrade -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/adapters.md b/content/docs/adapters.md deleted file mode 100644 index c3ca134..0000000 --- a/content/docs/adapters.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -title: "Adapters" -weight: 20 -menu: docs ---- - -Flutter Data's building blocks are called adapters, making it extremely customizable and composable. - -Adapters are essentially Dart mixins applied on `RemoteAdapter`. - -## Overriding basic behavior - -Several pieces of information are required, for example, to construct a remote `findAll` call on a `Repository`. The framework takes a sensible guess and makes that `GET /tasks` by default. - -Still, a base URL is necessary and the endpoint parts should be overridable. - -The way we use these adapters is by declaring them on our `@DataRepository` annotation in the corresponding model. For example: - -```dart {hl_lines=[1 2 3 4 7]} -mixin JSONServerTaskAdapter on RemoteAdapter { - @override - String get baseUrl => 'https://myapi.com/v1/'; -} - -@JsonSerializable() -@DataRepository([JSONServerTaskAdapter]) -class Task extends DataModel { - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); -} -``` - -What if the endpoint actually is at `https://myapi.com/v1/todos/all`? - -```dart -mixin JSONServerTaskAdapter on RemoteAdapter { - @override - String get baseUrl => 'https://myapi.com/v1/'; - - @override - String urlForFindAll(Map params) => 'todos/all'; -} -``` - -Here's a list of overridable members: - -
- -| | | -|--------------|-----------| -| `type` | defaults to a camel-cased, pluralized class name (`User` => `users`) | -| `baseUrl` | must be implemented or it will throw an error | -| `urlForFindAll` | defaults to `type` | -| `methodForFindAll` | defaults to `DataRequestMethod.GET` | -| `urlForFindOne` | defaults to `${type}/${id}` | -| `methodForFindOne` | defaults to `DataRequestMethod.GET` | -| `urlForSave` | defaults to `${type}/${id}` if updating | -| `methodForSave` | defaults to `DataRequestMethod.PATCH` if updating | -| `urlForDelete` | defaults to `${type}/${id}` | -| `methodForDelete` | defaults to `DataRequestMethod.DELETE`| -| `defaultParams` | defaults to `{}` | -| `defaultHeaders` | defaults to `{'Content-Type': 'application/json'}` | -| `shouldLoadRemoteAll` | fine-grained control over the `remote` param on `findAll` | -| `shouldLoadRemoteOne` | fine-grained control over the `remote` param on `findOne` | -| `serialize` | can customize serialization (like the [JSON API Adapter](https://github.com/flutterdata/flutter_data_json_api_adapter/) does) | -| `deserialize` | can customize deserialization (like the [JSON API Adapter](https://github.com/flutterdata/flutter_data_json_api_adapter/) does) | -| `isNetworkError` | whether to retry a request when back online | - -
- -And if we have multiple models that all share the same base URL? - -We can simply make the adapter generic and apply it to any `DataModel` in our app! - -```dart {hl_lines=[1 2 3 4 7]} -mixin JsonServerAdapter> on RemoteAdapter { - @override - String get baseUrl => 'https://myapi.com/v1/'; -} - -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class User extends DataModel { - final int? id; - final String name; - final String? email; - - Task({this.id, required this.name, this.email}); -} -``` - -{{< notice >}} -**Important**: As the repository is generated, any change in the list of adapters **must** be followed by a build in order to take effect. - -```bash -flutter pub run build_runner build -``` - -Trouble generating code? [See here](/docs/faq/#errors-generating-code). -{{< /notice >}} - -Any number of adapters can be added and they will be applied in order. - -That is: - -```dart -@DataRepository([A, B, C, D, E]) -``` - -after codegen will become: - -```dart -RemoteAdapter with A, B, C, D, E; -``` - -## Custom endpoints - -Not every API perfectly aligns to CRUD endpoints. Here's an example on how to create a custom action, using the `sendRequest` API. - -```dart -mixin PaymentAdapter on RemoteAdapter { - Future createManualPayment({ - required String paymentType, - required double amount, - }) async { - final appConfig = read(appConfigProvider).instance; - - final payload = { - 'payment': { - 'app_id': appConfig.appId, - 'amount': amount, - 'name': paymentType, - 'provider': 'manual' - }, - }; - - return sendRequest( - baseUrl.asUri / 'payments.json' & {'v': true}, - method: DataRequestMethod.POST, - headers: await defaultHeaders & {'X-Client-Id': appConfig.appId}, - body: json.encode(payload), - onSuccess: (data) { - return deserialize(data as Map).model; - }, - ); - } -} -``` - -Notice that a Riverpod `Reader` is available on every adapter as `read`, too. - -The `createManualPayment` action can now be invoked like: - -```dart -onPressed: () async { - final payment = await ref.payments.paymentAdapter.createManualPayment( - paymentType: PaymentType.LIGHTNING_NETWORK, - amount: amount, - ); - // ... -} -``` - -{{< notice >}} -This is the signature for the `sendRequest` method, that performs an HTTP request and returns the result of type `R` via `onSuccess`: - -```dart -Future sendRequest( - final Uri uri, { - DataRequestMethod method = DataRequestMethod.GET, - Map? headers, - String? body, - _OnSuccessGeneric? onSuccess, - _OnErrorGeneric? onError, - bool omitDefaultParams = false, - DataRequestLabel? label, -}); -``` - -- `uri` takes the full `Uri` (you must provider base URL and query parameters, too) -- `headers` takes the full headers (or `defaultHeaders` if omitted) - -{{< /notice >}} - -With all these building blocks adapters for Wordpress or Github REST access, or even JWT authentication are easy to build. - -## Overriding watchers - -Let's imagine our app has to list completed payments in different widgets. - -```dart -final state = ref.payments.watchAll(params: {'filter': {'complete': true}}); -``` - -We could use something like the above to only request completed payments from the backend API. - -But non-completed payments in local storage would still show up through `watchAll`, so we would have to filter them every time in every widget. - -Except if we override this behavior. Since the _meat_ of the watchers happens in the notifiers (`watchAllNotifier` in this case), that is what we are going to override: - -```dart -@override -DataStateNotifier?> watchAllNotifier({ - bool? remote, - Map? params, - Map? headers, - bool? syncLocal, - String? finder, - DataRequestLabel? label, -}) { - return super - .watchAllNotifier( - remote: remote, - params: params, - headers: headers, - syncLocal: syncLocal, - finder: finder, - label: label, - ) - .where((payment) => payment.isComplete); -} -``` - -Both `where` and `map` are available as notifier extensions. In the future these could be turned into watcher arguments for easier access. - -## Custom behavior on model initialization - -Creating a `Task` will not persist it by default, we'd need to call `saveLocal()` for that. - -There is a hook for model initialization which can be used to execute custom behavior such as auto-saving. - -```dart -mixin TaskAdapter on RemoteAdapter { - @override - void onModelInitialized(Task model) => model.saveLocal(); -} -``` - -**Many more adapter examples can be found perusing the [articles](/articles).** - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/faq.md b/content/docs/faq.md deleted file mode 100644 index 654c188..0000000 --- a/content/docs/faq.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "FAQ" -weight: 60 -menu: docs ---- - -## Why are `save` and other methods not available on my models? - -`DataModel` extensions are syntax sugar and will **only** work when importing Flutter Data: - -```dart -import 'package:flutter_data/flutter_data.dart'; -``` - -## Errors generating code? - -If you have trouble with the outputs, try: - -```bash -flutter pub run build_runner build --delete-conflicting-outputs -``` - -{{< notice >}} -**VSCode users!** - -If after generating code you still see errors in your files, try reopening the project. This is not a Flutter Data issue. -{{< /notice >}} - -Also make sure your dependencies are up to date: - -```bash -flutter pub upgrade -``` - -## Can I group multiple adapter mixins into one? - -Not yet. - -See https://stackoverflow.com/questions/59248686/how-to-group-mixins-in-dart - -## Does Flutter Data depend on Flutter? - -No! Despite its name this library does not depend on Flutter at all. - -See the `example` folder for an, uh, example. - -It does depend on [Riverpod](https://pub.dev/packages/riverpod) but this library is exported. - -## Where does Flutter Data place generated code? - -- in `*.g.dart` files (part of your models) -- in `main.data.dart` (as a library) - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/initialization.md b/content/docs/initialization.md deleted file mode 100644 index 57245a7..0000000 --- a/content/docs/initialization.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: "Initialization" -weight: 55 -menu: docs ---- - -Initializing Flutter Data consists of two parts: local storage initialization and repository initialization. - -The former happens when wiring up providers and the latter during widget build. - -### Local storage initialization - -Here are the configuration options with their default arguments explicit: - -```dart -ProviderScope( - child: MyApp(), - overrides: [ - configureRepositoryLocalStorage( - // callback that returns a base directory where to place local storage - // (if the path_provider package is present, otherwise you MUST override it) - baseDirFn: () => getApplicationDocumentsDirectory().then((dir) => dir.path), - // 256-bit key for AES encryption - encryptionKey: null, - // whether to clear all local storage during initialization - clear: false, - ), - graphNotifierThrottleDurationProvider.overrideWithValue(Duration.zero), - ], -), -``` - -Customizing the duration of the throttle on `GraphNotifier` will determine how often Flutter widgets are marked for rebuild when using [watchers](/docs/repositories#watchers). - -### Repository initialization - -Use `repositoryInitializerProvider` without arguments: - -```dart -Container( - child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) => Text('Flutter Data is ready: ${ref.tasks}'), - ), -), -``` - -## Flutter with Riverpod - -```dart {hl_lines=[3 5 6 12 "23-29"]} -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:flutter_data/flutter_data.dart'; - -import 'main.data.dart'; -import 'models/task.dart'; - -void main() { - runApp( - ProviderScope( - child: MyApp(), - overrides: [configureRepositoryLocalStorage()], - ), - ); -} - -class MyApp extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( - home: Scaffold( - body: Center( - child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) => Text('Flutter Data is ready: ${ref.tasks}'), - ), - ), - ), - ); - } -} -``` - -## Flutter with Provider - -See [Configure Flutter Data to Work with Provider](/articles/configure-provider/) - -## Flutter with GetIt - -See [Configure Flutter Data to Work with GetIt](/articles/configure-get-it/) - -## Dart - -```dart -// lib/main.dart - -late final Directory _dir; - -final container = ProviderContainer( - overrides: [ - // baseDirFn MUST be provided - configureRepositoryLocalStorage(baseDirFn: () => _dir.path), - ], -); - -try { - _dir = await Directory('tmp').create(); - _dir.deleteSync(recursive: true); - - await container.read(repositoryInitializerProvider.future); - - final usersRepo = container.read(usersRepositoryProvider); - await usersRepo.findOne(1); - // ... -} -``` - -## Re-initializing - -It is possible to re-initialize Flutter Data, for example to perform a restart with [Phoenix](https://pub.dev/packages/flutter_phoenix) or simply a Riverpod `ref.refresh`: - -```dart {hl_lines=[6]} -class MyApp 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: (_) => Text('Flutter Data is ready: ${ref.tasks}'), - ), - ), - ), - ), - ); - } -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/local-adapters.md b/content/docs/local-adapters.md deleted file mode 100644 index 42a4dfc..0000000 --- a/content/docs/local-adapters.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Local Adapters" -weight: 21 -menu: docs ---- - -Local adapters access the local storage, which for now is only Hive. - -It is extremely rare to have to override a local adapter, so use with caution if you do. - -A particularly useful use-case is data migration as `LocalAdapter`'s `deserialize` will be called after loading raw data from the Hive box and before the `json_serializable` call. - -Example: - -```dart -mixin TaskLocalAdapter on LocalAdapter { - @override - Task deserialize(Map map) { - // transform map from old format to new format - } -} -``` - -Activate the use of the overridden adapter with: - -```dart -@DataRepository( - [TaskAdapter], - localAdapters: [TaskLocalAdapter], -) -class Task extends DataModel { - // ... -} -``` \ No newline at end of file diff --git a/content/docs/models.md b/content/docs/models.md deleted file mode 100644 index f6df928..0000000 --- a/content/docs/models.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Models -date: 2020-04-20T19:01:08-03:00 -weight: 30 -menu: docs ---- - -Flutter Data models are data classes that extend `DataModel` and are annotated with `@DataRepository`: - -```dart {hl_lines=[3]} -@DataRepository([TaskAdapter]) -@JsonSerializable() -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); -} -``` - -`DataModel` automatically registers new data classes within the framework and enforces the implementation of an `id` getter. Use the type that better suits you: `int?` and `String?` are the most common. - -{{< notice >}} -The `json_serializable` library is helpful but not required. - -- Model with `@JsonSerializable`? You don't need to declare `fromJson` or `toJson` -- Model without `@JsonSerializable`? You must declare `fromJson` and `toJson` - -If you choose it, you can make use of `@JsonKey` and other configuration parameters as usual. A common use-case is having a different remote `id` attribute such as `objectId`. Annotating `id` with `@JsonKey(name: 'objectId')` takes care of it. -{{< /notice >}} - -## Freezed support - -Here's an example: - -```dart -@freezed -@DataRepository([TaskAdapter]) -class Task extends DataModel with _$Task { - Task._(); - - factory Task({ - int? id, - required String name, - required BelongsTo user, - }) = _Task; - - factory Task.fromJson(Map json) => _$TaskFromJson(json); -} -``` - -Unions haven't been tested yet. - -## Omitting attributes - -In order to omit an attribute simply use `@JsonKey(ignore: true)`. - -## Extension methods - -In addition, various useful methods become available on the class: - -### save - -```dart -final user = User(id: 1, name: 'Frank'); -await user.save(); -``` - -The call is syntax-sugar for [Repository#save](/docs/repositories/#save) and takes the same arguments (except the model). - -Or, saving locally (i.e. `remote: false`) with a sync API: - -```dart -final user = User(id: 1, name: 'Frank'); -user.saveLocal(); -``` - -### delete - -```dart -final user = await repository.findOne(1); -await user.delete(); -``` - -It is syntax-sugar for [Repository#delete](/docs/repositories/#delete) and takes the same arguments (except the model). - -Or, deleting locally (i.e. `remote: false`) with a sync API: - -```dart -final user = User(id: 1, name: 'Frank'); -user.deleteLocal(); -``` - -### find - -```dart -final updatedUser = await user.find(); -``` - -It's syntax-sugar for [Repository#findOne](/docs/repositories/#findone) and takes the same arguments (except the model/ID). - -Or, reloading locally (i.e. `remote: false`) with a sync API: - -```dart -final user = User(id: 1, name: 'Frank'); -final user2 = user.reloadLocal(); -``` - -### withKeyOf - -Used whenever we need to transfer identity to a model without identity (that is, without an ID). - -```dart -final user = User(id: 1, 'Parker'); -final user2 = user.copyWith(name: 'Frank').withKeyOf(user); -``` - -`id` will still be `null` but saving and retreiving will work: - -```dart -await user2.save(remote: false); -final user3 = await user2.find(); -// user3.id == 1 -``` - -{{< notice >}} -Any Dart file that wants to use these extensions must import the library. - -```dart -import 'package:flutter_data/flutter_data.dart'; -``` - -**VSCode protip!** Type `Command + .` over the missing method and choose to import! - -You can also disable them by hiding the extension: - -```dart -import 'package:flutter_data/flutter_data.dart' hide DataModelExtension; -``` - -{{< /notice >}} - - -## Polymorphic models - -An example where `Staff` and `Customer` are both `User`s: - -```dart -abstract class User> extends DataModel { - final String id; - final String name; - User({this.id, this.name}); -} - -@JsonSerializable() -@DataRepository([JSONAPIAdapter, BaseAdapter]) -class Customer extends User { - final String abc; - Customer({String id, String name, this.abc}) : super(id: id, name: name); -} - -@JsonSerializable() -@DataRepository([JSONAPIAdapter, BaseAdapter]) -class Staff extends User { - final String xyz; - Staff({String id, String name, this.xyz}) : super(id: id, name: name); -} -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/offline.md b/content/docs/offline.md deleted file mode 100644 index 1ceb4c4..0000000 --- a/content/docs/offline.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "Offline" -weight: 51 -menu: docs ---- - -You can do this in your `Scaffold` - -```dart -child: ref.watch(initializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) => Text('App boot is ready, replace me with main UI widget'), - ), -), -``` - -Then define your initializer where you initialize any number of services needed to display the main widget of your UI: - -```dart -final initializerProvider = FutureProvider((ref) async { - // initialize FD - await ref.container.refresh(repositoryInitializerProvider.future); - - // initialize other services - - // retry offline events - final _sub = ref.listen(offlineRetryProvider, (_, __) {}); - - // close offline retry subscription - ref.onDispose(() { - _sub.close(); - }); -}); -``` - -You could also place this offline retry logic in some more specific place (for example when a user logs in, and close the sub when the user logs out). \ No newline at end of file diff --git a/content/docs/quickstart.md b/content/docs/quickstart.md deleted file mode 100644 index 0daff0b..0000000 --- a/content/docs/quickstart.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Quickstart -menu: docs -weight: 1 ---- - -Add `flutter_data` and dependencies to your `pubspec.yaml` file: - -```yaml {hl_lines=[5 13]} -dependencies: - flutter: - sdk: flutter - - flutter_data: ^{{< latest >}} - - # Highly RECOMMENDED (but not required) packages - path_provider: ^2.0.11 - json_annotation: ^4.7.0 - hooks_riverpod: ^2.1.1 - -dev_dependencies: - build_runner: ^2.2.0 # REQUIRED! - - # Highly RECOMMENDED (but not required) packages - json_serializable: ^6.4.1 -``` - -Flutter Data doesn't require any library besides `build_runner` for code generation. - -However, `json_serializable` and `path_provider` are very convenient so they are recommended. - -{{< notice >}} -The latest `flutter_data` should be `{{% latest %}}`. Please check for all packages latest stable versions before copy-pasting dependencies. -{{< /notice >}} - -### On Riverpod - -This package is developed for Riverpod, specifically **[Riverpod 2.x Hooks](https://docs-v2.riverpod.dev/docs/about_hooks)**. Other libraries such as [Provider](/articles/configure-provider/) or [GetIt](/articles/configure-get-it/) might work but there are no guarantees. - -### Basic configuration 🔧 - -Make your models extend `DataModel`, override `id` and annotate them with `@DataRepository()`. - -```dart {hl_lines=[7 8]} -import 'package:flutter_data/flutter_data.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'task.g.dart'; - -@JsonSerializable() -@DataRepository([]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); -} -``` - -`@DataRepository()` takes a list of adapters. - -[Adapters](/docs/adapters) are Dart mixins used to customize the framework's behavior, ranging from the very basic to the extremely powerful. They are applied on Flutter Data's `RemoteAdapter` base class. - -Let's start by the most typical configuration to access a remote API, the base URL. - -```dart -mixin JsonServerAdapter> on RemoteAdapter { - @override - String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo/'; -} -``` - -Next, we'll pass it to the annotation: - -```dart {hl_lines=[2]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class Task extends DataModel { - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); -} -``` - -Notice two things about our model above: - -- We used `int?` to represent the actual type of the `id` identifier field as it is `null` when new (it could have been a `String` too) -- The `fromJson` and `toJson` functions were skipped as they are not required (Flutter Data will automatically use `_$TaskFromJson` and `_$TaskToJson` generated by `json_serializable` – but they can both be overridden) - -{{< notice >}} - -#### Default serialization - -Flutter Data ships with a built-in serializer/deserializer for [classic JSON](https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html). - -A `Task` instance in JSON would look like: - -```json -{ - "id": 1, - "title": "Finish this documentation for once", - "completed": false, - "userId": 1 -} -``` - -{{}} - -We are now ready to run a build: - -```bash -flutter pub run build_runner build -``` - -Flutter Data auto-generated a `Repository` class for `Task`. - -It also generated a Dart library at `main.data.dart` which makes Flutter Data initialization effortless. It's out-of-the-box compatible with Riverpod. - -{{< notice >}} -Trouble generating code? [See here](/docs/faq/#errors-generating-code). - -Here is how to make it work with [Provider](/docs/faq/#configure-for-provider) and [GetIt](/docs/faq/#configure-for-getit). -{{< /notice >}} - -Next step is to configure local storage and initialize the framework: - -```dart {hl_lines=[2 3 4 10 "21-25"]} -import 'package:flutter/material.dart'; -import 'package:flutter_data/flutter_data.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:tutorial/main.data.dart'; - -void main() { - runApp( - ProviderScope( - child: TasksApp(), - overrides: [configureRepositoryLocalStorage()], - ), - ); -} - -class TasksApp extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( - home: Scaffold( - body: Center( - child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) => Text('Hello from Flutter Data ${ref.tasks}!'), - ), - ), - ), - debugShowCheckedModeBanner: false, - ); - } -} -``` - -Once the `data` callback is invoked, Flutter Data is ready and the `Task` repository can be accessed via `ref.tasks`! - -{{< notice >}} -The `configureRepositoryLocalStorage` setup function has several optional arguments. - -If you do not have `path_provider` as a dependency you will have to supply `baseDirFn` (a function that returns a base directory for local storage). - -For more information see [initialization](/docs/initialization). - -Prefer a setup example? Here's the [sample setup app](https://github.com/flutterdata/flutter_data_setup_app) with support for Riverpod, Provider and get_it. -{{< /notice >}} - -➡ Continue with the [tutorial for a Tasks app](/tutorial) or learn more about [Repositories](/docs/repositories) diff --git a/content/docs/relationships.md b/content/docs/relationships.md deleted file mode 100644 index c46c897..0000000 --- a/content/docs/relationships.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: Relationships -date: 2020-04-20T17:21:33-03:00 -weight: 40 -menu: docs ---- - -Flutter Data features an advanced relationship mapping system. - -Use: - -- `HasMany` for to-many relationships -- `BelongsTo` for to-one relationships - -As an example, a `User` has many `Task`s: - -```dart {hl_lines=[7]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class User extends DataModel { - @override - final int? id; - final String name; - final HasMany? tasks; - - User({this.id, required this.name, this.tasks}); -} -``` - -and a `Task` belongs to a `User`: - -```dart {hl_lines=[8]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - final BelongsTo? user; - - Task({this.id, required this.title, this.completed = false, this.user}); -} -``` - -As long as the API responds correctly with relationship data (for example a `User` resource with a collection of `Task` models – or just IDs if models are already present in local storage) we can expect the following to work: - -```dart -final user = await repository.findOne(1, params: {'_embed': 'tasks'}); -final task = user!.tasks!.first; - -print(task.title); // write Flutter Data docs -print(task.user!.value!.name); // Frank - -// or - -final house = House(address: '123 Main Rd'); -final family = Family(surname: 'Assange', house: BelongsTo(house)); - -print(family.house.value.families.first.surname); // Free Assange -``` - -We can infinitely navigate the relationship graph as it's based on a reactive graph data structure (`GraphNotifier`). - -## Defaults - -For relationships to work they must not be `null`. - -```dart -final task = Task(title: 'do 1', user: BelongsTo()); -``` - -If we don't want to supply a new relationship object like above, we may provide defaults like so: - -```dart {hl_lines=[10 11]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - late final BelongsTo user; - - Task({this.id, required this.title, this.completed = false, BelongsTo? user}) : - user = user ?? BelongsTo(); -} -``` - -## Inverses - -Inverse relationships are guessed when unambiguous (one relationship of inverse type). - -Not in this case, as `Family` has two `BelongsTo`s: - -```dart -@JsonSerializable() -@DataRepository([]) -class Family extends DataModel { - @override - final String? id; - final BelongsTo? cottage; - final BelongsTo? residence; - - Family({ - this.id, - this.cottage, - this.residence, - }); -} - -@JsonSerializable() -@DataRepository([]) -class House extends DataModel { - @override - final String? id; - final BelongsTo? owner; - - House({ - this.id, - BelongsTo? owner, - }) : owner = owner ?? BelongsTo(); -``` - -If you wish to disambuiguate or to be explicit, annotate your relationship in the `House` model: - -```dart -@DataRelationship(inverse: 'residence') -final BelongsTo? owner; -``` - -Here's another example, a tree structure using custom inverses and Freezed: - -```dart -@freezed -@DataRepository([], remote: false) -class Node extends DataModel, _$Node { - Node._(); - factory Node( - {int? id, - String? name, - @DataRelationship(inverse: 'children') BelongsTo? parent, - @DataRelationship(inverse: 'parent') HasMany? children}) = _Node; - factory Node.fromJson(Map json) => _$NodeFromJson(json); -} -``` - -## Remove a relationship - -Given a `Post` with many `Comment`s we want to remove: - -```dart -final postWithNoComments = post.copyWith(comments: HasMany.remove()).was(post); -``` - -Works with both `HasMany` and `BelongsTo`. - -Removing a relationship does not delete its linked resources (the actual comments in this case). - -## Disable relationship serialization - -In order to keep the relationship working but avoid persisting it, use: - -```dart -@DataRelationship(serialize: false) -final BelongsTo? post; -``` - -## Self-referential relationships - -```dart -// in Post -@DataRelationship(serialize: false) -late BelongsTo post = asBelongsTo; -``` - -## Relationship extensions - -A `User` with `Task`s could be created like this: - -```dart -final t1 = Task(title: 'do 1'); -final t2 = Task(title: 'do 2'); -final user = User(name: 'Frank', tasks: HasMany({t1, t2})); - -// or using an extension on Set - -final user = User(name: 'Frank', tasks: {t1, t2}.asHasMany); -``` - -or a `Task` with `User`: - -```dart -final user = User(name: 'Frank'); -final task = Task(title: 'do 1', user: BelongsTo(user)); - -// or using an extension on DataModel - -final task = Task(title: 'do 1', user: user.asBelongsTo); -``` - -{{< contact >}} \ No newline at end of file diff --git a/content/docs/repositories.md b/content/docs/repositories.md deleted file mode 100644 index e8beb21..0000000 --- a/content/docs/repositories.md +++ /dev/null @@ -1,554 +0,0 @@ ---- -title: "Repositories" -weight: 10 -menu: docs ---- - -Flutter Data is organized around the concept of [models](/docs/models) which are data classes extending `DataModel`. - -```dart -@DataRepository([TaskAdapter]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); - - // ... -} -``` - -When annotated with `@DataRepository` (and [adapters](/docs/adapters) as arguments, as we'll see later) a model gets its own fully-fledged repository. - -`Repository` is the API used to interact with models, whether local or remote. - -Assuming a `Task` model and its corresponding `Repository`, let's see how to retrieve such resources from an API. - -## Finders - -### findAll - -Using `ref.tasks` (short for `ref.watch(tasksRepositoryProvider)`) to obtain a repository we can find all resources in the collection. - -```dart -Repository repository = ref.tasks; -final tasks = await repository.findAll(); - -// GET http://base.url/tasks -``` - -This async call triggered a request to `GET http://base.url/tasks`. - -{{< notice >}} - {{< partial "magic1.md" >}} -{{< /notice >}} - -Method signature: - -```dart -Future?> findAll({ - bool? remote, - bool? background, - Map? params, - Map? headers, - bool? syncLocal, - OnSuccessAll? onSuccess, - OnErrorAll? onError, - DataRequestLabel? label, -}); -``` - -Further information on the `remote`, `background`, `params`, `headers`, `onSuccess`, `onError` and `label` arguments available in [common arguments](#common-arguments) below. - -The `syncLocal` argument instructs local storage to synchronize the exact resources returned from the remote source (for example, to reflect server-side deletions). - -```dart -final tasks = await ref.tasks.findAll(syncLocal: true); -``` - -Consider this example: - -If a first call to `findAll` returns data for task IDs `1`, `2`, `3` and a second call updated data for `2`, `3`, `4` you will end up in your local storage with: `1`, `2` (updated), `3` (updated) and `4`. - -Passing `syncLocal: true` to the second call will leave the local storage state with `2`, `3` and `4`. - -📚 [See API docs](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/findAll.html) - -### findOne - -Finds a resource by ID and saves it in local storage. - -```dart -final task = await ref.tasks.findOne(1); - -// GET http://base.url/tasks/1 -``` - -{{< notice >}} - -Similar to what's shown above in [findAll](#findall), Flutter Data resolves the URL by using the `urlForFindOne` function. We can override this in an [adapter](/docs/adapters). - -For example, use path `/tasks/something/1`: - -```dart -mixin TaskURLAdapter on RemoteAdapter { - @override - String urlForFindOne(id, params) => '$type/something/$id'; -} - -// would result in GET http://base.url/tasks/something/1 -``` - -{{< /notice >}} - -It can also take a `T` with an ID: - -```dart -final task = await ref.tasks.findOne(anotherTaskWithId3); - -// GET http://base.url/tasks/3 -``` - -Method signature: - -```dart -Future findOne( - Object id, { - bool? remote, - bool? background, - Map? params, - Map? headers, - OnSuccessOne? onSuccess, - OnErrorOne? onError, - DataRequestLabel? label, -}); -``` - -The `remote`, `background`, `params`, `headers`, `onSuccess`, `onError` and `label` arguments are detailed in [common arguments](#common-arguments) below. - -📚 [See API docs](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/findOne.html) - -## Save and delete - -### save - -Persists a model to local storage and remote. - -```dart -final savedTask = await repository.save(task); -``` - -{{< notice >}} - -Want to use the `PUT` verb instead of `PATCH`? Use this [adapter](/docs/adapters): - -```dart -mixin TaskURLAdapter on RemoteAdapter { - @override - String methodForSave(id, params) => id != null ? DataRequestMethod.PUT : DataRequestMethod.POST; -} -``` -{{< /notice >}} - -Method signature: - -```dart -Future save( - T model, { - bool? remote, - Map? params, - Map? headers, - OnSuccessOne? onSuccess, - OnErrorOne? onError, - DataRequestLabel? label, -}); -``` - -The `remote`, `params`, `headers`, `onSuccess`, `onError` and `label` arguments are detailed in [common arguments](#common-arguments) below. - -📚 [See API docs](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/save.html) - -### delete - -Deletes a model from local storage and sends a `DELETE` HTTP request. - -```dart -await repository.delete(model); -``` - -Method signature: - -```dart -Future delete( - Object model, { - bool? remote, - Map? params, - Map? headers, - OnSuccessOne? onSuccess, - OnErrorOne? onError, - DataRequestLabel? label, -}); -``` - -The `remote`, `params`, `headers`, `onSuccess`, `onError` and `label` arguments are detailed in [common arguments](#common-arguments) below. - -📚 [See API docs](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/delete.html) - - -## Watchers - -`DataState` is a class that holds state related to resource fetching and is practical in UI applications. It is returned in `watchAll` and `watchOne`. - -```dart -class DataState with EquatableMixin { - T model; - bool isLoading; - DataException? exception; - StackTrace? stackTrace; - // ... -} -``` - -It's typically used in a widget's `build` method like: - -```dart -class MyApp extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(); - if (state.isLoading) { - return CircularProgressIndicator(); - } - if (state.hasException) { - return ErrorScreen(state.exception, state.stackTrace); - } - return ListView( - children: [ - for (final task in state.model) - Text(task.title), - // ... - } -} -``` - -{{< notice >}} - -Why not used a [Freezed union](https://pub.dev/packages/freezed#union-types-and-sealed-classes) instead? - -Because without forcing to branch, `DataState` easily allows rebuilding widgets when multiple substates happen simultaneously – a very common pattern. The tradeoff is having to remember to check for the `loading` and `error` substates. - -{{< /notice >}} - - -### watchAll - -Watches all models of a given type in local storage (through `ref.watch` and `watchAllNotifier`). - -For updates to any model of type `Task` to prompt a rebuild we can use: - -```dart -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(); - if (state.isLoading) { - return CircularProgressIndicator(); - } - // use state.model which is a List - } -); -``` - -By default when first rendered it triggers a background [`findAll`](#findall) call with `remote`, `params`, `headers`, `syncLocal` and `label` arguments. See [common arguments](#common-arguments). - -Method signature: - -```dart -DataState?> watchAll({ - bool? remote, - Map? params, - Map? headers, - bool? syncLocal, - String? finder, - DataRequestLabel? label, -}); -``` - -But this can easily be overridden. Any method in the [adapter](/docs/adapters) with the **exact [`findAll`](#findall) method signature** and annotated with `@DataFinder()` will be available to supply to the `finder` argument as a string (method name). - -Pass `remote: false` to prevent any remote fetch at all. - -{{< notice >}} -**Note:** Both [`watchAllProvider`](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/watchAllProvider.html) and [`watchAllNotifier`](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/watchAllNotifier.html) are also available. -{{< /notice >}} - -📚 [See API docs](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/watchAll.html) - - -### watchOne - -Watches a model of a given type in local storage (through `ref.watch` and `watchOneNotifier`). - -For updates to a given model of type `Task` to prompt a rebuild we can use: - -```dart -class TaskScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchOne(1); - if (state.isLoading) { - return CircularProgressIndicator(); - } - // use state.model which is a Task - } -); -``` - -By default when first rendered it triggers a background [`findOne`](#findone) call with `model`, `remote`, `params`, `headers` and `label` arguments. See [common arguments](#common-arguments). - -Method signature: - -```dart -DataState watchOne( - Object model, { - bool? remote, - Map? params, - Map? headers, - AlsoWatch? alsoWatch, - String? finder, - DataRequestLabel? label, -}); -``` - -But this can easily be overridden. Any method in the [adapter](/docs/adapters) with the **exact [`findOne`](#findone) method signature** and annotated with `@DataFinder()` will be available to supply to the `finder` argument as a string (method name). - -Pass `remote: false` to prevent any remote fetch at all. - -In addition, this watcher can react to relationships via `alsoWatch`: - -```dart -watchOneNotifier(3, alsoWatch: (task) => [task.user]); -``` - -This feature is extremely powerful, actually any number of relationships can be watched: - -```dart -watchOneNotifier(3, alsoWatch: (task) => [task.reminders, task.user, task.user.profile, task.user.profile.comments]); -``` - - -{{< notice >}} -**Note:** Both [`watchOneProvider`](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/watchOneProvider.html) and [`watchOneNotifier`](https://pub.dev/documentation/flutter_data/latest/flutter_data/Repository/watchOneNotifier.html) are also available. -{{< /notice >}} - - -### watch - -This method takes a `DataModel` and watches its local changes. - -```dart -class TaskScreen extends HookConsumerWidget { - final Task model; - @override - Widget build(BuildContext context, WidgetRef ref) { - final task = ref.tasks.watch(model); - return Text(task.title); - } -); -``` - -Note that it returns a model, not a `DataState`. - -### notifierFor - -Obtain the notifier for a model. Does not trigger a remote request. - -```dart -final notifier = ref.tasks.notifierFor(task); - -// equivalent to -ref.tasks.watchOneNotifier(task, remote: false); -``` - -{{< notice >}} -By default, changes will be notified immediately and trigger widget rebuilds. - -For performance improvements, they can be throttled [by overriding `graphNotifierThrottleDurationProvider`](/docs/initialization). -{{< /notice >}} - -## Common arguments - -### remote - -Request only models in local storage: - -```dart -final tasks = await ref.tasks.findAll(remote: false); -``` - -Argument is of type `bool` and the default is `true`. - -{{< notice >}} -In addition to adapters, the `@DataRepository` annotation can take a `remote` boolean argument which will make it the default for the repository. - -```dart -@DataRepository([TaskAdapter], remote: false) -class Task extends DataModel { - // by default no operation hits the remote endpoint -} -``` - -{{< /notice >}} - -### background - -Default `false`. Calling a finder with `background = true` will make it return immediately with the current value in local storage while triggering a remote request in the background. This is typically useful when using this primitive in certain adapter customizations. - -### params - -Include query parameters (of type `Map`, in this case used for pagination and resource inclusion): - -```dart -final tasks = await ref.tasks.findAll( - params: {'include': 'comments', 'page': { 'limit': 20 }} -); - -// GET http://base.url/tasks?include=comments&page[limit]=20 -``` - -### headers - -Include HTTP headers as a `Map`: - -```dart -final tasks = await ref.tasks.findAll( - headers: { 'Authorization': 't0k3n' } -); -``` - -### onSuccess - -Overrides the handler for the success state, useful when requiring a specific transformation of raw response data. - -```dart -await ref.tasks.save( - task, - onSuccess: (data, label, adapter) async { - final model = await adapter.onSuccess(data, label); - return model as Task; - }, -); -``` - -[`RemoteAdapter#onSuccess`](https://pub.dev/documentation/flutter_data/latest/flutter_data/RemoteAdapter/onSuccess.html) is the default (overridable, of course). It esentially boils down to calls to `deserialize`. - -### onError - -Overrides the error handler: - -```dart -await ref.tasks.save( - task, - onError: (error, label, adapter) async { - throw WrappedException(error); - }, -); -``` - -[`RemoteAdapter#onError`](https://pub.dev/documentation/flutter_data/latest/flutter_data/RemoteAdapter/onError.html) is the default (overridable, of course). It essentially rethrows the error except if it is due to loss of connectivity or the remote resource was not found (HTTP 404). - -### label - -Optional argument of type `DataRequestLabel`. [See below](#logging-and-labels). - -## Logging and labels - -Labels are used in Flutter Data to easily track requests in logs. They carry an auto-generated `requestId` along with type and ID. They are provided by default and also by default finders log different events. - -Some examples: - -- `findAll/tasks@b5d14c` -- `findOne/users#3@c4a1bb` -- `findAll/tasks@b5d14c - -

- -Clients should only interact with repositories and adapters, while using the [Adapter API](/docs/adapters) to customize behavior. - -{{< contact >}} - -{{< internal >}} -GRAPHVIZ - -``` -digraph g { - rankdir="TB" - "RemoteAdapter" -> "LocalAdapter" -> "LocalStorage" - "RemoteAdapter" -> "LocalAdapter" -> "LocalStorage" - "LocalAdapter" -> "GraphNotifier" - "LocalAdapter" -> "GraphNotifier" - "GraphNotifier" -> "LocalStorage" - "Repository" -> "RemoteAdapter" - "Repository" -> "RemoteAdapter" - "Repository" -> "RemoteAdapter" - "Repository" -> "RemoteAdapter" - "tasksRepositoryProvider" -> "Repository" - "usersRepositoryProvider" -> "Repository" -} -``` - -{{< /internal >}} diff --git a/content/tutorial/creating.md b/content/tutorial/creating.md deleted file mode 100644 index 94486d7..0000000 --- a/content/tutorial/creating.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Creating a new task -weight: 20 -menu: tutorial ---- - -First off let's add just one line during the initialization. This will enable very helpful logging of our tasks repository! - -```dart {hl_lines=[6 7]} -// ... -child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) { - // enable verbose - ref.tasks.logLevel = 2; - return TasksScreen(); - } -), -// ... -``` - -When we restart we notice the following: - -```text -flutter: 34:061 [watchAll/tasks@e20025] initializing -flutter: 34:100 [findAll/tasks@e2046b task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ], - ); - } -} -``` - -For this we need to import `flutter_hooks`! - -Hot-reloading once again we see our `TextField` ready to create new tasks: - -{{< iphone "../w4.gif" >}} - -It was that easy! - -{{< notice >}} -You may have noticed that there was a flash with `[id: null]` (we didn't supply any ID upon model creation), until the server responds with one (in this case `11`) triggering an update. - -Be aware that our [dummy JSON backend](https://my-json-server.typicode.com/flutterdata/demo) does not actually save new resources so **it will always respond with ID `11`**, causing a confusing situation if you keep adding tasks! - -In the console: - -```text -flutter: 25:084 [watchAll/tasks@68f651] updated models -flutter: 25:087 [save/tasks@6bb411] requesting [HTTP POST] https://my-json-server.typicode.com/flutterdata/demo/tasks -flutter: 25:713 [save/tasks@6bb411] saved in local storage and remote -flutter: 25:714 [watchAll/tasks@68f651] updated models -``` - -{{< /notice >}} - -**NEXT: [Reloading the list](/tutorial/reloading)** - -{{< contact >}} \ No newline at end of file diff --git a/content/tutorial/deleting.md b/content/tutorial/deleting.md deleted file mode 100644 index 071c374..0000000 --- a/content/tutorial/deleting.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Deleting tasks -weight: 40 -menu: tutorial ---- - -There's stuff we just don't want to do! - -We can delete a `Task` on dismiss by wrapping the tile with a `Dismissible` and calling its `delete` method: - -```dart {hl_lines=["24-27"]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final _newTaskController = useTextEditingController(); - final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true); - - if (state.isLoading) { - return CircularProgressIndicator(); - } - - return RefreshIndicator( - onRefresh: () => - ref.tasks.findAll(params: {'_limit': 5}, syncLocal: true), - child: ListView( - children: [ - TextField( - controller: _newTaskController, - onSubmitted: (value) async { - Task(title: value).save(); - _newTaskController.clear(); - }, - ), - for (final task in state.model) - Dismissible( - key: ValueKey(task), - direction: DismissDirection.endToStart, - onDismissed: (_) => task.delete(), - child: ListTile( - leading: Checkbox( - value: task.completed, - onChanged: (value) => task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ), - ], - ), - ); - } -} -``` - -Hot-reload, swipe left and... they're gone! - -{{< iphone "../w7.gif" >}} - -{{< notice >}} -Remember to check out the debug console where you can find some Flutter Data activity logs like: - -```text -flutter: 25:691 [watchAll/tasks@1744b4] updated models -flutter: 25:693 [delete/tasks#4@1936e7] requesting [HTTP DELETE] https://my-json-server.typicode.com/flutterdata/demo/tasks/4 -flutter: 26:266 [delete/tasks#4@1936e7] deleted in local storage and remote -``` -{{< /notice >}} - -**NEXT: [Using relationships](/tutorial/relationships)** - -{{< contact >}} \ No newline at end of file diff --git a/content/tutorial/fetching.md b/content/tutorial/fetching.md deleted file mode 100644 index a08da2b..0000000 --- a/content/tutorial/fetching.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Fetching tasks -weight: 10 -menu: tutorial ---- - -{{< notice >}} -**Before you continue:** - -Make sure you went through the **[Quickstart](/docs/quickstart)** and got Flutter Data up and running! - -Also, you can check out the full source code for this tutorial at **https://github.com/flutterdata/tutorial** -{{< /notice >}} - -We now have access to our `Repository` through `ref.tasks`, with an API base URL set to `https://my-json-server.typicode.com/flutterdata/demo/`. - -Inspecting the `/tasks` endpoint we see: - -```json -[ - { - "id": 1, - "title": "Laundry 🧺", - "completed": false, - "userId": 1 - }, - { - "id": 2, - "title": "Groceries 🛒", - "completed": true, - "userId": 1 - }, - { - "id": 3, - "title": "Reservation at Malloys", - "completed": true, - "userId": 1 - }, - // ... -] -``` - -To bring these tasks into our app we'll use the `watchAll` method. (It internally makes a remote `findAll` call to `/tasks` and keeps watching local storage for any further changes in these models.) - -```dart {hl_lines=["10-20"]} -class TasksApp extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( - home: Scaffold( - body: Center( - child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) { - final state = ref.tasks.watchAll(); - if (state.isLoading) { - return CircularProgressIndicator(); - } - return ListView( - children: [ - for (final task in state.model) Text(task.title), - ], - ); - }, - ), - ), - ), - debugShowCheckedModeBanner: false, - ); - } -} -``` - -Bam 💥! - -{{< iphone "../w1.png" >}} - -**Whoa,** how did that happen? - - -{{< notice >}} -{{< partial "magic1.md" >}} - -For more information see the [Repository docs](/repository). -{{}} - -**NEXT: [Marking tasks as done](/tutorial/updating)** - -{{< contact >}} \ No newline at end of file diff --git a/content/tutorial/relationships.md b/content/tutorial/relationships.md deleted file mode 100644 index 787f986..0000000 --- a/content/tutorial/relationships.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Using relationships -weight: 100 -menu: tutorial ---- - -Let's now slightly rethink our query. Instead of **"fetching all tasks for user 1"** we are going to **"fetch user 1 with all their tasks"**. - -Flutter Data has first-class support for [relationships](/docs/relationships). - -First, in `models/user.dart`, we'll create the `User` model with a `HasMany` relationship: - -```dart {hl_lines=[16]} -import 'package:flutter_data/flutter_data.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import 'task.dart'; - -part 'user.g.dart'; - -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class User extends DataModel { - @override - final int? id; - final String name; - final HasMany tasks; - - User({this.id, required this.name, required this.tasks}); -} -``` - -Time to run code generation and get a brand-new `Repository`: - -```text -flutter pub run build_runner build -``` - -Great. We are now going to issue the remote request via `watchOne()`, in order to list (_and watch for changes of_) user `1`'s `Task` models: - - - `params: {'_embed': 'tasks'},` tells the server to include this user's tasks (which our JSON adapter knows how to deserialize) - - `alsoWatch: (user) => [user.tasks]` tells the [watcher](/docs/repositories/#watchone) to rebuild the widget any time user _or_ its tasks are updated or deleted; any number of relationships of any depth can be watched. (For instance, `alsoWatch: (user) => [user.tasks, user.tasks.comments, user.tasks.comments.owner]`) - -```dart {hl_lines=[5 6 7 8 9 15 18 28]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final _newTaskController = useTextEditingController(); - final state = ref.users.watchOne( - 1, // user ID, an integer - params: {'_embed': 'tasks'}, // HTTP param - alsoWatch: (user) => [user.tasks] // watcher - ); - - if (state.isLoading) { - return CircularProgressIndicator(); - } - - final tasks = state.model!.tasks.toList(); - - return RefreshIndicator( - onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}), - child: ListView( - children: [ - TextField( - controller: _newTaskController, - onSubmitted: (value) async { - Task(title: value).save(); - _newTaskController.clear(); - }, - ), - for (final task in tasks) - Dismissible( - key: ValueKey(task), - direction: DismissDirection.endToStart, - onDismissed: (_) => task.delete(), - child: ListTile( - leading: Checkbox( - value: task.completed, - onChanged: (value) => task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ), - ], - ), - ); - } -} -``` - -Import the `user.dart` file, reload and watch it working! - -{{< iphone "../w8a.png" >}} - -{{< notice >}} - -Note that tasks `4`, `5` and `9` for example were not loaded as they do not belong to user `1`! - -This is the API response for https://my-json-server.typicode.com/flutterdata/demo/users/1?_embed=tasks that was parsed by the built-in `deserialize` method: - -```json -{ - "id": 1, - "name": "frank06", - "tasks": [ - { - "id": 1, - "title": "Laundry 🧺", - "completed": false, - "userId": 1 - }, - { - "id": 2, - "title": "Groceries 🛒", - "completed": true, - "userId": 1 - }, - { - "id": 3, - "title": "Reservation at Malloys", - "completed": true, - "userId": 1 - }, - { - "id": 7, - "title": "Take Amanda to birthday", - "completed": true, - "userId": 1 - }, - { - "id": 8, - "title": "Get new surfboard 🏄‍♀️", - "completed": false, - "userId": 1 - }, - { - "id": 10, - "title": "Protest tyrannical mandates 👊", - "completed": true, - "userId": 1 - } - ] -} -``` - -{{< /notice >}} - -## Creating a task - -As it is, adding a new task will not work. Why is that? - -We are creating new `Task` models without any `User` associated to them: - -```dart -onSubmitted: (value) async { - Task(title: value).save(); - _newTaskController.clear(); -}, -``` - -Let's fix this. Add a `BelongsTo` relationship in `models/task.dart` and regenerate our code: - -```dart {hl_lines=[8 10 13]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - final BelongsTo user; - - Task({this.id, required this.title, this.completed = false, required this.user}); - - Task toggleCompleted() { - return Task(id: this.id, title: this.title, user: user, completed: !this.completed) - .withKeyOf(this); - } -} -``` - -Now we can provide the right user as a `BelongsTo`: - -```dart {hl_lines=[15 16 25]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final _newTaskController = useTextEditingController(); - final state = ref.users.watchOne( - 1, // user ID, an integer - params: {'_embed': 'tasks'}, // HTTP param - alsoWatch: (user) => [user.tasks] // watcher - ); - - if (state.isLoading) { - return CircularProgressIndicator(); - } - - final user = state.model!; - final tasks = user.tasks.toList(); - - return RefreshIndicator( - onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}), - child: ListView( - children: [ - TextField( - controller: _newTaskController, - onSubmitted: (value) async { - Task(title: value, user: BelongsTo(user)).save(); - _newTaskController.clear(); - }, - ), - for (final task in tasks) - Dismissible( - key: ValueKey(task), - direction: DismissDirection.endToStart, - onDismissed: (_) => task.delete(), - child: ListTile( - leading: Checkbox( - value: task.completed, - onChanged: (value) => task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ), - ], - ), - ); - } -} -``` - -And adding new tasks now works! - -{{< iphone "../w8.png" >}} - - -{{< notice >}} -**Check out the source code: https://github.com/flutterdata/tutorial** -{{< /notice >}} - -{{< contact >}} diff --git a/content/tutorial/reloading.md b/content/tutorial/reloading.md deleted file mode 100644 index 6bd5b95..0000000 --- a/content/tutorial/reloading.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: Reloading the list -weight: 30 -menu: tutorial ---- - -Let's make the number of tasks more manageable via the `_limit` server query param, which in this case will return a maximum of `5` resources. - -```dart {hl_lines=[4]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(params: {'_limit': 5}); - // ... -} -``` - -Hot restarting the app we should only see five tasks, but... - -{{< iphone "../w4.png" >}} - -It's exactly the same as before. **Why isn't this working? 🤔** - -Turns out `watchAll` is wired to show _all_ tasks in local storage. If the server responds with some tasks, this won't affect older tasks stored locally. - -To fix this, `watchAll` takes a `syncLocal` argument which forces local storage to mirror exactly the resources returned from the remote source. This can be useful to reflect server-side resource deletions, too. - -Let's try this out: - -```dart {hl_lines=[4]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true); - // ... -} -``` - -And it works like a charm: - -{{< iphone "../w5.png" >}} - -{{< notice >}} -With a real-world API we would still see all tasks marked as done. We went back to default as our dummy JSON backend does not store data. - -Another useful trick is to use `clear: true` on local storage configuration: - -```dart {hl_lines=[5]} -void main() { - runApp( - ProviderScope( - child: TasksApp(), - overrides: [configureRepositoryLocalStorage(clear: true)], - ), - ); -} -``` - -For more on initialization [see here](/docs/initialization). -{{< /notice >}} - -### Replacing the manual reload - -Instead of manually reloading/restarting we will now integrate `RefreshIndicator`. In the event handler we simply use [`findAll`](/docs/repositories#findall) and pass the same arguments: - -```dart {hl_lines=[10 11 12]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final _newTaskController = useTextEditingController(); - final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true); - - if (state.isLoading) { - return CircularProgressIndicator(); - } - return RefreshIndicator( - onRefresh: () => - ref.tasks.findAll(params: {'_limit': 5}, syncLocal: true), - child: ListView( - children: [ - TextField( - controller: _newTaskController, - onSubmitted: (value) async { - Task(title: value, completed: false).save(); - _newTaskController.clear(); - }, - ), - for (final task in state.model) - ListTile( - leading: Checkbox( - value: task.completed, - onChanged: (value) => task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ], - ), - ); - } -} -``` - -Now simply pull to refresh! - -{{< iphone "../w6.png" >}} - -{{< notice >}} -A similar method can be used to [fully re-initialize Flutter Data](/articles/how-to-reinitialize-flutter-data/). -{{< /notice >}} - -A DRY'er alternative would be getting hold of the notifier and calling `reload()` on it: - -```dart {hl_lines=[6 7 8 14]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final _newTaskController = useTextEditingController(); - - final provider = - ref.tasks.watchAllProvider(params: {'_limit': 5}, syncLocal: true); - final state = ref.watch(provider); - - if (state.isLoading) { - return CircularProgressIndicator(); - } - return RefreshIndicator( - onRefresh: () => ref.read(provider.notifier).reload(), - child: ListView( - // ... -``` - -**NEXT: [Deleting tasks](/tutorial/deleting)** - -{{< contact >}} \ No newline at end of file diff --git a/content/tutorial/updating.md b/content/tutorial/updating.md deleted file mode 100644 index c67b8b0..0000000 --- a/content/tutorial/updating.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Marking tasks as done -weight: 15 -menu: tutorial ---- - -A read-only tasks app is not very practical! Let's add the ability to update the `completed` state and mark/unmark our tasks as done. - -First, though, we'll extract the tasks-specific code to a separate screen named `TasksScreen`: - -```dart -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(); - if (state.isLoading) { - return CircularProgressIndicator(); - } - return ListView( - children: [ - for (final task in state.model!) Text(task.title), - ], - ); - } -} -``` - -Remember to return this new widget from `TasksApp`: - -```dart {hl_lines=[10]} -class TasksApp extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( - home: Scaffold( - body: Center( - child: ref.watch(repositoryInitializerProvider).when( - error: (error, _) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - data: (_) => TasksScreen(), - ), - ), - ), - debugShowCheckedModeBanner: false, - ); - } -} -``` - -Back to our `TasksScreen` we are going to wrap our title text widget in a `ListTile` prefixing it with a checkbox which, upon clicking, will toggle task completion: - - -```dart {hl_lines=["11-17"]} -class TasksScreen extends HookConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.tasks.watchAll(); - if (state.isLoading) { - return CircularProgressIndicator(); - } - return ListView( - children: [ - for (final task in state.model!) - ListTile( - leading: Checkbox( - value: task.completed, - onChanged: (value) => task.toggleCompleted().save(), - ), - title: Text('${task.title} [id: ${task.id}]'), - ), - ], - ); - } -} -``` - -If only the `toggleCompleted()` method existed... 😀 - -Since `Task` is immutable, we return a new `Task` object with the inverse boolean value of `completed`: - -```dart {hl_lines=[11 12 13]} -@JsonSerializable() -@DataRepository([JsonServerAdapter]) -class Task extends DataModel { - @override - final int? id; - final String title; - final bool completed; - - Task({this.id, required this.title, this.completed = false}); - - Task toggleCompleted() { - return Task(id: this.id, title: this.title, completed: !this.completed).withKeyOf(this); - } -} -``` - -{{< notice >}} -**What exactly is `withKeyOf(this)` for?** - -When a new model is created, Flutter Data initializes it looking up its internal key based on its `id`. - -This way, `Task(id: 4, title: 'a')` and `Task(id: 4, title: 'b')` are essentially two versions of the same model. - -When there is no `id` (or potentially no `id`, as in the case above) we can use [`withKeyOf`](/docs/models/#withkeyof) to ensure the new model is treated as an updated version of the old one. -{{< /notice >}} - -Regenerate code, hot-reload and check all boxes... - -{{< iphone "../w2.gif" >}} - -Done! **NEXT: [Creating a new task](/tutorial/creating)** - -{{< contact >}} \ No newline at end of file diff --git a/static/css/fonts.css b/css/fonts.css similarity index 100% rename from static/css/fonts.css rename to css/fonts.css diff --git a/docs/adapters/index.html b/docs/adapters/index.html new file mode 100644 index 0000000..cb41459 --- /dev/null +++ b/docs/adapters/index.html @@ -0,0 +1,117 @@ +Adapters - Flutter Data

Adapters

Flutter Data’s building blocks are called adapters, making it extremely customizable and composable.

Adapters are essentially Dart mixins applied on RemoteAdapter<T>.

Overriding basic behavior

Several pieces of information are required, for example, to construct a remote findAll call on a Repository<Task>. The framework takes a sensible guess and makes that GET /tasks by default.

Still, a base URL is necessary and the endpoint parts should be overridable.

The way we use these adapters is by declaring them on our @DataRepository annotation in the corresponding model. For example:

mixin JSONServerTaskAdapter on RemoteAdapter<Task> {
+  @override
+  String get baseUrl => 'https://myapi.com/v1/';
+}
+
+@JsonSerializable()
+@DataRepository([JSONServerTaskAdapter])
+class Task extends DataModel<Task> {
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+}
+

What if the endpoint actually is at https://myapi.com/v1/todos/all?

mixin JSONServerTaskAdapter on RemoteAdapter<Task> {
+  @override
+  String get baseUrl => 'https://myapi.com/v1/';
+
+  @override
+  String urlForFindAll(Map<String, dynamic> params) => 'todos/all';
+}
+

Here’s a list of overridable members:


typedefaults to a camel-cased, pluralized class name (User => users)
baseUrlmust be implemented or it will throw an error
urlForFindAlldefaults to type
methodForFindAlldefaults to DataRequestMethod.GET
urlForFindOnedefaults to ${type}/${id}
methodForFindOnedefaults to DataRequestMethod.GET
urlForSavedefaults to ${type}/${id} if updating
methodForSavedefaults to DataRequestMethod.PATCH if updating
urlForDeletedefaults to ${type}/${id}
methodForDeletedefaults to DataRequestMethod.DELETE
defaultParamsdefaults to {}
defaultHeadersdefaults to {'Content-Type': 'application/json'}
shouldLoadRemoteAllfine-grained control over the remote param on findAll
shouldLoadRemoteOnefine-grained control over the remote param on findOne
serializecan customize serialization (like the JSON API Adapter does)
deserializecan customize deserialization (like the JSON API Adapter does)
isNetworkErrorwhether to retry a request when back online

And if we have multiple models that all share the same base URL?

We can simply make the adapter generic and apply it to any DataModel in our app!

mixin JsonServerAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+  @override
+  String get baseUrl => 'https://myapi.com/v1/';
+}
+
+@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class User extends DataModel<User> {
+  final int? id;
+  final String name;
+  final String? email;
+
+  Task({this.id, required this.name, this.email});
+}
+

Important: As the repository is generated, any change in the list of adapters must be followed by a build in order to take effect.

flutter pub run build_runner build
+

Trouble generating code? See here.

Any number of adapters can be added and they will be applied in order.

That is:

@DataRepository([A, B, C, D, E])
+

after codegen will become:

RemoteAdapter<User> with A, B, C, D, E;
+

Custom endpoints

Not every API perfectly aligns to CRUD endpoints. Here’s an example on how to create a custom action, using the sendRequest API.

mixin PaymentAdapter on RemoteAdapter<Payment> {
+  Future<Payment?> createManualPayment({
+    required String paymentType,
+    required double amount,
+  }) async {
+    final appConfig = read(appConfigProvider).instance;
+
+    final payload = {
+      'payment': {
+        'app_id': appConfig.appId,
+        'amount': amount,
+        'name': paymentType,
+        'provider': 'manual'
+      },
+    };
+
+    return sendRequest(
+      baseUrl.asUri / 'payments.json' & {'v': true},
+      method: DataRequestMethod.POST,
+      headers: await defaultHeaders & {'X-Client-Id': appConfig.appId},
+      body: json.encode(payload),
+      onSuccess: (data) {
+        return deserialize(data as Map<String, dynamic>).model;
+      },
+    );
+  }
+}
+

Notice that a Riverpod Reader is available on every adapter as read, too.

The createManualPayment action can now be invoked like:

onPressed: () async {
+  final payment = await ref.payments.paymentAdapter.createManualPayment(
+    paymentType: PaymentType.LIGHTNING_NETWORK,
+    amount: amount,
+  );
+  // ...
+}
+

This is the signature for the sendRequest method, that performs an HTTP request and returns the result of type R via onSuccess:

Future<R?> sendRequest<R>(
+  final Uri uri, {
+  DataRequestMethod method = DataRequestMethod.GET,
+  Map<String, String>? headers,
+  String? body,
+  _OnSuccessGeneric<R>? onSuccess,
+  _OnErrorGeneric<R>? onError,
+  bool omitDefaultParams = false,
+  DataRequestLabel? label,
+});
+
  • uri takes the full Uri (you must provider base URL and query parameters, too)
  • headers takes the full headers (or defaultHeaders if omitted)

With all these building blocks adapters for Wordpress or Github REST access, or even JWT authentication are easy to build.

Overriding watchers

Let’s imagine our app has to list completed payments in different widgets.

final state = ref.payments.watchAll(params: {'filter': {'complete': true}});
+

We could use something like the above to only request completed payments from the backend API.

But non-completed payments in local storage would still show up through watchAll, so we would have to filter them every time in every widget.

Except if we override this behavior. Since the meat of the watchers happens in the notifiers (watchAllNotifier in this case), that is what we are going to override:

@override
+DataStateNotifier<List<Payment>?> watchAllNotifier({
+  bool? remote,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  bool? syncLocal,
+  String? finder,
+  DataRequestLabel? label,
+}) {
+  return super
+      .watchAllNotifier(
+        remote: remote,
+        params: params,
+        headers: headers,
+        syncLocal: syncLocal,
+        finder: finder,
+        label: label,
+      )
+      .where((payment) => payment.isComplete);
+}
+

Both where and map are available as notifier extensions. In the future these could be turned into watcher arguments for easier access.

Custom behavior on model initialization

Creating a Task will not persist it by default, we’d need to call saveLocal() for that.

There is a hook for model initialization which can be used to execute custom behavior such as auto-saving.

mixin TaskAdapter on RemoteAdapter<Task> {
+  @override
+  void onModelInitialized(Task model) => model.saveLocal();
+}
+

Many more adapter examples can be found perusing the articles.

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/faq/index.html b/docs/faq/index.html new file mode 100644 index 0000000..38114a2 --- /dev/null +++ b/docs/faq/index.html @@ -0,0 +1,13 @@ +FAQ - Flutter Data

FAQ

Why are save and other methods not available on my models?

DataModel extensions are syntax sugar and will only work when importing Flutter Data:

import 'package:flutter_data/flutter_data.dart';
+

Errors generating code?

If you have trouble with the outputs, try:

flutter pub run build_runner build --delete-conflicting-outputs
+

VSCode users!

If after generating code you still see errors in your files, try reopening the project. This is not a Flutter Data issue.

Also make sure your dependencies are up to date:

flutter pub upgrade
+

Can I group multiple adapter mixins into one?

Not yet.

See https://stackoverflow.com/questions/59248686/how-to-group-mixins-in-dart

Does Flutter Data depend on Flutter?

No! Despite its name this library does not depend on Flutter at all.

See the example folder for an, uh, example.

It does depend on Riverpod but this library is exported.

Where does Flutter Data place generated code?

  • in *.g.dart files (part of your models)
  • in main.data.dart (as a library)

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..c46c418 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,10 @@ +Docs - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/index.xml b/docs/index.xml new file mode 100644 index 0000000..068d1e5 --- /dev/null +++ b/docs/index.xml @@ -0,0 +1,29 @@ +Docs on Flutter Data/docs/Recent content in Docs on Flutter DataHugo -- gohugo.ioen-usMon, 20 Apr 2020 19:01:08 -0300Quickstart/docs/quickstart/Mon, 01 Jan 0001 00:00:00 +0000/docs/quickstart/Add flutter_data and dependencies to your pubspec.yaml file: +dependencies: flutter: sdk: flutter flutter_data: ^1.5.6 # Highly RECOMMENDED (but not required) packages path_provider: ^2.0.11 json_annotation: ^4.7.0 hooks_riverpod: ^2.1.1 dev_dependencies: build_runner: ^2.2.0 # REQUIRED! # Highly RECOMMENDED (but not required) packages json_serializable: ^6.4.1 Flutter Data doesn&rsquo;t require any library besides build_runner for code generation. +However, json_serializable and path_provider are very convenient so they are recommended. +The latest flutter_data should be 1.5.6. Please check for all packages latest stable versions before copy-pasting dependencies.Repositories/docs/repositories/Mon, 01 Jan 0001 00:00:00 +0000/docs/repositories/Flutter Data is organized around the concept of models which are data classes extending DataModel. +@DataRepository([TaskAdapter]) class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; Task({this.id, required this.title, this.completed = false}); // ... } When annotated with @DataRepository (and adapters as arguments, as we&rsquo;ll see later) a model gets its own fully-fledged repository. +Repository is the API used to interact with models, whether local or remote.Adapters/docs/adapters/Mon, 01 Jan 0001 00:00:00 +0000/docs/adapters/Flutter Data&rsquo;s building blocks are called adapters, making it extremely customizable and composable. +Adapters are essentially Dart mixins applied on RemoteAdapter&lt;T&gt;. +Overriding basic behavior Several pieces of information are required, for example, to construct a remote findAll call on a Repository&lt;Task&gt;. The framework takes a sensible guess and makes that GET /tasks by default. +Still, a base URL is necessary and the endpoint parts should be overridable. +The way we use these adapters is by declaring them on our @DataRepository annotation in the corresponding model.Local Adapters/docs/local-adapters/Mon, 01 Jan 0001 00:00:00 +0000/docs/local-adapters/Local adapters access the local storage, which for now is only Hive. +It is extremely rare to have to override a local adapter, so use with caution if you do. +A particularly useful use-case is data migration as LocalAdapter&rsquo;s deserialize will be called after loading raw data from the Hive box and before the json_serializable call. +Example: +mixin TaskLocalAdapter on LocalAdapter&lt;Task&gt; { @override Task deserialize(Map&lt;String, dynamic&gt; map) { // transform map from old format to new format } } Activate the use of the overridden adapter with:Models/docs/models/Mon, 20 Apr 2020 19:01:08 -0300/docs/models/Flutter Data models are data classes that extend DataModel and are annotated with @DataRepository: +@DataRepository([TaskAdapter]) @JsonSerializable() class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; Task({this.id, required this.title, this.completed = false}); } DataModel automatically registers new data classes within the framework and enforces the implementation of an id getter. Use the type that better suits you: int? and String? are the most common. +The json_serializable library is helpful but not required.Relationships/docs/relationships/Mon, 20 Apr 2020 17:21:33 -0300/docs/relationships/Flutter Data features an advanced relationship mapping system. +Use: +HasMany&lt;T&gt; for to-many relationships BelongsTo&lt;T&gt; for to-one relationships As an example, a User has many Tasks: +@JsonSerializable() @DataRepository([JsonServerAdapter]) class User extends DataModel&lt;User&gt; { @override final int? id; final String name; final HasMany&lt;Task&gt;? tasks; User({this.id, required this.name, this.tasks}); } and a Task belongs to a User: +@JsonSerializable() @DataRepository([JsonServerAdapter]) class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; final BelongsTo&lt;User&gt;?Offline/docs/offline/Mon, 01 Jan 0001 00:00:00 +0000/docs/offline/You can do this in your Scaffold +child: ref.watch(initializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) =&gt; Text(&#39;App boot is ready, replace me with main UI widget&#39;), ), ), Then define your initializer where you initialize any number of services needed to display the main widget of your UI: +final initializerProvider = FutureProvider&lt;void&gt;((ref) async { // initialize FD await ref.container.refresh(repositoryInitializerProvider.future); // initialize other services // retry offline events final _sub = ref.Initialization/docs/initialization/Mon, 01 Jan 0001 00:00:00 +0000/docs/initialization/Initializing Flutter Data consists of two parts: local storage initialization and repository initialization. +The former happens when wiring up providers and the latter during widget build. +Local storage initialization Here are the configuration options with their default arguments explicit: +ProviderScope( child: MyApp(), overrides: [ configureRepositoryLocalStorage( // callback that returns a base directory where to place local storage // (if the path_provider package is present, otherwise you MUST override it) baseDirFn: () =&gt; getApplicationDocumentsDirectory().FAQ/docs/faq/Mon, 01 Jan 0001 00:00:00 +0000/docs/faq/Why are save and other methods not available on my models? DataModel extensions are syntax sugar and will only work when importing Flutter Data: +import &#39;package:flutter_data/flutter_data.dart&#39;; Errors generating code? If you have trouble with the outputs, try: +flutter pub run build_runner build --delete-conflicting-outputs VSCode users! +If after generating code you still see errors in your files, try reopening the project. This is not a Flutter Data issue. +Also make sure your dependencies are up to date: \ No newline at end of file diff --git a/docs/initialization/index.html b/docs/initialization/index.html new file mode 100644 index 0000000..245ce40 --- /dev/null +++ b/docs/initialization/index.html @@ -0,0 +1,104 @@ +Initialization - Flutter Data

Initialization

Initializing Flutter Data consists of two parts: local storage initialization and repository initialization.

The former happens when wiring up providers and the latter during widget build.

Local storage initialization

Here are the configuration options with their default arguments explicit:

ProviderScope(
+  child: MyApp(),
+  overrides: [
+    configureRepositoryLocalStorage(
+      // callback that returns a base directory where to place local storage
+      // (if the path_provider package is present, otherwise you MUST override it)
+      baseDirFn: () => getApplicationDocumentsDirectory().then((dir) => dir.path),
+      // 256-bit key for AES encryption
+      encryptionKey: null,
+      // whether to clear all local storage during initialization
+      clear: false,
+    ),
+    graphNotifierThrottleDurationProvider.overrideWithValue(Duration.zero),
+  ],
+),
+

Customizing the duration of the throttle on GraphNotifier will determine how often Flutter widgets are marked for rebuild when using watchers.

Repository initialization

Use repositoryInitializerProvider without arguments:

Container(
+  child: ref.watch(repositoryInitializerProvider).when(
+        error: (error, _) => Text(error.toString()),
+        loading: () => const CircularProgressIndicator(),
+        data: (_) => Text('Flutter Data is ready: ${ref.tasks}'),
+      ),
+),
+

Flutter with Riverpod

import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:flutter_data/flutter_data.dart';
+
+import 'main.data.dart';
+import 'models/task.dart';
+
+void main() {
+  runApp(
+    ProviderScope(
+      child: MyApp(),
+      overrides: [configureRepositoryLocalStorage()],
+    ),
+  );
+}
+
+class MyApp extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return MaterialApp(
+      home: Scaffold(
+        body: Center(
+          child: ref.watch(repositoryInitializerProvider).when(
+                error: (error, _) => Text(error.toString()),
+                loading: () => const CircularProgressIndicator(),
+                data: (_) => Text('Flutter Data is ready: ${ref.tasks}'),
+              ),
+        ),
+      ),
+    );
+  }
+}
+

Flutter with Provider

See Configure Flutter Data to Work with Provider

Flutter with GetIt

See Configure Flutter Data to Work with GetIt

Dart

// lib/main.dart
+
+late final Directory _dir;
+
+final container = ProviderContainer(
+  overrides: [
+    // baseDirFn MUST be provided
+    configureRepositoryLocalStorage(baseDirFn: () => _dir.path),
+  ],
+);
+
+try {
+  _dir = await Directory('tmp').create();
+  _dir.deleteSync(recursive: true);
+
+  await container.read(repositoryInitializerProvider.future);
+
+  final usersRepo = container.read(usersRepositoryProvider);
+  await usersRepo.findOne(1);
+  // ...
+}
+

Re-initializing

It is possible to re-initialize Flutter Data, for example to perform a restart with Phoenix or simply a Riverpod ref.refresh:

class MyApp 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: (_) => Text('Flutter Data is ready: ${ref.tasks}'),
+                ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/local-adapters/index.html b/docs/local-adapters/index.html new file mode 100644 index 0000000..e14abc7 --- /dev/null +++ b/docs/local-adapters/index.html @@ -0,0 +1,23 @@ +Local Adapters - Flutter Data

Local Adapters

Local adapters access the local storage, which for now is only Hive.

It is extremely rare to have to override a local adapter, so use with caution if you do.

A particularly useful use-case is data migration as LocalAdapter’s deserialize will be called after loading raw data from the Hive box and before the json_serializable call.

Example:

mixin TaskLocalAdapter on LocalAdapter<Task> {
+  @override
+  Task deserialize(Map<String, dynamic> map) {
+    // transform map from old format to new format
+  }
+}
+

Activate the use of the overridden adapter with:

@DataRepository(
+  [TaskAdapter],
+  localAdapters: [TaskLocalAdapter],
+)
+class Task extends DataModel<Task> {
+  // ...
+}
+

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/models/index.html b/docs/models/index.html new file mode 100644 index 0000000..7b8934d --- /dev/null +++ b/docs/models/index.html @@ -0,0 +1,70 @@ +Models - Flutter Data

Models

Flutter Data models are data classes that extend DataModel and are annotated with @DataRepository:

@DataRepository([TaskAdapter])
+@JsonSerializable()
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+}
+

DataModel automatically registers new data classes within the framework and enforces the implementation of an id getter. Use the type that better suits you: int? and String? are the most common.

The json_serializable library is helpful but not required.

  • Model with @JsonSerializable? You don’t need to declare fromJson or toJson
  • Model without @JsonSerializable? You must declare fromJson and toJson

If you choose it, you can make use of @JsonKey and other configuration parameters as usual. A common use-case is having a different remote id attribute such as objectId. Annotating id with @JsonKey(name: 'objectId') takes care of it.

Freezed support

Here’s an example:

@freezed
+@DataRepository([TaskAdapter])
+class Task extends DataModel<Task> with _$Task {
+  Task._();
+
+  factory Task({
+    int? id,
+    required String name,
+    required BelongsTo<User> user,
+  }) = _Task;
+
+  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
+}
+

Unions haven’t been tested yet.

Omitting attributes

In order to omit an attribute simply use @JsonKey(ignore: true).

Extension methods

In addition, various useful methods become available on the class:

save

final user = User(id: 1, name: 'Frank');
+await user.save();
+

The call is syntax-sugar for Repository#save and takes the same arguments (except the model).

Or, saving locally (i.e. remote: false) with a sync API:

final user = User(id: 1, name: 'Frank');
+user.saveLocal();
+

delete

final user = await repository.findOne(1);
+await user.delete();
+

It is syntax-sugar for Repository#delete and takes the same arguments (except the model).

Or, deleting locally (i.e. remote: false) with a sync API:

final user = User(id: 1, name: 'Frank');
+user.deleteLocal();
+

find

final updatedUser = await user.find();
+

It’s syntax-sugar for Repository#findOne and takes the same arguments (except the model/ID).

Or, reloading locally (i.e. remote: false) with a sync API:

final user = User(id: 1, name: 'Frank');
+final user2 = user.reloadLocal();
+

withKeyOf

Used whenever we need to transfer identity to a model without identity (that is, without an ID).

final user = User(id: 1, 'Parker');
+final user2 = user.copyWith(name: 'Frank').withKeyOf(user);
+

id will still be null but saving and retreiving will work:

await user2.save(remote: false);
+final user3 = await user2.find();
+// user3.id == 1
+

Any Dart file that wants to use these extensions must import the library.

import 'package:flutter_data/flutter_data.dart';
+

VSCode protip! Type Command + . over the missing method and choose to import!

You can also disable them by hiding the extension:

import 'package:flutter_data/flutter_data.dart' hide DataModelExtension;
+

Polymorphic models

An example where Staff and Customer are both Users:

abstract class User<T extends User<T>> extends DataModel<T> {
+  final String id;
+  final String name;
+  User({this.id, this.name});
+}
+
+@JsonSerializable()
+@DataRepository([JSONAPIAdapter, BaseAdapter])
+class Customer extends User<Customer> {
+  final String abc;
+  Customer({String id, String name, this.abc}) : super(id: id, name: name);
+}
+
+@JsonSerializable()
+@DataRepository([JSONAPIAdapter, BaseAdapter])
+class Staff extends User<Staff> {
+  final String xyz;
+  Staff({String id, String name, this.xyz}) : super(id: id, name: name);
+}
+

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/offline/index.html b/docs/offline/index.html new file mode 100644 index 0000000..9c6a41c --- /dev/null +++ b/docs/offline/index.html @@ -0,0 +1,30 @@ +Offline - Flutter Data

Offline

You can do this in your Scaffold

child: ref.watch(initializerProvider).when(
+   error: (error, _) => Text(error.toString()),
+   loading: () => const CircularProgressIndicator(),
+   data: (_) => Text('App boot is ready, replace me with main UI widget'),
+  ),
+),
+

Then define your initializer where you initialize any number of services needed to display the main widget of your UI:

final initializerProvider = FutureProvider<void>((ref) async {
+  // initialize FD
+  await ref.container.refresh(repositoryInitializerProvider.future);
+  
+  // initialize other services
+  
+  // retry offline events
+  final _sub = ref.listen(offlineRetryProvider, (_, __) {});
+  
+  // close offline retry subscription
+  ref.onDispose(() {
+    _sub.close();
+  });
+});
+

You could also place this offline retry logic in some more specific place (for example when a user logs in, and close the sub when the user logs out).

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/quickstart/index.html b/docs/quickstart/index.html new file mode 100644 index 0000000..1f4d6a9 --- /dev/null +++ b/docs/quickstart/index.html @@ -0,0 +1,92 @@ +Quickstart - Flutter Data

Quickstart

Add flutter_data and dependencies to your pubspec.yaml file:

dependencies:
+  flutter:
+    sdk: flutter
+
+  flutter_data: ^1.5.6
+
+  # Highly RECOMMENDED (but not required) packages
+  path_provider: ^2.0.11
+  json_annotation: ^4.7.0
+  hooks_riverpod: ^2.1.1
+
+dev_dependencies:
+  build_runner: ^2.2.0 # REQUIRED!
+
+  # Highly RECOMMENDED (but not required) packages
+  json_serializable: ^6.4.1
+

Flutter Data doesn’t require any library besides build_runner for code generation.

However, json_serializable and path_provider are very convenient so they are recommended.

The latest flutter_data should be 1.5.6. Please check for all packages latest stable versions before copy-pasting dependencies.

On Riverpod

This package is developed for Riverpod, specifically Riverpod 2.x Hooks. Other libraries such as Provider or GetIt might work but there are no guarantees.

Basic configuration 🔧

Make your models extend DataModel<T>, override id and annotate them with @DataRepository().

import 'package:flutter_data/flutter_data.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'task.g.dart';
+
+@JsonSerializable()
+@DataRepository([])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+}
+

@DataRepository() takes a list of adapters.

Adapters are Dart mixins used to customize the framework’s behavior, ranging from the very basic to the extremely powerful. They are applied on Flutter Data’s RemoteAdapter<T> base class.

Let’s start by the most typical configuration to access a remote API, the base URL.

mixin JsonServerAdapter<T extends DataModel<T>> on RemoteAdapter<T> {
+  @override
+  String get baseUrl => 'https://my-json-server.typicode.com/flutterdata/demo/';
+}
+

Next, we’ll pass it to the annotation:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class Task extends DataModel<Task> {
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+}
+

Notice two things about our model above:

  • We used int? to represent the actual type of the id identifier field as it is null when new (it could have been a String too)
  • The fromJson and toJson functions were skipped as they are not required (Flutter Data will automatically use _$TaskFromJson and _$TaskToJson generated by json_serializable – but they can both be overridden)

Default serialization

Flutter Data ships with a built-in serializer/deserializer for classic JSON.

A Task instance in JSON would look like:

{
+  "id": 1,
+  "title": "Finish this documentation for once",
+  "completed": false,
+  "userId": 1
+}
+

We are now ready to run a build:

flutter pub run build_runner build
+

Flutter Data auto-generated a Repository class for Task.

It also generated a Dart library at main.data.dart which makes Flutter Data initialization effortless. It’s out-of-the-box compatible with Riverpod.

Trouble generating code? See here.

Here is how to make it work with Provider and GetIt.

Next step is to configure local storage and initialize the framework:

import 'package:flutter/material.dart';
+import 'package:flutter_data/flutter_data.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:tutorial/main.data.dart';
+
+void main() {
+  runApp(
+    ProviderScope(
+      child: TasksApp(),
+      overrides: [configureRepositoryLocalStorage()],
+    ),
+  );
+}
+
+class TasksApp extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return MaterialApp(
+      home: Scaffold(
+        body: Center(
+          child: ref.watch(repositoryInitializerProvider).when(
+                error: (error, _) => Text(error.toString()),
+                loading: () => const CircularProgressIndicator(),
+                data: (_) => Text('Hello from Flutter Data ${ref.tasks}!'),
+              ),
+        ),
+      ),
+      debugShowCheckedModeBanner: false,
+    );
+  }
+}
+

Once the data callback is invoked, Flutter Data is ready and the Task repository can be accessed via ref.tasks!

The configureRepositoryLocalStorage setup function has several optional arguments.

If you do not have path_provider as a dependency you will have to supply baseDirFn (a function that returns a base directory for local storage).

For more information see initialization.

Prefer a setup example? Here’s the sample setup app with support for Riverpod, Provider and get_it.

➡ Continue with the tutorial for a Tasks app or learn more about Repositories

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/relationships/index.html b/docs/relationships/index.html new file mode 100644 index 0000000..d77f01d --- /dev/null +++ b/docs/relationships/index.html @@ -0,0 +1,114 @@ +Relationships - Flutter Data

Relationships

Flutter Data features an advanced relationship mapping system.

Use:

  • HasMany<T> for to-many relationships
  • BelongsTo<T> for to-one relationships

As an example, a User has many Tasks:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class User extends DataModel<User> {
+  @override
+  final int? id;
+  final String name;
+  final HasMany<Task>? tasks;
+
+  User({this.id, required this.name, this.tasks});
+}
+

and a Task belongs to a User:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+  final BelongsTo<User>? user;
+
+  Task({this.id, required this.title, this.completed = false, this.user});
+}
+

As long as the API responds correctly with relationship data (for example a User resource with a collection of Task models – or just IDs if models are already present in local storage) we can expect the following to work:

final user = await repository.findOne(1, params: {'_embed': 'tasks'});
+final task = user!.tasks!.first;
+
+print(task.title); // write Flutter Data docs
+print(task.user!.value!.name); // Frank
+
+// or
+
+final house = House(address: '123 Main Rd');
+final family = Family(surname: 'Assange', house: BelongsTo(house));
+
+print(family.house.value.families.first.surname);  // Free Assange
+

We can infinitely navigate the relationship graph as it’s based on a reactive graph data structure (GraphNotifier).

Defaults

For relationships to work they must not be null.

final task = Task(title: 'do 1', user: BelongsTo());
+

If we don’t want to supply a new relationship object like above, we may provide defaults like so:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+  late final BelongsTo<User> user;
+
+  Task({this.id, required this.title, this.completed = false, BelongsTo<User>? user}) :
+    user = user ?? BelongsTo();
+}
+

Inverses

Inverse relationships are guessed when unambiguous (one relationship of inverse type).

Not in this case, as Family has two BelongsTo<House>s:

@JsonSerializable()
+@DataRepository([])
+class Family extends DataModel<Family> {
+  @override
+  final String? id;
+  final BelongsTo<House>? cottage;
+  final BelongsTo<House>? residence;
+
+  Family({
+    this.id,
+    this.cottage,
+    this.residence,
+  });
+}
+
+@JsonSerializable()
+@DataRepository([])
+class House extends DataModel<House> {
+  @override
+  final String? id;
+  final BelongsTo<Family>? owner;
+
+  House({
+    this.id,
+    BelongsTo<Family>? owner,
+  }) : owner = owner ?? BelongsTo();
+

If you wish to disambuiguate or to be explicit, annotate your relationship in the House model:

@DataRelationship(inverse: 'residence')
+final BelongsTo<Family>? owner;
+

Here’s another example, a tree structure using custom inverses and Freezed:

@freezed
+@DataRepository([], remote: false)
+class Node extends DataModel<Node>, _$Node {
+  Node._();
+  factory Node(
+      {int? id,
+      String? name,
+      @DataRelationship(inverse: 'children') BelongsTo<Node>? parent,
+      @DataRelationship(inverse: 'parent') HasMany<Node>? children}) = _Node;
+  factory Node.fromJson(Map<String, dynamic> json) => _$NodeFromJson(json);
+}
+

Remove a relationship

Given a Post with many Comments we want to remove:

final postWithNoComments = post.copyWith(comments: HasMany.remove()).was(post);
+

Works with both HasMany and BelongsTo.

Removing a relationship does not delete its linked resources (the actual comments in this case).

Disable relationship serialization

In order to keep the relationship working but avoid persisting it, use:

@DataRelationship(serialize: false)
+final BelongsTo<Post>? post;
+

Self-referential relationships

// in Post
+@DataRelationship(serialize: false)
+late BelongsTo<Post> post = asBelongsTo;
+

Relationship extensions

A User with Tasks could be created like this:

final t1 = Task(title: 'do 1');
+final t2 = Task(title: 'do 2');
+final user = User(name: 'Frank', tasks: HasMany({t1, t2}));
+
+// or using an extension on Set<DataModel>
+
+final user = User(name: 'Frank', tasks: {t1, t2}.asHasMany);
+

or a Task with User:

final user = User(name: 'Frank');
+final task = Task(title: 'do 1', user: BelongsTo(user));
+
+// or using an extension on DataModel
+
+final task = Task(title: 'do 1', user: user.asBelongsTo);
+

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/docs/repositories/index.html b/docs/repositories/index.html new file mode 100644 index 0000000..54ab2f2 --- /dev/null +++ b/docs/repositories/index.html @@ -0,0 +1,202 @@ +Repositories - Flutter Data

Repositories

Flutter Data is organized around the concept of models which are data classes extending DataModel.

@DataRepository([TaskAdapter])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+
+  // ...
+}
+

When annotated with @DataRepository (and adapters as arguments, as we’ll see later) a model gets its own fully-fledged repository.

Repository is the API used to interact with models, whether local or remote.

Assuming a Task model and its corresponding Repository<Task>, let’s see how to retrieve such resources from an API.

Finders

findAll

Using ref.tasks (short for ref.watch(tasksRepositoryProvider)) to obtain a repository we can find all resources in the collection.

Repository<Task> repository = ref.tasks;
+final tasks = await repository.findAll();
+
+// GET http://base.url/tasks
+

This async call triggered a request to GET http://base.url/tasks.

Understanding the magic ✨

How exactly does Flutter Data resolve the http://base.url/tasks URL?

Flutter Data adapters define functions and getters such as urlForFindAll, baseUrl and type among many others.

In this case, findAll will look up information in baseUrl and urlForFindAll (which defaults to type, and type defaults to tasks).

Result? http://base.url/tasks.

And, how exactly does Flutter Data instantiate Task models?

Flutter Data ships with a built-in serializer/deserializer for classic JSON. It means that the default serialized form of a Task instance looks like:

{
+  "id": 1,
+  "title": "delectus aut autem",
+  "completed": false
+}
+

Of course, this too can be overridden like the JSON API Adapter does.

Method signature:

Future<List<T>?> findAll({
+  bool? remote,
+  bool? background,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  bool? syncLocal,
+  OnSuccessAll<T>? onSuccess,
+  OnErrorAll<T>? onError,
+  DataRequestLabel? label,
+});
+

Further information on the remote, background, params, headers, onSuccess, onError and label arguments available in common arguments below.

The syncLocal argument instructs local storage to synchronize the exact resources returned from the remote source (for example, to reflect server-side deletions).

final tasks = await ref.tasks.findAll(syncLocal: true);
+

Consider this example:

If a first call to findAll returns data for task IDs 1, 2, 3 and a second call updated data for 2, 3, 4 you will end up in your local storage with: 1, 2 (updated), 3 (updated) and 4.

Passing syncLocal: true to the second call will leave the local storage state with 2, 3 and 4.

📚 See API docs

findOne

Finds a resource by ID and saves it in local storage.

final task = await ref.tasks.findOne(1);
+
+// GET http://base.url/tasks/1
+

Similar to what’s shown above in findAll, Flutter Data resolves the URL by using the urlForFindOne function. We can override this in an adapter.

For example, use path /tasks/something/1:

mixin TaskURLAdapter on RemoteAdapter<Task> {
+  @override
+  String urlForFindOne(id, params) => '$type/something/$id';
+}
+
+// would result in GET http://base.url/tasks/something/1
+

It can also take a T with an ID:

final task = await ref.tasks.findOne(anotherTaskWithId3);
+
+// GET http://base.url/tasks/3
+

Method signature:

Future<T?> findOne(
+  Object id, {
+  bool? remote,
+  bool? background,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  OnSuccessOne<T>? onSuccess,
+  OnErrorOne<T>? onError,
+  DataRequestLabel? label,
+});
+

The remote, background, params, headers, onSuccess, onError and label arguments are detailed in common arguments below.

📚 See API docs

Save and delete

save

Persists a model to local storage and remote.

final savedTask = await repository.save(task);
+

Want to use the PUT verb instead of PATCH? Use this adapter:

mixin TaskURLAdapter on RemoteAdapter<Task> {
+  @override
+  String methodForSave(id, params) => id != null ? DataRequestMethod.PUT : DataRequestMethod.POST;
+}
+

Method signature:

Future<T> save(
+  T model, {
+  bool? remote,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  OnSuccessOne<T>? onSuccess,
+  OnErrorOne<T>? onError,
+  DataRequestLabel? label,
+});
+

The remote, params, headers, onSuccess, onError and label arguments are detailed in common arguments below.

📚 See API docs

delete

Deletes a model from local storage and sends a DELETE HTTP request.

await repository.delete(model);
+

Method signature:

Future<T?> delete(
+  Object model, {
+  bool? remote,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  OnSuccessOne<T>? onSuccess,
+  OnErrorOne<T>? onError,
+  DataRequestLabel? label,
+});
+

The remote, params, headers, onSuccess, onError and label arguments are detailed in common arguments below.

📚 See API docs

Watchers

DataState is a class that holds state related to resource fetching and is practical in UI applications. It is returned in watchAll and watchOne.

class DataState<T> with EquatableMixin {
+  T model;
+  bool isLoading;
+  DataException? exception;
+  StackTrace? stackTrace;
+  // ...
+}
+

It’s typically used in a widget’s build method like:

class MyApp extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll();
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    if (state.hasException) {
+      return ErrorScreen(state.exception, state.stackTrace);
+    }
+    return ListView(
+      children: [
+        for (final task in state.model)
+          Text(task.title),
+    // ...
+  }
+}
+

Why not used a Freezed union instead?

Because without forcing to branch, DataState easily allows rebuilding widgets when multiple substates happen simultaneously – a very common pattern. The tradeoff is having to remember to check for the loading and error substates.

watchAll

Watches all models of a given type in local storage (through ref.watch and watchAllNotifier).

For updates to any model of type Task to prompt a rebuild we can use:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll();
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    // use state.model which is a List<Task>
+  }
+);
+

By default when first rendered it triggers a background findAll call with remote, params, headers, syncLocal and label arguments. See common arguments.

Method signature:

DataState<List<T>?> watchAll({
+  bool? remote,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  bool? syncLocal,
+  String? finder,
+  DataRequestLabel? label,
+});
+

But this can easily be overridden. Any method in the adapter with the exact findAll method signature and annotated with @DataFinder() will be available to supply to the finder argument as a string (method name).

Pass remote: false to prevent any remote fetch at all.

Note: Both watchAllProvider and watchAllNotifier are also available.

📚 See API docs

watchOne

Watches a model of a given type in local storage (through ref.watch and watchOneNotifier).

For updates to a given model of type Task to prompt a rebuild we can use:

class TaskScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchOne(1);
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    // use state.model which is a Task
+  }
+);
+

By default when first rendered it triggers a background findOne call with model, remote, params, headers and label arguments. See common arguments.

Method signature:

DataState<T?> watchOne(
+  Object model, {
+  bool? remote,
+  Map<String, dynamic>? params,
+  Map<String, String>? headers,
+  AlsoWatch<T>? alsoWatch,
+  String? finder,
+  DataRequestLabel? label,
+});
+

But this can easily be overridden. Any method in the adapter with the exact findOne method signature and annotated with @DataFinder() will be available to supply to the finder argument as a string (method name).

Pass remote: false to prevent any remote fetch at all.

In addition, this watcher can react to relationships via alsoWatch:

watchOneNotifier(3, alsoWatch: (task) => [task.user]);
+

This feature is extremely powerful, actually any number of relationships can be watched:

watchOneNotifier(3, alsoWatch: (task) => [task.reminders, task.user, task.user.profile, task.user.profile.comments]);
+
Note: Both watchOneProvider and watchOneNotifier are also available.

watch

This method takes a DataModel and watches its local changes.

class TaskScreen extends HookConsumerWidget {
+  final Task model;
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final task = ref.tasks.watch(model);
+    return Text(task.title);
+  }
+);
+

Note that it returns a model, not a DataState.

notifierFor

Obtain the notifier for a model. Does not trigger a remote request.

final notifier = ref.tasks.notifierFor(task);
+
+// equivalent to
+ref.tasks.watchOneNotifier(task, remote: false);
+

By default, changes will be notified immediately and trigger widget rebuilds.

For performance improvements, they can be throttled by overriding graphNotifierThrottleDurationProvider.

Common arguments

remote

Request only models in local storage:

final tasks = await ref.tasks.findAll(remote: false);
+

Argument is of type bool and the default is true.

In addition to adapters, the @DataRepository annotation can take a remote boolean argument which will make it the default for the repository.

@DataRepository([TaskAdapter], remote: false)
+class Task extends DataModel<Task> {
+  // by default no operation hits the remote endpoint
+}
+

background

Default false. Calling a finder with background = true will make it return immediately with the current value in local storage while triggering a remote request in the background. This is typically useful when using this primitive in certain adapter customizations.

params

Include query parameters (of type Map<String, dynamic>, in this case used for pagination and resource inclusion):

final tasks = await ref.tasks.findAll(
+  params: {'include': 'comments', 'page': { 'limit': 20 }}
+);
+
+// GET http://base.url/tasks?include=comments&page[limit]=20
+

headers

Include HTTP headers as a Map<String, String>:

final tasks = await ref.tasks.findAll(
+  headers: { 'Authorization': 't0k3n' }
+);
+

onSuccess

Overrides the handler for the success state, useful when requiring a specific transformation of raw response data.

await ref.tasks.save(
+  task,
+  onSuccess: (data, label, adapter) async {
+    final model = await adapter.onSuccess(data, label);
+    return model as Task;
+  },
+);
+

RemoteAdapter#onSuccess is the default (overridable, of course). It esentially boils down to calls to deserialize.

onError

Overrides the error handler:

await ref.tasks.save(
+  task,
+  onError: (error, label, adapter) async {
+    throw WrappedException(error);
+  },
+);
+

RemoteAdapter#onError is the default (overridable, of course). It essentially rethrows the error except if it is due to loss of connectivity or the remote resource was not found (HTTP 404).

label

Optional argument of type DataRequestLabel. See below.

Logging and labels

Labels are used in Flutter Data to easily track requests in logs. They carry an auto-generated requestId along with type and ID. They are provided by default and also by default finders log different events.

Some examples:

  • findAll/tasks@b5d14c
  • findOne/users#3@c4a1bb
  • findAll/tasks@b5d14c<c4a1bb

Request IDs can be nested like the last one above, where the b5d14c call happened inside c4a1bb. In other words, during the request for User with ID 3, a collection of tasks was also requested (presumably for that same user).

In the console these would be logged as:

flutter: 05:961   [findAll/tasks@b5d14c] requesting
+flutter: 05:973   [findOne/users#3@c4a1bb] requesting
+flutter: 05:974     [findAll/tasks@b5d14c<c4a1bb] requesting
+

with the nested labels properly indented.

Watchers also output useful logs in level 1 and 2.

Log levels can be set via the logLevel argument in log, and to adjust the global level:

ref.tasks.logLevel = 2;
+

In order to log custom information use something like:

final label = DataRequestLabel('save', type: 'users', id: '3');
+ref.tasks.log(label, 'testing labels');
+
+// or
+
+final nestedLabel1 = DataRequestLabel('findAll',
+  type: 'other', requestId: 'ff01b1', withParent: label);
+ref.tasks.log(label, 'testing nested labels', logLevel: 2);
+

Local adapters

See Local Adapters.

Graph

Flutter Data uses a reactive bidirectional graph data structure to keep track of relationships and key-ID mappings. Changes on this graph will trigger updates to the watchers, by default once per change but a throttle can be configured to prevent superfluous re-renders.

graphNotifierThrottleDurationProvider
+  .overrideWithValue(Duration(milliseconds: 100)),
+

Architecture overview

This is the dependency graph for an app with models User and Task:

Clients should only interact with repositories and adapters, while using the Adapter API to customize behavior.

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/static/images/apps.jpg b/images/apps.jpg similarity index 100% rename from static/images/apps.jpg rename to images/apps.jpg diff --git a/static/images/default.svg b/images/default.svg similarity index 100% rename from static/images/default.svg rename to images/default.svg diff --git a/static/images/deps.png b/images/deps.png similarity index 100% rename from static/images/deps.png rename to images/deps.png diff --git a/static/images/favicon.png b/images/favicon.png similarity index 100% rename from static/images/favicon.png rename to images/favicon.png diff --git a/static/images/fd.png b/images/fd.png similarity index 100% rename from static/images/fd.png rename to images/fd.png diff --git a/index.html b/index.html new file mode 100644 index 0000000..13b5683 --- /dev/null +++ b/index.html @@ -0,0 +1,10 @@ +Flutter Data

Persistent reactive models in Flutter. With zero boilerplate.

Flutter Data is an offline-first data framework with a +customizable REST client and powerful model relationships.

Apps using Flutter Data

Features 🚀

Repositories for all models
Built for offline-first
  • Hive-based local storage at its core
  • Failure handling & retry API
Intuitive APIs, effortless setup
  • Truly configurable and composable via Dart mixins and codegen
  • Built-in Riverpod providers for all models
Exceptional relationship support
  • Automatically synchronized, fully traversable relationship graph
  • Reactive relationships

Compatibility

Fully compatible with the tools we know and love:

FlutterAnd pure Dart, too.
Flutter WebSupported!
json_serializableFully supported (but not required)
RiverpodSupported & automatically wired up
ProviderSupported with minimal extra code
get_itSupported with minimal extra code
Classic JSON REST APIBuilt-in support!
JSON:APISupported via external adapter
FreezedSupported!
\ No newline at end of file diff --git a/index.xml b/index.xml new file mode 100644 index 0000000..a01d68c --- /dev/null +++ b/index.xml @@ -0,0 +1,103 @@ +Flutter Data/Recent content on Flutter DataHugo -- gohugo.ioen-usSat, 18 Dec 2021 17:08:28 -0300Quickstart/docs/quickstart/Mon, 01 Jan 0001 00:00:00 +0000/docs/quickstart/Add flutter_data and dependencies to your pubspec.yaml file: +dependencies: flutter: sdk: flutter flutter_data: ^1.5.6 # Highly RECOMMENDED (but not required) packages path_provider: ^2.0.11 json_annotation: ^4.7.0 hooks_riverpod: ^2.1.1 dev_dependencies: build_runner: ^2.2.0 # REQUIRED! # Highly RECOMMENDED (but not required) packages json_serializable: ^6.4.1 Flutter Data doesn&rsquo;t require any library besides build_runner for code generation. +However, json_serializable and path_provider are very convenient so they are recommended. +The latest flutter_data should be 1.5.6. Please check for all packages latest stable versions before copy-pasting dependencies.Fetching tasks/tutorial/fetching/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/fetching/Before you continue: +Make sure you went through the Quickstart and got Flutter Data up and running! +Also, you can check out the full source code for this tutorial at https://github.com/flutterdata/tutorial +We now have access to our Repository&lt;Task&gt; through ref.tasks, with an API base URL set to https://my-json-server.typicode.com/flutterdata/demo/. +Inspecting the /tasks endpoint we see: +[ { &#34;id&#34;: 1, &#34;title&#34;: &#34;Laundry 🧺&#34;, &#34;completed&#34;: false, &#34;userId&#34;: 1 }, { &#34;id&#34;: 2, &#34;title&#34;: &#34;Groceries 🛒&#34;, &#34;completed&#34;: true, &#34;userId&#34;: 1 }, { &#34;id&#34;: 3, &#34;title&#34;: &#34;Reservation at Malloys&#34;, &#34;completed&#34;: true, &#34;userId&#34;: 1 }, // .Repositories/docs/repositories/Mon, 01 Jan 0001 00:00:00 +0000/docs/repositories/Flutter Data is organized around the concept of models which are data classes extending DataModel. +@DataRepository([TaskAdapter]) class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; Task({this.id, required this.title, this.completed = false}); // ... } When annotated with @DataRepository (and adapters as arguments, as we&rsquo;ll see later) a model gets its own fully-fledged repository. +Repository is the API used to interact with models, whether local or remote.Marking tasks as done/tutorial/updating/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/updating/A read-only tasks app is not very practical! Let&rsquo;s add the ability to update the completed state and mark/unmark our tasks as done. +First, though, we&rsquo;ll extract the tasks-specific code to a separate screen named TasksScreen: +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.tasks.watchAll(); if (state.isLoading) { return CircularProgressIndicator(); } return ListView( children: [ for (final task in state.model!) Text(task.title), ], ); } } Remember to return this new widget from TasksApp:Adapters/docs/adapters/Mon, 01 Jan 0001 00:00:00 +0000/docs/adapters/Flutter Data&rsquo;s building blocks are called adapters, making it extremely customizable and composable. +Adapters are essentially Dart mixins applied on RemoteAdapter&lt;T&gt;. +Overriding basic behavior Several pieces of information are required, for example, to construct a remote findAll call on a Repository&lt;Task&gt;. The framework takes a sensible guess and makes that GET /tasks by default. +Still, a base URL is necessary and the endpoint parts should be overridable. +The way we use these adapters is by declaring them on our @DataRepository annotation in the corresponding model.Creating a new task/tutorial/creating/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/creating/First off let&rsquo;s add just one line during the initialization. This will enable very helpful logging of our tasks repository! +// ... child: ref.watch(repositoryInitializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) { // enable verbose ref.tasks.logLevel = 2; return TasksScreen(); } ), // ... When we restart we notice the following: +flutter: 34:061 [watchAll/tasks@e20025] initializing flutter: 34:100 [findAll/tasks@e2046b&lt;e20025] requesting [HTTP GET] https://my-json-server.typicode.com/flutterdata/demo/tasks flutter: 34:835 [findAll/tasks@e2046b&lt;e20025] {1, 2, 3, 4, 5} (and 5 more) fetched from remote Let&rsquo;s add a TextField, turn the input into a new Task and immediately save it.Local Adapters/docs/local-adapters/Mon, 01 Jan 0001 00:00:00 +0000/docs/local-adapters/Local adapters access the local storage, which for now is only Hive. +It is extremely rare to have to override a local adapter, so use with caution if you do. +A particularly useful use-case is data migration as LocalAdapter&rsquo;s deserialize will be called after loading raw data from the Hive box and before the json_serializable call. +Example: +mixin TaskLocalAdapter on LocalAdapter&lt;Task&gt; { @override Task deserialize(Map&lt;String, dynamic&gt; map) { // transform map from old format to new format } } Activate the use of the overridden adapter with:Models/docs/models/Mon, 20 Apr 2020 19:01:08 -0300/docs/models/Flutter Data models are data classes that extend DataModel and are annotated with @DataRepository: +@DataRepository([TaskAdapter]) @JsonSerializable() class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; Task({this.id, required this.title, this.completed = false}); } DataModel automatically registers new data classes within the framework and enforces the implementation of an id getter. Use the type that better suits you: int? and String? are the most common. +The json_serializable library is helpful but not required.Reloading the list/tutorial/reloading/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/reloading/Let&rsquo;s make the number of tasks more manageable via the _limit server query param, which in this case will return a maximum of 5 resources. +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.tasks.watchAll(params: {&#39;_limit&#39;: 5}); // ... } Hot restarting the app we should only see five tasks, but&hellip; +It&rsquo;s exactly the same as before. Why isn&rsquo;t this working? 🤔 +Turns out watchAll is wired to show all tasks in local storage.Relationships/docs/relationships/Mon, 20 Apr 2020 17:21:33 -0300/docs/relationships/Flutter Data features an advanced relationship mapping system. +Use: +HasMany&lt;T&gt; for to-many relationships BelongsTo&lt;T&gt; for to-one relationships As an example, a User has many Tasks: +@JsonSerializable() @DataRepository([JsonServerAdapter]) class User extends DataModel&lt;User&gt; { @override final int? id; final String name; final HasMany&lt;Task&gt;? tasks; User({this.id, required this.name, this.tasks}); } and a Task belongs to a User: +@JsonSerializable() @DataRepository([JsonServerAdapter]) class Task extends DataModel&lt;Task&gt; { @override final int? id; final String title; final bool completed; final BelongsTo&lt;User&gt;?Deleting tasks/tutorial/deleting/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/deleting/There&rsquo;s stuff we just don&rsquo;t want to do! +We can delete a Task on dismiss by wrapping the tile with a Dismissible and calling its delete method: +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final _newTaskController = useTextEditingController(); final state = ref.tasks.watchAll(params: {&#39;_limit&#39;: 5}, syncLocal: true); if (state.isLoading) { return CircularProgressIndicator(); } return RefreshIndicator( onRefresh: () =&gt; ref.tasks.findAll(params: {&#39;_limit&#39;: 5}, syncLocal: true), child: ListView( children: [ TextField( controller: _newTaskController, onSubmitted: (value) async { Task(title: value).Offline/docs/offline/Mon, 01 Jan 0001 00:00:00 +0000/docs/offline/You can do this in your Scaffold +child: ref.watch(initializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) =&gt; Text(&#39;App boot is ready, replace me with main UI widget&#39;), ), ), Then define your initializer where you initialize any number of services needed to display the main widget of your UI: +final initializerProvider = FutureProvider&lt;void&gt;((ref) async { // initialize FD await ref.container.refresh(repositoryInitializerProvider.future); // initialize other services // retry offline events final _sub = ref.Initialization/docs/initialization/Mon, 01 Jan 0001 00:00:00 +0000/docs/initialization/Initializing Flutter Data consists of two parts: local storage initialization and repository initialization. +The former happens when wiring up providers and the latter during widget build. +Local storage initialization Here are the configuration options with their default arguments explicit: +ProviderScope( child: MyApp(), overrides: [ configureRepositoryLocalStorage( // callback that returns a base directory where to place local storage // (if the path_provider package is present, otherwise you MUST override it) baseDirFn: () =&gt; getApplicationDocumentsDirectory().FAQ/docs/faq/Mon, 01 Jan 0001 00:00:00 +0000/docs/faq/Why are save and other methods not available on my models? DataModel extensions are syntax sugar and will only work when importing Flutter Data: +import &#39;package:flutter_data/flutter_data.dart&#39;; Errors generating code? If you have trouble with the outputs, try: +flutter pub run build_runner build --delete-conflicting-outputs VSCode users! +If after generating code you still see errors in your files, try reopening the project. This is not a Flutter Data issue. +Also make sure your dependencies are up to date:Using relationships/tutorial/relationships/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/relationships/Let&rsquo;s now slightly rethink our query. Instead of &ldquo;fetching all tasks for user 1&rdquo; we are going to &ldquo;fetch user 1 with all their tasks&rdquo;. +Flutter Data has first-class support for relationships. +First, in models/user.dart, we&rsquo;ll create the User model with a HasMany&lt;Task&gt; relationship: +import &#39;package:flutter_data/flutter_data.dart&#39;; import &#39;package:json_annotation/json_annotation.dart&#39;; import &#39;task.dart&#39;; part &#39;user.g.dart&#39;; @JsonSerializable() @DataRepository([JsonServerAdapter]) class User extends DataModel&lt;User&gt; { @override final int? id; final String name; final HasMany&lt;Task&gt; tasks; User({this.id, required this.How to Reinitialize Flutter Data/articles/how-to-reinitialize-flutter-data/Sat, 18 Dec 2021 17:08:28 -0300/articles/how-to-reinitialize-flutter-data/By calling repositoryInitializerProvider again with Riverpod&rsquo;s refresh we can reinitialize Flutter Data. +class TasksApp extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return MaterialApp( home: RefreshIndicator( onRefresh: () async =&gt; ref.container.refresh(repositoryInitializerProvider.future), child: Scaffold( body: Center( child: ref.watch(repositoryInitializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) =&gt; TasksScreen(), ), ), ), ), ); } }Nested Resources Adapter/articles/nested-resources-adapter/Thu, 09 Dec 2021 23:17:30 -0300/articles/nested-resources-adapter/Here&rsquo;s how you could access nested resources such as: /posts/1/comments +mixin NestedURLAdapter on RemoteAdapter&lt;Comment&gt; { // ... @override String urlForFindAll(params) =&gt; &#39;/posts/${params[&#39;postId&#39;]}/comments&#39;; // or even @override String urlForFindAll(params) { final postId = params[&#39;postId&#39;]; if (postId != null) { return &#39;/posts/${params[&#39;postId&#39;]}/comments&#39;; } return super.urlForFindAll(params); } } and call it like: +final comments = await commentRepository.findAll(params: {&#39;postId&#39;: post.id });Custom Deserialization Adapter/articles/custom-deserialization-adapter/Thu, 09 Dec 2021 23:15:44 -0300/articles/custom-deserialization-adapter/Example: +mixin AuthAdapter on RemoteAdapter&lt;User&gt; { Future&lt;String&gt; login(String email, String password) async { return sendRequest( baseUrl.asUri / &#39;token&#39;, method: DataRequestMethod.POST, body: json.encode({&#39;email&#39;: email, &#39;password&#39;: password}), onSuccess: (data) =&gt; data[&#39;token&#39;] as String, ); } } and use it: +final token = await userRepository.authAdapter.login(&#39;e@mail, p*ssword&#39;); Also see JSONAPIAdapter for inspiration.Intercept Logout Adapter/articles/intercept-logout-adapter/Thu, 09 Dec 2021 23:15:11 -0300/articles/intercept-logout-adapter/The global onError handler will call logout if certain conditions are met: +mixin BaseAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override FutureOr&lt;Null?&gt; onError&lt;Null&gt;(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; } }Override findAll Adapter/articles/override-findall-adapter/Thu, 09 Dec 2021 23:14:28 -0300/articles/override-findall-adapter/In this example we completely override findAll to return random models: +mixin FindAllAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override Future&lt;List&lt;T&gt;&gt; findAll({ bool? remote, Map&lt;String, dynamic&gt;? params, Map&lt;String, String&gt;? headers, bool? syncLocal, OnDataError&lt;List&lt;T&gt;&gt;? onError, }) async { // could use: super.findAll(); return _generateRandomModels&lt;T&gt;(); } }Override findOne URL Adapter/articles/override-findone-url-method/Thu, 09 Dec 2021 23:14:28 -0300/articles/override-findone-url-method/In this example we override URLs to hit finder endpoints with snake case, and for save to always use HTTP PUT: +mixin URLAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override String urlForFindAll(Map&lt;String, dynamic&gt; params) =&gt; type.snakeCase; @override String urlForFindOne(id, Map&lt;String, dynamic&gt; params) =&gt; &#39;${type.snakeCase}/$id&#39;; @override DataRequestMethod methodForSave(id, Map&lt;String, dynamic&gt; params) { return DataRequestMethod.PUT; } }Iterator Style Adapter/articles/iterator-style-adapter/Thu, 09 Dec 2021 23:13:36 -0300/articles/iterator-style-adapter/mixin AppointmentAdapter on RemoteAdapter&lt;Appointment&gt; { Future&lt;Appointment?&gt; fetchNext() async { return await sendRequest( baseUrl.asUri / type / &#39;next&#39;, onSuccess: (data) =&gt; 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();Override HTTP Client Adapter/articles/override-http-client-adapter/Thu, 09 Dec 2021 23:10:10 -0300/articles/override-http-client-adapter/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&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { 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) =&gt; true); _httpClient!.findProxy = (uri) =&gt; &#39;PROXY (proxy url)&#39;; return _ioClient!Override Default Headers and Query Parameters/articles/override-headers-query-parameters/Thu, 09 Dec 2021 23:07:40 -0300/articles/override-headers-query-parameters/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&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { final _localStorageService = read(localStorageProvider); @override String get baseUrl =&gt; &#34;http://my.remote.url:8080/&#34;; @override FutureOr&lt;Map&lt;String, String&gt;&gt; get defaultHeaders async { final token = _localStorageService.getToken(); return await super.defaultHeaders &amp; {&#39;Authorization&#39;: token}; } @override FutureOr&lt;Map&lt;String, dynamic&gt;&gt; get defaultParams async { return await super.Configure Flutter Data to Work with GetIt/articles/configure-get-it/Sun, 05 Dec 2021 23:12:05 -0300/articles/configure-get-it/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&lt;Repository&lt;Todo&gt;&gt;(); return GestureDetector( onDoubleTap: () async { print((await repository.Configure Flutter Data to Work with Provider/articles/configure-provider/Sun, 05 Dec 2021 23:12:05 -0300/articles/configure-provider/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&lt;Repository&lt;Todo&gt;?, SessionService?&gt;( lazy: false, create: (_) =&gt; SessionService(), update: (context, repository, service) { if (service != null &amp;&amp; repository != null) { return service..initialize(repository); } return service; }, ), ], child: MaterialApp( home: Scaffold( body: Center( child: Builder( builder: (context) { if (context.Override Base URL Adapter/articles/override-base-url/Fri, 03 Dec 2021 18:45:45 -0300/articles/override-base-url/Flutter Data is extended via adapters. +mixin UserURLAdapter on RemoteAdapter&lt;User&gt; { @override String get baseUrl =&gt; &#39;https://my-json-server.typicode.com/flutterdata/demo&#39;; } Need to apply the adapter to all your models? Make it generic: +mixin UserURLAdapter&lt;T extends DataModel&lt;T&gt;&gt; on RemoteAdapter&lt;T&gt; { @override String get baseUrl =&gt; &#39;https://my-json-server.typicode.com/flutterdata/demo&#39;; }Deconstructing Dart Constructors/articles/deconstructing-dart-constructors/Wed, 12 Feb 2020 13:43:48 -0500/articles/deconstructing-dart-constructors/Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories&hellip; +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:Dart Getter Shorthand to Cache Computed Properties/articles/dart-getter-cache-computed-properties/Sat, 04 Jan 2020 13:43:48 -0500/articles/dart-getter-cache-computed-properties/An elegant Dart getter shorthand used to cache computed properties: +T get foo =&gt; _foo ??= _computeFoo(); // which depends on having T _foo; T _computeFoo() =&gt; /** ... **/; It makes use of the fallback assignment operator ??=. +Check out Null-Aware Operators in Dart for a complete guide on dealing with nulls in Dart!Final vs const in Dart/articles/dart-final-const-difference/Sat, 04 Jan 2020 13:43:48 -0500/articles/dart-final-const-difference/What&rsquo;s the difference between final and const in Dart? +Easy! +Final means single-assignment. +Const means immutable. +Let&rsquo;s see an example: +final _final = [2, 3]; const _const = [2, 3]; _final = [4,5]; // ERROR: can&#39;t re-assign _final.add(6); // OK: can mutate _const.add(6); // ERROR: can&#39;t mutate Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!How To Define an Interface in Dart/articles/define-interface-dart/Sat, 04 Jan 2020 13:43:48 -0500/articles/define-interface-dart/Dart defines implicit interfaces. What does this mean? +In your app you&rsquo;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! +Want to know EVERYTHING about Dart constructors? Check out Deconstructing Dart Constructors!How to Build Widgets with an Async Method Call/articles/build-widget-with-async-method-call/Wed, 18 Dec 2019 00:00:00 +0000/articles/build-widget-with-async-method-call/You want to return a widget in a build method&hellip; +But your data comes from an async function! +class MyWidget extends StatelessWidget { @override Widget build(context) { callAsyncFetch().then((data) { return Text(data); // doesn&#39;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? 🤔Why Is My Future/Async Called Multiple Times?/articles/future-async-called-multiple-times/Wed, 18 Dec 2019 00:00:00 +0000/articles/future-async-called-multiple-times/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&lt;String&gt;( 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.The Ultimate Javascript vs Dart Syntax Guide/articles/ultimate-javascript-dart-syntax-guide/Tue, 15 Oct 2019 13:43:48 -0500/articles/ultimate-javascript-dart-syntax-guide/Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart&rsquo;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! +Variables and constants // js var dog1 = &#34;Lucy&#34;; // variable let dog2 = &#34;Milo&#34;; // block scoped variable const maleDogs = [&#34;Max&#34;, &#34;Bella&#34;]; // mutable single-assignment variable maleDogs.Checking Nulls and Null-Aware Operators in Dart/articles/checking-null-aware-operators-dart/Wed, 18 Sep 2019 00:00:00 +0000/articles/checking-null-aware-operators-dart/What is the best practice for checking nulls in Dart? +var value = maybeSomeNumber(); if (value != null) { doSomething(); } That&rsquo;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. +Default operator: ?? In other languages we can use the logical-or shortcut. If maybeSomeNumber() returns null, assign a default value of 2:How to Format a Duration as a HH:MM:SS String/articles/how-to-format-duration/Tue, 10 Sep 2019 23:43:48 -0500/articles/how-to-format-duration/The shortest, most elegant and reliable way to get HH:mm:ss from a Duration is doing: +format(Duration d) =&gt; d.toString().split(&#39;.&#39;).first.padLeft(8, &#34;0&#34;); 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) =&gt; d.How to Upgrade Flutter/articles/upgrade-flutter-sdk/Tue, 27 Aug 2019 12:43:48 -0500/articles/upgrade-flutter-sdk/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 &#39;dev&#39;... # ... And run upgrade again: +flutter upgradeMinimal Flutter Apps to Get Started/articles/minimal-hello-world-flutter-app/Tue, 30 Jul 2019 23:43:48 -0500/articles/minimal-hello-world-flutter-app/Every time I do a flutter create project I get the default &ldquo;counter&rdquo; sample app full of comments. +While it&rsquo;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. +Bare bones app // lib/main.dart import &#39;package:flutter/widgets.dart&#39;; main() =&gt; runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(context) =&gt; Center( child: Text(&#39;Hello Flutter! \ No newline at end of file diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html deleted file mode 100644 index 9ba7972..0000000 --- a/layouts/_default/baseof.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - {{ if .Params.description }} - - {{ end }} - - {{ if .Params.description }} - - {{ end }} - - {{ if .Title}} - - {{ end }} - - {{block "meta" .}}{{ end }} - - {{ if .Title }}{{ if eq .Section "tutorial"}}Tutorial:{{ end }} {{ .Title }} - {{ end }}{{ .Site.Title }} - - - - - - - - - -
-
-
-
- -
-
-
- -
- - - - - -
-
-
-
- -
- {{if ne .Section "articles" }} - - {{ end }} -
-
- {{block "main" .}}{{ end }} -
-
- {{if eq .Section "articles" }} - - {{ end }} -
- -
-
-
-
-
-
-
-

- tests - codecov - pub.dev - license -

-

- Created and maintained by frank06. -

-
- -
-
-
-
- - - - - \ No newline at end of file diff --git a/layouts/_default/list.html b/layouts/_default/list.html deleted file mode 100644 index 103610f..0000000 --- a/layouts/_default/list.html +++ /dev/null @@ -1,21 +0,0 @@ -{{ define "meta" }} - {{ if eq .Title "Docs" }} - - {{ end }} - {{ if eq .Title "Tutorials" }} - - {{ end }} -{{ end}} - -{{ define "main" }} - {{ if .Title }} -

- {{ .Title }} -

- - {{ end }} - -
- {{ .Content }} -
-{{end}} \ No newline at end of file diff --git a/layouts/_default/single.html b/layouts/_default/single.html deleted file mode 120000 index 43e3cee..0000000 --- a/layouts/_default/single.html +++ /dev/null @@ -1 +0,0 @@ -list.html \ No newline at end of file diff --git a/layouts/index.html b/layouts/index.html deleted file mode 100644 index 15165c6..0000000 --- a/layouts/index.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - - - - Flutter Data - - - - - - - - - -
-
-
-
-

- - - Flutter Data -

-
- -
- Docs - - Tutorial - - Articles -
- - -
-
-
- -
-
-
-
-

- Persistent reactive models in Flutter. With zero boilerplate. -

-

- Flutter Data is an offline-first data framework with a - customizable REST client and powerful model relationships. -

- -
- -
-
-
- -
- -
-
-

Apps using Flutter Data

-
-
- -
-
-
-
- -
-
-

Features 🚀

-
-
- Repositories for all models -
    -
  • CRUD and custom remote endpoints
  • -
  • StateNotifier watcher APIs
  • -
-
-
- Built for offline-first -
    -
  • Hive-based local storage at its core
  • -
  • Failure handling & retry API
  • -
-
-
- Intuitive APIs, effortless setup -
    -
  • Truly configurable and composable via Dart mixins and codegen
  • -
  • Built-in Riverpod providers for all models
  • -
-
-
- Exceptional relationship support -
    -
  • Automatically synchronized, fully traversable relationship graph
  • -
  • Reactive relationships
  • -
-
-
-
-
- -
-
-

Compatibility

-

Fully compatible with the tools we know and love:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FlutterAnd pure Dart, too.
Flutter WebSupported!
json_serializableFully supported (but not required) -
RiverpodSupported & automatically wired up
ProviderSupported with minimal extra code
get_itSupported with minimal extra code
Classic JSON REST APIBuilt-in support!
JSON:APISupported via external adapter
FreezedSupported!
-
-
- - - - - \ No newline at end of file diff --git a/layouts/partials/latest.html b/layouts/partials/latest.html deleted file mode 100644 index 770af36..0000000 --- a/layouts/partials/latest.html +++ /dev/null @@ -1 +0,0 @@ -1.5.6{{/* with getJSON "https://api.github.com/repos/flutterdata/flutter_data/tags" }}{{ with index . 0 }}{{- .name -}}{{end}}{{ end */}} \ No newline at end of file diff --git a/layouts/partials/magic1.md b/layouts/partials/magic1.md deleted file mode 100644 index ee7dad7..0000000 --- a/layouts/partials/magic1.md +++ /dev/null @@ -1,23 +0,0 @@ -#### Understanding the magic ✨ - -**How exactly does Flutter Data resolve the `http://base.url/tasks` URL?** - -Flutter Data [adapters](/docs/adapters) define functions and getters such as `urlForFindAll`, `baseUrl` and `type` among many others. - -In this case, `findAll` will look up information in `baseUrl` and `urlForFindAll` (which defaults to `type`, and `type` defaults to `tasks`). - -Result? `http://base.url/tasks`. - -**And, how exactly does Flutter Data instantiate `Task` models?** - -Flutter Data ships with a built-in serializer/deserializer for [classic JSON](https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html). It means that the default serialized form of a `Task` instance looks like: - -```json -{ - "id": 1, - "title": "delectus aut autem", - "completed": false -} -``` - -Of course, this too can be overridden like the [JSON API Adapter](https://github.com/flutterdata/flutter_data_json_api_adapter/) does. \ No newline at end of file diff --git a/layouts/partials/toc.html b/layouts/partials/toc.html deleted file mode 100644 index 2cec373..0000000 --- a/layouts/partials/toc.html +++ /dev/null @@ -1,6 +0,0 @@ -{{if ne (print .TableOfContents) "" }} -

- On this page -

-{{ .TableOfContents }} -{{ end }} \ No newline at end of file diff --git a/layouts/shortcodes/archive.html b/layouts/shortcodes/archive.html deleted file mode 100644 index 22fb879..0000000 --- a/layouts/shortcodes/archive.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ $pages := .Site.Pages.ByDate.Reverse }} - -
- {{ range $pages }} - {{ if eq .Type "articles" }} -
-
- - {{ if (fileExists (path.Join .File.Dir "featured-mini.jpg")) -}} - Featured - {{else }} - Featured - {{ end }} - -
-
- {{ if .Site.IsServer }} - {{ if eq .Draft true }}[DRAFT]{{ end }} - {{ end }} - {{ .Title }} -
-
-
-
- {{ end }} - {{ end }} -
\ No newline at end of file diff --git a/layouts/shortcodes/contact.html b/layouts/shortcodes/contact.html deleted file mode 100644 index 8a859c6..0000000 --- a/layouts/shortcodes/contact.html +++ /dev/null @@ -1,15 +0,0 @@ - \ No newline at end of file diff --git a/layouts/shortcodes/dartpad.html b/layouts/shortcodes/dartpad.html deleted file mode 100644 index 58f710c..0000000 --- a/layouts/shortcodes/dartpad.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ if .Site.IsServer }} -

(here a dartpad)

-{{ else }} -
- -
-{{ end }} \ No newline at end of file diff --git a/layouts/shortcodes/internal.html b/layouts/shortcodes/internal.html deleted file mode 100644 index 681b0a8..0000000 --- a/layouts/shortcodes/internal.html +++ /dev/null @@ -1,8 +0,0 @@ -{{ if .Site.IsServer }} -
-

Internal notes

-
- {{ .Inner | markdownify }} -
-
-{{ end }} diff --git a/layouts/shortcodes/iphone.html b/layouts/shortcodes/iphone.html deleted file mode 100644 index f4e05ec..0000000 --- a/layouts/shortcodes/iphone.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
-
-
-
- -
-
-
-
-
\ No newline at end of file diff --git a/layouts/shortcodes/latest.html b/layouts/shortcodes/latest.html deleted file mode 100644 index 37bcca0..0000000 --- a/layouts/shortcodes/latest.html +++ /dev/null @@ -1 +0,0 @@ -{{- partial "latest" -}} \ No newline at end of file diff --git a/layouts/shortcodes/notice.html b/layouts/shortcodes/notice.html deleted file mode 100644 index 4f86592..0000000 --- a/layouts/shortcodes/notice.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ .Inner | markdownify }} -
\ No newline at end of file diff --git a/layouts/shortcodes/partial.html b/layouts/shortcodes/partial.html deleted file mode 100644 index 0291490..0000000 --- a/layouts/shortcodes/partial.html +++ /dev/null @@ -1 +0,0 @@ -{{ partial (.Get 0) }} \ No newline at end of file diff --git a/static/main.css b/main.css similarity index 100% rename from static/main.css rename to main.css diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 00a80e7..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2007 +0,0 @@ -{ - "name": "docs", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "tailwindcss": "^3.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", - "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", - "dependencies": { - "@babel/highlight": "^7.16.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", - "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==" - }, - "node_modules/autoprefixer": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.0.tgz", - "integrity": "sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==", - "peer": true, - "dependencies": { - "browserslist": "^4.17.5", - "caniuse-lite": "^1.0.30001272", - "fraction.js": "^4.1.1", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.1.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001368", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001368.tgz", - "integrity": "sha512-wgfRYa9DenEomLG/SdWgQxpIyvdtH3NW8Vq+tB6AwR9e56iOIcu1im5F/wNdDf04XlKHXqIx4N8Jo0PemeBenQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ], - "peer": true - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "node_modules/detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "dependencies": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.24.tgz", - "integrity": "sha512-erwx5r69B/WFfFuF2jcNN0817BfDBdC4765kQ6WltOMuwsimlQo3JTEq0Cle+wpHralwdeX3OfAtw/mHxPK0Wg==", - "peer": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz", - "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==", - "peer": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/import-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", - "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", - "dependencies": { - "import-from": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-from/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "peer": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", - "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", - "dependencies": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", - "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", - "dependencies": { - "camelcase-css": "^2.0.1", - "postcss": "^8.1.6" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", - "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", - "dependencies": { - "import-cwd": "^3.0.0", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dependencies": { - "postcss-selector-parser": "^6.0.6" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.7.tgz", - "integrity": "sha512-U+b/Deoi4I/UmE6KOVPpnhS7I7AYdKbhGcat+qTQ27gycvaACvNEw11ba6RrkwVmDVRW7sigWgLj4/KbbJjeDA==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tailwindcss": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.7.tgz", - "integrity": "sha512-rZdKNHtC64jcQncLoWOuCzj4lQDTAgLtgK3WmQS88tTdpHh9OwLqULTQxI3tw9AMJsqSpCKlmcjW/8CSnni6zQ==", - "dependencies": { - "arg": "^5.0.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "color-name": "^1.1.4", - "cosmiconfig": "^7.0.1", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.7", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "normalize-path": "^3.0.0", - "object-hash": "^2.2.0", - "postcss-js": "^3.0.3", - "postcss-load-config": "^3.1.0", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.7", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.20.0", - "tmp": "^0.2.1" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "autoprefixer": "^10.0.2", - "postcss": "^8.0.9" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", - "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", - "requires": { - "@babel/highlight": "^7.16.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" - }, - "@babel/highlight": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", - "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", - "requires": { - "@babel/helper-validator-identifier": "^7.15.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", - "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==" - }, - "autoprefixer": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.0.tgz", - "integrity": "sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==", - "peer": true, - "requires": { - "browserslist": "^4.17.5", - "caniuse-lite": "^1.0.30001272", - "fraction.js": "^4.1.1", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.1.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "peer": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" - }, - "caniuse-lite": { - "version": "1.0.30001368", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001368.tgz", - "integrity": "sha512-wgfRYa9DenEomLG/SdWgQxpIyvdtH3NW8Vq+tB6AwR9e56iOIcu1im5F/wNdDf04XlKHXqIx4N8Jo0PemeBenQ==", - "peer": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "requires": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - } - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "electron-to-chromium": { - "version": "1.4.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.24.tgz", - "integrity": "sha512-erwx5r69B/WFfFuF2jcNN0817BfDBdC4765kQ6WltOMuwsimlQo3JTEq0Cle+wpHralwdeX3OfAtw/mHxPK0Wg==", - "peer": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - } - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "peer": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fraction.js": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz", - "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==", - "peer": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "requires": { - "is-glob": "^4.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "import-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", - "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", - "requires": { - "import-from": "^3.0.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", - "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" - } - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==" - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "nanoid": { - "version": "3.1.30", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" - }, - "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "peer": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "peer": true - }, - "object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - }, - "postcss": { - "version": "8.4.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", - "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", - "requires": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - } - }, - "postcss-js": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", - "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", - "requires": { - "camelcase-css": "^2.0.1", - "postcss": "^8.1.6" - } - }, - "postcss-load-config": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", - "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", - "requires": { - "import-cwd": "^3.0.0", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - } - }, - "postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "requires": { - "postcss-selector-parser": "^6.0.6" - } - }, - "postcss-selector-parser": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.7.tgz", - "integrity": "sha512-U+b/Deoi4I/UmE6KOVPpnhS7I7AYdKbhGcat+qTQ27gycvaACvNEw11ba6RrkwVmDVRW7sigWgLj4/KbbJjeDA==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "tailwindcss": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.7.tgz", - "integrity": "sha512-rZdKNHtC64jcQncLoWOuCzj4lQDTAgLtgK3WmQS88tTdpHh9OwLqULTQxI3tw9AMJsqSpCKlmcjW/8CSnni6zQ==", - "requires": { - "arg": "^5.0.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "color-name": "^1.1.4", - "cosmiconfig": "^7.0.1", - "detective": "^5.2.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.7", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "normalize-path": "^3.0.0", - "object-hash": "^2.2.0", - "postcss-js": "^3.0.3", - "postcss-load-config": "^3.1.0", - "postcss-nested": "5.0.6", - "postcss-selector-parser": "^6.0.7", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.20.0", - "tmp": "^0.2.1" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index d48ced0..0000000 --- a/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "scripts": { - "dev": "NODE_ENV=development ./node_modules/tailwindcss/lib/cli.js -i ./static/tailwind.css -o ./static/main.css --jit -w", - "build": "NODE_ENV=production ./node_modules/tailwindcss/lib/cli.js -i ./static/tailwind.css -o ./static/main.css --jit --minify" - }, - "dependencies": { - "tailwindcss": "^3.0.0" - } -} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..4e1f498 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1 @@ +/docs/quickstart//tutorial/fetching//docs/repositories//tutorial/updating//docs/adapters//tutorial/creating//docs/local-adapters//docs/models/2020-04-20T19:01:08-03:00/tutorial/reloading//docs/relationships/2020-04-20T17:21:33-03:00/tutorial/deleting//docs/offline//docs/initialization//docs/faq//tutorial/relationships//articles/2021-12-18T17:08:28-03:00/2021-12-18T17:08:28-03:00/articles/how-to-reinitialize-flutter-data/2021-12-18T17:08:28-03:00/articles/nested-resources-adapter/2021-12-09T23:17:30-03:00/articles/custom-deserialization-adapter/2021-12-09T23:15:44-03:00/articles/intercept-logout-adapter/2021-12-09T23:15:11-03:00/articles/override-findall-adapter/2021-12-09T23:14:28-03:00/articles/override-findone-url-method/2021-12-09T23:14:28-03:00/articles/iterator-style-adapter/2021-12-09T23:13:36-03:00/articles/override-http-client-adapter/2021-12-09T23:10:10-03:00/articles/override-headers-query-parameters/2021-12-09T23:07:40-03:00/articles/configure-get-it/2021-12-05T23:12:05-03:00/articles/configure-provider/2021-12-05T23:12:05-03:00/articles/override-base-url/2021-12-03T18:45:45-03:00/docs/2020-04-20T19:01:08-03:00/tags/dart/2020-02-12T13:43:48-05:00/articles/deconstructing-dart-constructors/2020-02-12T13:43:48-05:00/tags/2020-02-12T13:43:48-05:00/articles/dart-getter-cache-computed-properties/2020-01-04T13:43:48-05:00/articles/dart-final-const-difference/2020-01-04T13:43:48-05:00/articles/define-interface-dart/2020-01-04T13:43:48-05:00/articles/build-widget-with-async-method-call/2019-12-18T00:00:00+00:00/articles/future-async-called-multiple-times/2019-12-18T00:00:00+00:00/tags/es6/2019-10-15T13:43:48-05:00/tags/javascript/2019-10-15T13:43:48-05:00/articles/ultimate-javascript-dart-syntax-guide/2019-10-15T13:43:48-05:00/articles/checking-null-aware-operators-dart/2019-09-18T00:00:00+00:00/articles/how-to-format-duration/2019-09-10T23:43:48-05:00/articles/upgrade-flutter-sdk/2019-08-27T12:43:48-05:00/tags/pub/2019-08-27T12:43:48-05:00/tags/vscode/2019-08-27T12:43:48-05:00/articles/minimal-hello-world-flutter-app/2019-07-30T23:43:48-05:00/categories//tutorial/ \ No newline at end of file diff --git a/tags/dart/index.html b/tags/dart/index.html new file mode 100644 index 0000000..5b6d55e --- /dev/null +++ b/tags/dart/index.html @@ -0,0 +1,9 @@ +dart - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/dart/index.xml b/tags/dart/index.xml new file mode 100644 index 0000000..e84377f --- /dev/null +++ b/tags/dart/index.xml @@ -0,0 +1,11 @@ +dart on Flutter Data/tags/dart/Recent content in dart on Flutter DataHugo -- gohugo.ioen-usWed, 12 Feb 2020 13:43:48 -0500Deconstructing Dart Constructors/articles/deconstructing-dart-constructors/Wed, 12 Feb 2020 13:43:48 -0500/articles/deconstructing-dart-constructors/Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories&hellip; +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:The Ultimate Javascript vs Dart Syntax Guide/articles/ultimate-javascript-dart-syntax-guide/Tue, 15 Oct 2019 13:43:48 -0500/articles/ultimate-javascript-dart-syntax-guide/Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart&rsquo;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! +Variables and constants // js var dog1 = &#34;Lucy&#34;; // variable let dog2 = &#34;Milo&#34;; // block scoped variable const maleDogs = [&#34;Max&#34;, &#34;Bella&#34;]; // mutable single-assignment variable maleDogs.Checking Nulls and Null-Aware Operators in Dart/articles/checking-null-aware-operators-dart/Wed, 18 Sep 2019 00:00:00 +0000/articles/checking-null-aware-operators-dart/What is the best practice for checking nulls in Dart? +var value = maybeSomeNumber(); if (value != null) { doSomething(); } That&rsquo;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. +Default operator: ?? In other languages we can use the logical-or shortcut. If maybeSomeNumber() returns null, assign a default value of 2: \ No newline at end of file diff --git a/tags/es6/index.html b/tags/es6/index.html new file mode 100644 index 0000000..0a89a68 --- /dev/null +++ b/tags/es6/index.html @@ -0,0 +1,9 @@ +es6 - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/es6/index.xml b/tags/es6/index.xml new file mode 100644 index 0000000..9b22708 --- /dev/null +++ b/tags/es6/index.xml @@ -0,0 +1,4 @@ +es6 on Flutter Data/tags/es6/Recent content in es6 on Flutter DataHugo -- gohugo.ioen-usTue, 15 Oct 2019 13:43:48 -0500The Ultimate Javascript vs Dart Syntax Guide/articles/ultimate-javascript-dart-syntax-guide/Tue, 15 Oct 2019 13:43:48 -0500/articles/ultimate-javascript-dart-syntax-guide/Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart&rsquo;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! +Variables and constants // js var dog1 = &#34;Lucy&#34;; // variable let dog2 = &#34;Milo&#34;; // block scoped variable const maleDogs = [&#34;Max&#34;, &#34;Bella&#34;]; // mutable single-assignment variable maleDogs. \ No newline at end of file diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..6f7e297 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,9 @@ +Tags - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/index.xml b/tags/index.xml new file mode 100644 index 0000000..f3050de --- /dev/null +++ b/tags/index.xml @@ -0,0 +1 @@ +Tags on Flutter Data/tags/Recent content in Tags on Flutter DataHugo -- gohugo.ioen-usWed, 12 Feb 2020 13:43:48 -0500dart/tags/dart/Wed, 12 Feb 2020 13:43:48 -0500/tags/dart/es6/tags/es6/Tue, 15 Oct 2019 13:43:48 -0500/tags/es6/javascript/tags/javascript/Tue, 15 Oct 2019 13:43:48 -0500/tags/javascript/pub/tags/pub/Tue, 27 Aug 2019 12:43:48 -0500/tags/pub/vscode/tags/vscode/Tue, 27 Aug 2019 12:43:48 -0500/tags/vscode/ \ No newline at end of file diff --git a/tags/javascript/index.html b/tags/javascript/index.html new file mode 100644 index 0000000..0dd2332 --- /dev/null +++ b/tags/javascript/index.html @@ -0,0 +1,9 @@ +javascript - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/javascript/index.xml b/tags/javascript/index.xml new file mode 100644 index 0000000..e8724d5 --- /dev/null +++ b/tags/javascript/index.xml @@ -0,0 +1,4 @@ +javascript on Flutter Data/tags/javascript/Recent content in javascript on Flutter DataHugo -- gohugo.ioen-usTue, 15 Oct 2019 13:43:48 -0500The Ultimate Javascript vs Dart Syntax Guide/articles/ultimate-javascript-dart-syntax-guide/Tue, 15 Oct 2019 13:43:48 -0500/articles/ultimate-javascript-dart-syntax-guide/Nowadays, Dart is almost only used in the context of Flutter. This guide is exclusively focused in comparing Javascript and Dart&rsquo;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! +Variables and constants // js var dog1 = &#34;Lucy&#34;; // variable let dog2 = &#34;Milo&#34;; // block scoped variable const maleDogs = [&#34;Max&#34;, &#34;Bella&#34;]; // mutable single-assignment variable maleDogs. \ No newline at end of file diff --git a/tags/pub/index.html b/tags/pub/index.html new file mode 100644 index 0000000..0321c98 --- /dev/null +++ b/tags/pub/index.html @@ -0,0 +1,9 @@ +pub - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/pub/index.xml b/tags/pub/index.xml new file mode 100644 index 0000000..540c4ac --- /dev/null +++ b/tags/pub/index.xml @@ -0,0 +1,5 @@ +pub on Flutter Data/tags/pub/Recent content in pub on Flutter DataHugo -- gohugo.ioen-usTue, 27 Aug 2019 12:43:48 -0500How to Upgrade Flutter/articles/upgrade-flutter-sdk/Tue, 27 Aug 2019 12:43:48 -0500/articles/upgrade-flutter-sdk/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 &#39;dev&#39;... # ... And run upgrade again: +flutter upgrade \ No newline at end of file diff --git a/tags/vscode/index.html b/tags/vscode/index.html new file mode 100644 index 0000000..a3c43a5 --- /dev/null +++ b/tags/vscode/index.html @@ -0,0 +1,9 @@ +vscode - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tags/vscode/index.xml b/tags/vscode/index.xml new file mode 100644 index 0000000..c3bd07c --- /dev/null +++ b/tags/vscode/index.xml @@ -0,0 +1,5 @@ +vscode on Flutter Data/tags/vscode/Recent content in vscode on Flutter DataHugo -- gohugo.ioen-usTue, 27 Aug 2019 12:43:48 -0500How to Upgrade Flutter/articles/upgrade-flutter-sdk/Tue, 27 Aug 2019 12:43:48 -0500/articles/upgrade-flutter-sdk/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 &#39;dev&#39;... # ... And run upgrade again: +flutter upgrade \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index d867ec2..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = { - content: ["./content/**/*.md", "./content/**/*.html", "./layouts/**/*.html"], - theme: { - fontFamily: { - sans: [ - "Inter", - "system-ui", - "BlinkMacSystemFont", - "-apple-system", - "sans-serif", - ], - }, - screens: { - sm: "640px", - md: "768px", - lg: "1024px", - xl: "1280px", - xxl: "1920px", - xxxl: "2048px", - }, - extend: { - colors: { - "yellow-50": "#fdfdea", - "original-gray": "#1A202C", - flutter: { - yellow: "#FFF0B1", - }, - }, - }, - }, - variants: {}, - plugins: [], -}; \ No newline at end of file diff --git a/static/tailwind.css b/tailwind.css similarity index 100% rename from static/tailwind.css rename to tailwind.css diff --git a/content/tutorial/01.png b/tutorial/01.png similarity index 100% rename from content/tutorial/01.png rename to tutorial/01.png diff --git a/content/tutorial/01b.png b/tutorial/01b.png similarity index 100% rename from content/tutorial/01b.png rename to tutorial/01b.png diff --git a/content/tutorial/01c.png b/tutorial/01c.png similarity index 100% rename from content/tutorial/01c.png rename to tutorial/01c.png diff --git a/content/tutorial/02a.png b/tutorial/02a.png similarity index 100% rename from content/tutorial/02a.png rename to tutorial/02a.png diff --git a/content/tutorial/02b.png b/tutorial/02b.png similarity index 100% rename from content/tutorial/02b.png rename to tutorial/02b.png diff --git a/content/tutorial/03.png b/tutorial/03.png similarity index 100% rename from content/tutorial/03.png rename to tutorial/03.png diff --git a/content/tutorial/04a.png b/tutorial/04a.png similarity index 100% rename from content/tutorial/04a.png rename to tutorial/04a.png diff --git a/content/tutorial/05a.png b/tutorial/05a.png similarity index 100% rename from content/tutorial/05a.png rename to tutorial/05a.png diff --git a/content/tutorial/05b.png b/tutorial/05b.png similarity index 100% rename from content/tutorial/05b.png rename to tutorial/05b.png diff --git a/content/tutorial/06.png b/tutorial/06.png similarity index 100% rename from content/tutorial/06.png rename to tutorial/06.png diff --git a/tutorial/creating/index.html b/tutorial/creating/index.html new file mode 100644 index 0000000..5b93d5e --- /dev/null +++ b/tutorial/creating/index.html @@ -0,0 +1,58 @@ +Tutorial: Creating a new task - Flutter Data

Creating a new task

First off let’s add just one line during the initialization. This will enable very helpful logging of our tasks repository!

// ...
+child: ref.watch(repositoryInitializerProvider).when(
+  error: (error, _) => Text(error.toString()),
+  loading: () => const CircularProgressIndicator(),
+  data: (_) {
+    // enable verbose
+    ref.tasks.logLevel = 2;
+    return TasksScreen();
+  }
+),
+// ...
+

When we restart we notice the following:

flutter: 34:061 [watchAll/tasks@e20025] initializing
+flutter: 34:100   [findAll/tasks@e2046b<e20025] requesting [HTTP GET] https://my-json-server.typicode.com/flutterdata/demo/tasks
+flutter: 34:835   [findAll/tasks@e2046b<e20025] {1, 2, 3, 4, 5} (and 5 more) fetched from remote
+

Let’s add a TextField, turn the input into a new Task and immediately save it.

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+    final state = ref.tasks.watchAll();
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    return ListView(
+      children: [
+        TextField(
+          controller: _newTaskController,
+          onSubmitted: (value) async {
+            Task(title: value).save();
+            _newTaskController.clear();
+          },
+        ),
+        for (final task in state.model!)
+          ListTile(
+            leading: Checkbox(
+              value: task.completed,
+              onChanged: (value) => task.toggleCompleted().save(),
+            ),
+            title: Text('${task.title} [id: ${task.id}]'),
+          ),
+      ],
+    );
+  }
+}
+

For this we need to import flutter_hooks!

Hot-reloading once again we see our TextField ready to create new tasks:

It was that easy!

You may have noticed that there was a flash with [id: null] (we didn’t supply any ID upon model creation), until the server responds with one (in this case 11) triggering an update.

Be aware that our dummy JSON backend does not actually save new resources so it will always respond with ID 11, causing a confusing situation if you keep adding tasks!

In the console:

flutter: 25:084 [watchAll/tasks@68f651] updated models
+flutter: 25:087 [save/tasks@6bb411] requesting [HTTP POST] https://my-json-server.typicode.com/flutterdata/demo/tasks
+flutter: 25:713 [save/tasks@6bb411] saved in local storage and remote
+flutter: 25:714 [watchAll/tasks@68f651] updated models
+

NEXT: Reloading the list

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/deleting/index.html b/tutorial/deleting/index.html new file mode 100644 index 0000000..756eead --- /dev/null +++ b/tutorial/deleting/index.html @@ -0,0 +1,53 @@ +Tutorial: Deleting tasks - Flutter Data

Deleting tasks

There’s stuff we just don’t want to do!

We can delete a Task on dismiss by wrapping the tile with a Dismissible and calling its delete method:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+    final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true);
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+
+    return RefreshIndicator(
+      onRefresh: () =>
+          ref.tasks.findAll(params: {'_limit': 5}, syncLocal: true),
+      child: ListView(
+        children: [
+          TextField(
+            controller: _newTaskController,
+            onSubmitted: (value) async {
+              Task(title: value).save();
+              _newTaskController.clear();
+            },
+          ),
+          for (final task in state.model)
+            Dismissible(
+              key: ValueKey(task),
+              direction: DismissDirection.endToStart,
+              onDismissed: (_) => task.delete(),
+              child: ListTile(
+                leading: Checkbox(
+                  value: task.completed,
+                  onChanged: (value) => task.toggleCompleted().save(),
+                ),
+                title: Text('${task.title} [id: ${task.id}]'),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+}
+

Hot-reload, swipe left and… they’re gone!

Remember to check out the debug console where you can find some Flutter Data activity logs like:

flutter: 25:691 [watchAll/tasks@1744b4] updated models
+flutter: 25:693 [delete/tasks#4@1936e7] requesting [HTTP DELETE] https://my-json-server.typicode.com/flutterdata/demo/tasks/4
+flutter: 26:266 [delete/tasks#4@1936e7] deleted in local storage and remote
+

NEXT: Using relationships

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/fetching/index.html b/tutorial/fetching/index.html new file mode 100644 index 0000000..d83fe86 --- /dev/null +++ b/tutorial/fetching/index.html @@ -0,0 +1,63 @@ +Tutorial: Fetching tasks - Flutter Data

Fetching tasks

Before you continue:

Make sure you went through the Quickstart and got Flutter Data up and running!

Also, you can check out the full source code for this tutorial at https://github.com/flutterdata/tutorial

We now have access to our Repository<Task> through ref.tasks, with an API base URL set to https://my-json-server.typicode.com/flutterdata/demo/.

Inspecting the /tasks endpoint we see:

[
+  {
+    "id": 1,
+    "title": "Laundry 🧺",
+    "completed": false,
+    "userId": 1
+  },
+  {
+    "id": 2,
+    "title": "Groceries 🛒",
+    "completed": true,
+    "userId": 1
+  },
+  {
+    "id": 3,
+    "title": "Reservation at Malloys",
+    "completed": true,
+    "userId": 1
+  },
+  // ...
+]
+

To bring these tasks into our app we’ll use the watchAll method. (It internally makes a remote findAll call to /tasks and keeps watching local storage for any further changes in these models.)

class TasksApp extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return MaterialApp(
+      home: Scaffold(
+        body: Center(
+          child: ref.watch(repositoryInitializerProvider).when(
+            error: (error, _) => Text(error.toString()),
+            loading: () => const CircularProgressIndicator(),
+            data: (_) {
+              final state = ref.tasks.watchAll();
+              if (state.isLoading) {
+                return CircularProgressIndicator();
+              }
+              return ListView(
+                children: [
+                  for (final task in state.model) Text(task.title),
+                ],
+              );
+            },
+          ),
+        ),
+      ),
+      debugShowCheckedModeBanner: false,
+    );
+  }
+}
+

Bam 💥!

Whoa, how did that happen?

Understanding the magic ✨

How exactly does Flutter Data resolve the http://base.url/tasks URL?

Flutter Data adapters define functions and getters such as urlForFindAll, baseUrl and type among many others.

In this case, findAll will look up information in baseUrl and urlForFindAll (which defaults to type, and type defaults to tasks).

Result? http://base.url/tasks.

And, how exactly does Flutter Data instantiate Task models?

Flutter Data ships with a built-in serializer/deserializer for classic JSON. It means that the default serialized form of a Task instance looks like:

{
+  "id": 1,
+  "title": "delectus aut autem",
+  "completed": false
+}
+

Of course, this too can be overridden like the JSON API Adapter does.

For more information see the Repository docs.

NEXT: Marking tasks as done

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/index.html b/tutorial/index.html new file mode 100644 index 0000000..eb37bb3 --- /dev/null +++ b/tutorial/index.html @@ -0,0 +1,10 @@ +Tutorial: Tutorials - Flutter Data

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/index.xml b/tutorial/index.xml new file mode 100644 index 0000000..a37d4f4 --- /dev/null +++ b/tutorial/index.xml @@ -0,0 +1,18 @@ +Tutorials on Flutter Data/tutorial/Recent content in Tutorials on Flutter DataHugo -- gohugo.ioen-usFetching tasks/tutorial/fetching/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/fetching/Before you continue: +Make sure you went through the Quickstart and got Flutter Data up and running! +Also, you can check out the full source code for this tutorial at https://github.com/flutterdata/tutorial +We now have access to our Repository&lt;Task&gt; through ref.tasks, with an API base URL set to https://my-json-server.typicode.com/flutterdata/demo/. +Inspecting the /tasks endpoint we see: +[ { &#34;id&#34;: 1, &#34;title&#34;: &#34;Laundry 🧺&#34;, &#34;completed&#34;: false, &#34;userId&#34;: 1 }, { &#34;id&#34;: 2, &#34;title&#34;: &#34;Groceries 🛒&#34;, &#34;completed&#34;: true, &#34;userId&#34;: 1 }, { &#34;id&#34;: 3, &#34;title&#34;: &#34;Reservation at Malloys&#34;, &#34;completed&#34;: true, &#34;userId&#34;: 1 }, // .Marking tasks as done/tutorial/updating/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/updating/A read-only tasks app is not very practical! Let&rsquo;s add the ability to update the completed state and mark/unmark our tasks as done. +First, though, we&rsquo;ll extract the tasks-specific code to a separate screen named TasksScreen: +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.tasks.watchAll(); if (state.isLoading) { return CircularProgressIndicator(); } return ListView( children: [ for (final task in state.model!) Text(task.title), ], ); } } Remember to return this new widget from TasksApp:Creating a new task/tutorial/creating/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/creating/First off let&rsquo;s add just one line during the initialization. This will enable very helpful logging of our tasks repository! +// ... child: ref.watch(repositoryInitializerProvider).when( error: (error, _) =&gt; Text(error.toString()), loading: () =&gt; const CircularProgressIndicator(), data: (_) { // enable verbose ref.tasks.logLevel = 2; return TasksScreen(); } ), // ... When we restart we notice the following: +flutter: 34:061 [watchAll/tasks@e20025] initializing flutter: 34:100 [findAll/tasks@e2046b&lt;e20025] requesting [HTTP GET] https://my-json-server.typicode.com/flutterdata/demo/tasks flutter: 34:835 [findAll/tasks@e2046b&lt;e20025] {1, 2, 3, 4, 5} (and 5 more) fetched from remote Let&rsquo;s add a TextField, turn the input into a new Task and immediately save it.Reloading the list/tutorial/reloading/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/reloading/Let&rsquo;s make the number of tasks more manageable via the _limit server query param, which in this case will return a maximum of 5 resources. +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.tasks.watchAll(params: {&#39;_limit&#39;: 5}); // ... } Hot restarting the app we should only see five tasks, but&hellip; +It&rsquo;s exactly the same as before. Why isn&rsquo;t this working? 🤔 +Turns out watchAll is wired to show all tasks in local storage.Deleting tasks/tutorial/deleting/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/deleting/There&rsquo;s stuff we just don&rsquo;t want to do! +We can delete a Task on dismiss by wrapping the tile with a Dismissible and calling its delete method: +class TasksScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final _newTaskController = useTextEditingController(); final state = ref.tasks.watchAll(params: {&#39;_limit&#39;: 5}, syncLocal: true); if (state.isLoading) { return CircularProgressIndicator(); } return RefreshIndicator( onRefresh: () =&gt; ref.tasks.findAll(params: {&#39;_limit&#39;: 5}, syncLocal: true), child: ListView( children: [ TextField( controller: _newTaskController, onSubmitted: (value) async { Task(title: value).Using relationships/tutorial/relationships/Mon, 01 Jan 0001 00:00:00 +0000/tutorial/relationships/Let&rsquo;s now slightly rethink our query. Instead of &ldquo;fetching all tasks for user 1&rdquo; we are going to &ldquo;fetch user 1 with all their tasks&rdquo;. +Flutter Data has first-class support for relationships. +First, in models/user.dart, we&rsquo;ll create the User model with a HasMany&lt;Task&gt; relationship: +import &#39;package:flutter_data/flutter_data.dart&#39;; import &#39;package:json_annotation/json_annotation.dart&#39;; import &#39;task.dart&#39;; part &#39;user.g.dart&#39;; @JsonSerializable() @DataRepository([JsonServerAdapter]) class User extends DataModel&lt;User&gt; { @override final int? id; final String name; final HasMany&lt;Task&gt; tasks; User({this.id, required this. \ No newline at end of file diff --git a/tutorial/relationships/index.html b/tutorial/relationships/index.html new file mode 100644 index 0000000..5b68196 --- /dev/null +++ b/tutorial/relationships/index.html @@ -0,0 +1,181 @@ +Tutorial: Using relationships - Flutter Data

Using relationships

Let’s now slightly rethink our query. Instead of “fetching all tasks for user 1” we are going to “fetch user 1 with all their tasks”.

Flutter Data has first-class support for relationships.

First, in models/user.dart, we’ll create the User model with a HasMany<Task> relationship:

import 'package:flutter_data/flutter_data.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+import 'task.dart';
+
+part 'user.g.dart';
+
+@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class User extends DataModel<User> {
+  @override
+  final int? id;
+  final String name;
+  final HasMany<Task> tasks;
+
+  User({this.id, required this.name, required this.tasks});
+}
+

Time to run code generation and get a brand-new Repository<User>:

flutter pub run build_runner build
+

Great. We are now going to issue the remote request via watchOne(), in order to list (and watch for changes of) user 1’s Task models:

  • params: {'_embed': 'tasks'}, tells the server to include this user’s tasks (which our JSON adapter knows how to deserialize)
  • alsoWatch: (user) => [user.tasks] tells the watcher to rebuild the widget any time user or its tasks are updated or deleted; any number of relationships of any depth can be watched. (For instance, alsoWatch: (user) => [user.tasks, user.tasks.comments, user.tasks.comments.owner])
class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+    final state = ref.users.watchOne(
+        1, // user ID, an integer
+        params: {'_embed': 'tasks'}, // HTTP param
+        alsoWatch: (user) => [user.tasks] // watcher
+      );
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+
+    final tasks = state.model!.tasks.toList();
+
+    return RefreshIndicator(
+      onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}),
+      child: ListView(
+        children: [
+          TextField(
+            controller: _newTaskController,
+            onSubmitted: (value) async {
+              Task(title: value).save();
+              _newTaskController.clear();
+            },
+          ),
+          for (final task in tasks)
+            Dismissible(
+              key: ValueKey(task),
+              direction: DismissDirection.endToStart,
+              onDismissed: (_) => task.delete(),
+              child: ListTile(
+                leading: Checkbox(
+                  value: task.completed,
+                  onChanged: (value) => task.toggleCompleted().save(),
+                ),
+                title: Text('${task.title} [id: ${task.id}]'),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+}
+

Import the user.dart file, reload and watch it working!

Note that tasks 4, 5 and 9 for example were not loaded as they do not belong to user 1!

This is the API response for https://my-json-server.typicode.com/flutterdata/demo/users/1?_embed=tasks that was parsed by the built-in deserialize method:

{
+  "id": 1,
+  "name": "frank06",
+  "tasks": [
+    {
+      "id": 1,
+      "title": "Laundry 🧺",
+      "completed": false,
+      "userId": 1
+    },
+    {
+      "id": 2,
+      "title": "Groceries 🛒",
+      "completed": true,
+      "userId": 1
+    },
+    {
+      "id": 3,
+      "title": "Reservation at Malloys",
+      "completed": true,
+      "userId": 1
+    },
+    {
+      "id": 7,
+      "title": "Take Amanda to birthday",
+      "completed": true,
+      "userId": 1
+    },
+    {
+      "id": 8,
+      "title": "Get new surfboard 🏄‍♀️",
+      "completed": false,
+      "userId": 1
+    },
+    {
+      "id": 10,
+      "title": "Protest tyrannical mandates 👊",
+      "completed": true,
+      "userId": 1
+    }
+  ]
+}
+

Creating a task

As it is, adding a new task will not work. Why is that?

We are creating new Task models without any User associated to them:

onSubmitted: (value) async {
+  Task(title: value).save();
+  _newTaskController.clear();
+},
+

Let’s fix this. Add a BelongsTo<User> relationship in models/task.dart and regenerate our code:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+  final BelongsTo<User> user;
+
+  Task({this.id, required this.title, this.completed = false, required this.user});
+  
+  Task toggleCompleted() {
+    return Task(id: this.id, title: this.title, user: user, completed: !this.completed)
+        .withKeyOf(this);
+  }
+}
+

Now we can provide the right user as a BelongsTo:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+    final state = ref.users.watchOne(
+        1, // user ID, an integer
+        params: {'_embed': 'tasks'}, // HTTP param
+        alsoWatch: (user) => [user.tasks] // watcher
+      );
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+
+    final user = state.model!;
+    final tasks = user.tasks.toList();
+
+    return RefreshIndicator(
+      onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}),
+      child: ListView(
+        children: [
+          TextField(
+            controller: _newTaskController,
+            onSubmitted: (value) async {
+              Task(title: value, user: BelongsTo(user)).save();
+              _newTaskController.clear();
+            },
+          ),
+          for (final task in tasks)
+            Dismissible(
+              key: ValueKey(task),
+              direction: DismissDirection.endToStart,
+              onDismissed: (_) => task.delete(),
+              child: ListTile(
+                leading: Checkbox(
+                  value: task.completed,
+                  onChanged: (value) => task.toggleCompleted().save(),
+                ),
+                title: Text('${task.title} [id: ${task.id}]'),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+}
+

And adding new tasks now works!

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/reloading/index.html b/tutorial/reloading/index.html new file mode 100644 index 0000000..11a1783 --- /dev/null +++ b/tutorial/reloading/index.html @@ -0,0 +1,80 @@ +Tutorial: Reloading the list - Flutter Data

Reloading the list

Let’s make the number of tasks more manageable via the _limit server query param, which in this case will return a maximum of 5 resources.

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll(params: {'_limit': 5});
+  // ...
+}
+

Hot restarting the app we should only see five tasks, but…

It’s exactly the same as before. Why isn’t this working? 🤔

Turns out watchAll is wired to show all tasks in local storage. If the server responds with some tasks, this won’t affect older tasks stored locally.

To fix this, watchAll takes a syncLocal argument which forces local storage to mirror exactly the resources returned from the remote source. This can be useful to reflect server-side resource deletions, too.

Let’s try this out:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true);
+  // ...
+}
+

And it works like a charm:

With a real-world API we would still see all tasks marked as done. We went back to default as our dummy JSON backend does not store data.

Another useful trick is to use clear: true on local storage configuration:

void main() {
+  runApp(
+    ProviderScope(
+      child: TasksApp(),
+      overrides: [configureRepositoryLocalStorage(clear: true)],
+    ),
+  );
+}
+

For more on initialization see here.

Replacing the manual reload

Instead of manually reloading/restarting we will now integrate RefreshIndicator. In the event handler we simply use findAll and pass the same arguments:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+    final state = ref.tasks.watchAll(params: {'_limit': 5}, syncLocal: true);
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    return RefreshIndicator(
+      onRefresh: () =>
+          ref.tasks.findAll(params: {'_limit': 5}, syncLocal: true),
+      child: ListView(
+        children: [
+          TextField(
+            controller: _newTaskController,
+            onSubmitted: (value) async {
+              Task(title: value, completed: false).save();
+              _newTaskController.clear();
+            },
+          ),
+          for (final task in state.model)
+            ListTile(
+              leading: Checkbox(
+                value: task.completed,
+                onChanged: (value) => task.toggleCompleted().save(),
+              ),
+              title: Text('${task.title} [id: ${task.id}]'),
+            ),
+        ],
+      ),
+    );
+  }
+}
+

Now simply pull to refresh!

A similar method can be used to fully re-initialize Flutter Data.

A DRY’er alternative would be getting hold of the notifier and calling reload() on it:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final _newTaskController = useTextEditingController();
+
+    final provider =
+        ref.tasks.watchAllProvider(params: {'_limit': 5}, syncLocal: true);
+    final state = ref.watch(provider);
+
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    return RefreshIndicator(
+      onRefresh: () => ref.read(provider.notifier).reload(),
+      child: ListView(
+    // ...
+

NEXT: Deleting tasks

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/tutorial/updating/index.html b/tutorial/updating/index.html new file mode 100644 index 0000000..1e01662 --- /dev/null +++ b/tutorial/updating/index.html @@ -0,0 +1,76 @@ +Tutorial: Marking tasks as done - Flutter Data

Marking tasks as done

A read-only tasks app is not very practical! Let’s add the ability to update the completed state and mark/unmark our tasks as done.

First, though, we’ll extract the tasks-specific code to a separate screen named TasksScreen:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll();
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    return ListView(
+      children: [
+        for (final task in state.model!) Text(task.title),
+      ],
+    );
+  }
+}
+

Remember to return this new widget from TasksApp:

class TasksApp extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return MaterialApp(
+      home: Scaffold(
+        body: Center(
+          child: ref.watch(repositoryInitializerProvider).when(
+                error: (error, _) => Text(error.toString()),
+                loading: () => const CircularProgressIndicator(),
+                data: (_) => TasksScreen(),
+              ),
+        ),
+      ),
+      debugShowCheckedModeBanner: false,
+    );
+  }
+}
+

Back to our TasksScreen we are going to wrap our title text widget in a ListTile prefixing it with a checkbox which, upon clicking, will toggle task completion:

class TasksScreen extends HookConsumerWidget {
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final state = ref.tasks.watchAll();
+    if (state.isLoading) {
+      return CircularProgressIndicator();
+    }
+    return ListView(
+      children: [
+        for (final task in state.model!)
+          ListTile(
+            leading: Checkbox(
+              value: task.completed,
+              onChanged: (value) => task.toggleCompleted().save(),
+            ),
+            title: Text('${task.title} [id: ${task.id}]'),
+          ),
+      ],
+    );
+  }
+}
+

If only the toggleCompleted() method existed… 😀

Since Task is immutable, we return a new Task object with the inverse boolean value of completed:

@JsonSerializable()
+@DataRepository([JsonServerAdapter])
+class Task extends DataModel<Task> {
+  @override
+  final int? id;
+  final String title;
+  final bool completed;
+
+  Task({this.id, required this.title, this.completed = false});
+
+  Task toggleCompleted() {
+    return Task(id: this.id, title: this.title, completed: !this.completed).withKeyOf(this);
+  }
+}
+

What exactly is withKeyOf(this) for?

When a new model is created, Flutter Data initializes it looking up its internal key based on its id.

This way, Task(id: 4, title: 'a') and Task(id: 4, title: 'b') are essentially two versions of the same model.

When there is no id (or potentially no id, as in the case above) we can use withKeyOf to ensure the new model is treated as an updated version of the old one.

Regenerate code, hot-reload and check all boxes…

Done! NEXT: Creating a new task

tests +codecov +pub.dev +license

Created and maintained by frank06.

\ No newline at end of file diff --git a/content/tutorial/w.gif b/tutorial/w.gif similarity index 100% rename from content/tutorial/w.gif rename to tutorial/w.gif diff --git a/content/tutorial/w1.png b/tutorial/w1.png similarity index 100% rename from content/tutorial/w1.png rename to tutorial/w1.png diff --git a/content/tutorial/w2.gif b/tutorial/w2.gif similarity index 100% rename from content/tutorial/w2.gif rename to tutorial/w2.gif diff --git a/content/tutorial/w2.png b/tutorial/w2.png similarity index 100% rename from content/tutorial/w2.png rename to tutorial/w2.png diff --git a/content/tutorial/w4.gif b/tutorial/w4.gif similarity index 100% rename from content/tutorial/w4.gif rename to tutorial/w4.gif diff --git a/content/tutorial/w4.png b/tutorial/w4.png similarity index 100% rename from content/tutorial/w4.png rename to tutorial/w4.png diff --git a/content/tutorial/w5.png b/tutorial/w5.png similarity index 100% rename from content/tutorial/w5.png rename to tutorial/w5.png diff --git a/content/tutorial/w6.png b/tutorial/w6.png similarity index 100% rename from content/tutorial/w6.png rename to tutorial/w6.png diff --git a/content/tutorial/w7.gif b/tutorial/w7.gif similarity index 100% rename from content/tutorial/w7.gif rename to tutorial/w7.gif diff --git a/content/tutorial/w8.png b/tutorial/w8.png similarity index 100% rename from content/tutorial/w8.png rename to tutorial/w8.png diff --git a/content/tutorial/w8a.png b/tutorial/w8a.png similarity index 100% rename from content/tutorial/w8a.png rename to tutorial/w8a.png