0.4.1

Repositories

Repositories are a core concept in Flutter Data.

Each model annotated with @DataRepository gets its repository.

findAll

Return a list of users:

Repository<User> repository;
final users = await repository.findAll();

// This triggers an HTTP request in
// the background to:

// GET http://base.url/users

Let’s understand the magic ✨

Where does http://base.url come from?

User must’ve had defined a @DataRepository([UserURLAdapter]) annotation:

mixin UserURLAdapter on RemoteAdapter<User> {
  @override
  String get baseUrl => 'http://base.url';
}

Where does the users path come from?

Every repository has a type field, which by default is defined as:

String type = pluralize(T.toString().toLowerCase());

And findAll resolves its URL path and HTTP method through:

String urlForFindAll(params) => '$type';
DataRequestMethod methodForFindAll(params) => DataRequestMethod.GET;

Therefore, a findAll request on a Repository<User> resolves to the users path.

Need to make it /v3/USERS?

mixin UserURLAdapter on RemoteAdapter<User> {
  // ...
  @override
  String urlForFindAll(params) => '/v3/USERS';

  // or even
  @override
  String get type => 'USERS';

  @override
  String urlForFindAll(params) => '/v3/$type';
}

Or, if we want to make it generic and use it with all our models:

mixin URLAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  // ...
  @override
  String urlForFindAll(params) => '/v3/${type.toUpperCase()}';
}

Specify query parameters (in this case used for pagination and resource inclusion):

final users1 = await repository.findAll(params: {'include': 'posts', 'page': { 'limit': 20 }});

// HTTP GET http://base.url/users?include=posts&page[limit]=20

Or headers:

final users = await repository.findAll(headers: { 'Authorization': 't0k3n' });

We can also request only for models in local storage:

final users = await repository.findAll(remote: false);

It may throw a DataException (has errors and status fields).

Accessing nested resources

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

watchAll

Return a list of models and be notified of local and remote updates:

return DataStateBuilder<List<User>>(
    notifier: () => repository.watchAll(params: {'houseId': 1, '_limit': 5}),
    builder: (context, state, notifier, _) {
      // ...
    }
);

If you need to sort make sure you make a COPY of the list! For example by calling toList():

final sortedModels = state.model.toList()..sort();

findOne

Return a user by ID:

Repository<User> repository;
final user = await repository.findOne(1);

// GET http://base.url/users/1

Takes remote, params and headers as named arguments.

Override the path to /users/something/1?

mixin URLAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  // ...
  @override
  String urlForFindOne(id, params) => '$type/something/$id';
}

It may throw a DataException (has errors and status fields).

watchOne

Return a model and be notified of local and remote updates:

return DataStateBuilder<User>(
    notifier: () => repository.watchOne(1, params: {'include': 'posts'}),
    builder: (context, state, notifier, _) {
      // ...
    }
);

save

Save a model:

await repository.save(user);

Takes remote, params and headers as named arguments.

Want to use the PUT verb instead of PATCH?

mixin URLAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  // ...
  @override
  String methodForSave(id, params) => id != null ? DataRequestMethod.PUT : DataRequestMethod.POST;
}

It may throw a DataException (has errors and status fields).

delete

Delete a model:

await repository.delete(model);

Takes remote, params and headers as named arguments.

The (overridable, of course) default is:

String urlForDelete(id, params) => '$type/$id';
DataRequestMethod methodForDelete(id, params) => DataRequestMethod.DELETE;

It may throw a DataException (has errors and status fields).

Custom REST actions

Awful APIs also supported™️!

CRUD actions have fine-grained endpoint configuration via the urlFor* functions.

Additional ad-hoc endpoints are not a rare finding in most REST APIs. Let’s see how we can implement one with the power of adapters.

Appointment with an iterator-style API

mixin AppointmentAdapter on RemoteAdapter<Appointment> {
  Future<Appointment> fetchNext() async {
    final response = await withHttpClient(
      (client) => client.get(
        '$baseUrl/$type/next',
        headers: headers,
      ),
    );
    return withResponse<T>(response, (data) {
      return deserialize(data);
    });
  }
}

All methods and getters in Repository (baseUrl, type and many more) can be used in all these adapters.

We can get hold of a http.Client() via withHttpClient. Once we have a raw response, we can parse it via withResponse. Lastly, if needed, call deserialize.

This new ad-hoc action can be consumed like this:

final Repository<Appointment> repository;
(repository as AppointmentAdapter).fetchNext();

Want to use a completely different HTTP client?

Totally possible. Check out the examples here.

Manual deserialization

mixin AuthAdapter on RemoteAdapter<User> {
  Future<String> login(String email, String password) async {
    final response = await withHttpClient(
      (client) => client.post(
        '$baseUrl/token',
        body: _serializeCredentials(user, password),
        headers: headers,
      ),
    );

    final map = json.decode(response.body);
    return map['token'] as String;
  }
}

Of course, any of these adapters can be made generic!

Entirely override findAll

mixin FindAllAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  @override
  Future<List<T>> findAll({bool remote = true, Map<String, dynamic> params, Map<String, dynamic> headers}) {
    // could use: super.findAll(remote, params, headers);
    return _generateRandomModels<T>();
  }
}

Need to override serialize/deserialize?

These are great examples to get inspiration from:

How about an interceptor?

mixin BaseAdapter<T extends DataSupport<T>> on RemoteAdapter<T> {
  // ...
  @override
  FutureOr<R> withResponse<R>(response, onSuccess) async {
    if (response.statusCode == 401) {
      final _sessionService = locator<SessionService>();
      await _sessionService.logout();
      return null;
    }
    return super.withResponse(response, onSuccess);
  }
}

For more adapters see the cookbook!