Brick Documentation

repository·main·Indexed 19 days ago

https://github.com/getdutchie/brick

An extensible query interface for Dart applications designed for offline-first development. Brick provides a single access point for data from sources including REST, GraphQL, and Supabase, while automatically handling complex serialization and migrations. It utilizes code generation via brick_build and build_runner to create adapters and model dictionaries.

Tokens
56.7K
Snippets
164
Records
219
Agent score
66%

What's inside Brick

  1. What is Brick?

    main
    Brick is an extensible query interface for Dart applications designed to represent business data regardless of its source. It is an all-in-one solution inspired by ActiveRecord and Ecto, specifically built to enable offline-first capabilities. It allows developers to focus on application logic by abstracting away the complexities of where data lives and how it is retrieved.
  2. When to use Brick

    main

    Brick is most effective when your application requires:

    • Offline access: Enabling the app to function without a constant network connection.
    • Complex Serialization: Automatically handling the serialization and deserialization logic between your application and various external data sources.
    • Consistent Data Access: Using a single access point and an opinionated DSL to maintain consistency when pushing and pulling data.
    • Automated Migrations: Leveraging intelligently-generated migrations for your data schema.
    • Structured Querying: Using a legible querying interface to interact with data.
  3. Understand the role of Adapters in data translation

    main

    Adapters are responsible for translating raw data from a source into a model (or a list of models). They are discovered via a modelDictionary (a hash table connecting models to their respective adapters).

    Adapters consist of:

    1. Serdes (Serialize/Deserialize) code: The logic to convert data formats.
    2. Custom translation maps: Such as fieldsToSqliteColumns or restEndpoint.

    Adapters are generated using brick_build.

    // Conceptual view of adapter usage inside a provider
    Future<_Model> get<_Model extends RestModel>({Query query}) async {
      final adapter = modelDictionary.forAdapter[_Model];
      final resp = await fetchRawData();
      return response.map((r) => adapter.fromRest(r));
    }
  4. Avoid using Memory Cache Provider with parent models containing child associations

    main
    When using the MemoryCacheProvider, avoid using it for parent models that have child associations. This is because child models may be updated in the future without notifying the parent model, which can lead to stale data in the memory cache.
  5. Why are both annotations and `extends` required for models?

    main

    In Brick, models require both a class-level annotation and a type extension (e.g., extends OfflineFirstModel):

    1. Annotations: Required to build the generated files, such as adapters and migrations.
    2. Type Extension (extends): Used by the repository's type system to identify and manage the model.
  6. Configure OfflineFirstUpsertPolicy

    main

    When saving or updating data (upserting), use OfflineFirstUpsertPolicy to manage the synchronization between the local database and the remote provider.

    • optimisticLocal (default): Saves results to the local database immediately before waiting for the remote provider to respond.
    • requireRemote: Saves results to the local database only after the remote provider responds successfully. If the remote request fails, the local database is not updated.
    • localOnly: Saves results to the local database only and does not send any request to the remote provider.
    // Example usage pattern
    await repository.upsert<User>(user: myUser, policy: OfflineFirstUpsertPolicy.requireRemote);
  7. Handle Data Associations

    main

    Brick supports associations between models and handles automatic (de)serialization of child models. You can query parent models based on properties of their associated children.

    Example:

    class Hat extends OfflineFirstWithRestModel {
      final String color;
      Hat({this.color});
    }
    
    class User extends OfflineFirstWithRestModel {
      final List<Hat> hats;
      User({this.hats});
    }
    
    // Querying users who have a specific type of hat
    final query = Query.where('hats', Where('color').isExactly('brown'));
    final usersWithBrownHats = await repository.get<User>(query: query);
    final query = Query.where('hats', Where('color').isExactly('brown'));
    final usersWithBrownHats = repository.get<User>(query: query);
  8. Important: Do not import brick_rest_generators into end applications

    main
    The brick_rest_generators package is strictly a build-time dependency. It is designed to be used within Brick build domains to provide (de)serialization logic for RestProvider. Because it does not contain any generated code itself, importing it into an end-user application will not provide the necessary functionality and is not the intended usage pattern.
  9. Use OfflineFirstWithGraphqlRepository for GraphQL integration

    main

    The OfflineFirstWithGraphqlRepository simplifies integrating GraphQL with an OfflineFirstRepository. It includes a serial queue that tracks GraphQL mutations in a separate SQLite database. Mutations are only removed from the queue once a response is successfully returned from the host, allowing for seamless operation when a device loses internet connectivity.

    This domain uses the same configurations and annotations as the standard OfflineFirst domain.

    You can override default behavior on a per-request basis using the policy: parameter. This is supported for the following methods:

    • delete
    • get
    • getBatched
    • subscribe
    • upsert

    Example policy usage: get<Person>(policy: OfflineFirstUpsertPolicy.localOnly)

    get<Person>(policy: OfflineFirstUpsertPolicy.localOnly)
  10. Brick vs State Management

    main

    Brick is a data store management library, not a state management library.

    While Brick manages the routing, persistence, and synchronization of data between different sources, it does not dictate how that data is rendered in the UI. You can continue to use state management libraries like BLoC, Scoped Model, or Redux alongside Brick. The typical pattern is for a state manager to request data from a Brick Repository, receive the data, and then deliver it to the UI components.

  11. How the code generation process works

    main

    The brick_offline_first_with_supabase_build package automates the creation of adapters and database schemas through several steps:

    1. Discovery: Finds classes annotated with @ConnectOfflineFirstWithSupabase that extend OfflineFirstWithSupabaseModel.
    2. Field Expansion: Extracts fields and creates SupabaseFields and SqliteFields instances to ensure fields are processed in declaration order.
    3. Generator Creation: Creates SupabaseSerialize, SupabaseDeserialize, SqliteSerialize, and SqliteDeserialize generators.
    4. Transformation Logic: The generators produce code to transform field types (e.g., converting List<Future<int>> into a format suitable for Supabase).
    5. Adapter Generation: Wraps the transformation logic into functions like MODELToSupabase() and saves the result to adapters/MODELNAME.g.dart.
    6. Model Dictionary: A central brick.g.dart is generated containing a model dictionary for all annotated classes.
    7. Schema & Migrations: The SqliteSchemaGenerator detects changes in your data structure. If differences are found, it generates a new migration file (e.g., db/migrations/VERSION_migration.dart) and updates db/schema.g.dart.
  12. Configure OfflineFirstDeletePolicy

    main

    When deleting data, you can specify a OfflineFirstDeletePolicy to control how the local database and remote provider interact. Use these policies to decide whether to prioritize immediate local UI updates or ensure remote synchronization before local deletion.

    • optimisticLocal (default): Deletes local results immediately before waiting for the remote provider to respond.
    • requireRemote: Deletes local results only after the remote provider responds successfully. If the remote request fails, local results are preserved.
    • localOnly: Deletes local results only and does not attempt to contact the remote provider.
    // Example usage pattern
    await repository.delete<User>(id: '123', policy: OfflineFirstDeletePolicy.requireRemote);