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.
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.
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.
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.
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.
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.
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.
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.
watchAllProvider and watchAllNotifier are also available.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]);
watchOneProvider and watchOneNotifier are also available.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.
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.
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
}
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.
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
Include HTTP headers as a Map<String, String>:
final tasks = await ref.tasks.findAll(
  headers: { 'Authorization': 't0k3n' }
);
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.
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).
Optional argument of type DataRequestLabel. See below.
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@b5d14cfindOne/users#3@c4a1bbfindAll/tasks@b5d14c<c4a1bbRequest 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);
See Local Adapters.
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)),
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.