Skip to content

Latest commit

 

History

History
158 lines (123 loc) · 5.6 KB

repositories.md

File metadata and controls

158 lines (123 loc) · 5.6 KB

Repositories

A repository routes application data to and from one or many providers. Repositories should only hold repository-specific logic and not pass interpreted data to its providers (e.g. the repository does not transform a Query into a SQL statement for its SQLite provider).

Brick does not synchronize data automatically between providers. Learn about how to synchronize and reconile data between multiple providers on Synchronization.

Integrate

To use a repository seamlessly with a state management system like BLoCs without passing around context, access the repository as a singleton:

import 'package:brick_core/core.dart';
import 'package:brick_rest/brick_rest.dart';
import 'package:my_app/brick/brick.g.dart' show restModelDictionary;

// brick/repository.dart
class MyRepository extends SingleProviderRepository<RestModel> {
  MyRepository._({
    required String baseEndpoint,
  }) : super(
    RestProvider(baseEndpoint, modelDictionary: restModelDictionary),
  );
  factory MyRepository() => _singleton!;

  static MyRepository create(String baseEnpoint) {
    _singleton = MyRepository._(
      baseEndpoint: baseEndpoint,
    );
  }
}

However, the singleton is not required (such as via an InheritedWidget). Multiple repositories can also manage different data streams. Each repository should have only one type of a provider (e.g. a repository cannot have two RestProviders but it can have a RestProvider, a SqliteProvider, and a MemoryCacheProvider).

Once the app is initialized, it is recommended to immediately run #initialize. Repositories will execute setup functions (e.g. running SQL migrations) exactly once within this method:

// configure and initialize at the application's entrypoint
class BootScreenState extends State<BootScreen> {
  ...
  initState() {
    super.initState();
    // initialize only needs to be run once:
    MyRepository.create("https://api.com");
    MyRepository().initialize();
  }
}

Access

End-implementation uses (e.g. a Flutter application) should extend an abstract repository and pass arguments to super. If custom methods need to be added, they can be written in the application-specific repository and not the abstract one. Application-specific brick.g.dart are also imported:

// brick/repository.dart
import 'brick.g.dart' show migrations, restModelDictionary;
class MyRepository extends OfflineFirstRepository {
  MyRepository({
    String baseEndpoint,
  }) : super(
    migrations: migrations,
    restProvider: RestProvider(baseEndpoint, modelDictionary: restModelDictionary),
  );
}

Creating a Custom Repository

There are several principles for repositories that should be considered beyond its implementation of ModelRepository:

  • The repository only fetches data from providers
  • The repository cannot (de)serialize models with a provider
  • The repository does not preserve model states
  • Every method returns from the same provider
  • Query#action is applied when it does not exist on a query from arguments

To generate code for a custom repository, please see brick_build.

Methods

While repositories share method names with providers, they are distinct from providers in that they are synthesizers:

class MyRestAndMemoryRepository implements ModelRepository {
  get<_Model>({Query query}) async {
    // check one provider for data
    if (memoryProvider.has(query)) return memoryProvider.get<_Model>(query: query);

    // fetch data from another provider
    final restResults = await restProvider.get<_Model>(query: query);

    // ensure that the data is accessible across all providers
    restResults.forEach((r) => memoryProvider.upsert<_Model>(r));

    // now that the data is inserted, we're confident in a refetch from the provider
    // without checking for existence
    return memoryProvider.get<_Model>(query: query);
  }
}

!> When juggling multiple providers, consistently resolve with data from the same provider across all methods. When in doubt, prioritize data from a local provider:

// BAD:
get() {
  ...
  return sqliteProvider.get();
}
upsert() {
  ...
  return memoryProvider.upsert();
}

// GOOD:
get() {
  ...
  return sqliteProvider.get();
}
upsert() {
  ...
  return sqliteProvider.upsert();
}

Repositories should be the only class that can call a provider method. This enforces a consistent data stream throughout an application.

Applying Query#action

Before passing a query to a provider method, it is recommended for the repository to apply an action to a query if it doesn't otherwise exist. For example, while RestProvider#upsert accepts both new and updated instances, its invoking repository has separate methods for update and insert:

class MyRepository {
  insert<_Model>(_Model instance, {Query query}) {
    query = (query ?? Query()).copyWith(action: QueryAction.insert);
    await restProvider.upsert<_Model>(instance, query: query);
  }

  update(_Model instance, {Query query}) {
    query = (query ?? Query()).copyWith(action: QueryAction.update);
    await restProvider.upsert<_Model>(instance, query: query);
  }
}

class RestProvider {
  upsert<_Model>(_Model instance, {Query query}) {
    final headers = {};
    if (query.action.update) headers['method'] = "PUT";
    if (query.action.insert) headers['method'] = "POST";
  }
}

FAQ

How can I (de)serialize a model with a repository?

Repositories do not have model dictionaries because they do not interpret sources. Providers are the only classes with access to adapters.