Brick Documentation
repository·main·Indexed 19 days ago
https://github.com/getdutchie/brickAn 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.
What's inside Brick
- 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.
When to use Brick
mainBrick 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.
Understand the role of Adapters in data translation
mainAdapters 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:
- Serdes (Serialize/Deserialize) code: The logic to convert data formats.
- Custom translation maps: Such as
fieldsToSqliteColumnsorrestEndpoint.
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)); }Avoid using Memory Cache Provider with parent models containing child associations
mainWhen using theMemoryCacheProvider, 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.Why are both annotations and `extends` required for models?
mainIn Brick, models require both a class-level annotation and a type extension (e.g.,
extends OfflineFirstModel):- Annotations: Required to build the generated files, such as adapters and migrations.
- Type Extension (
extends): Used by the repository's type system to identify and manage the model.
Configure OfflineFirstUpsertPolicy
mainWhen saving or updating data (upserting), use
OfflineFirstUpsertPolicyto 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);Handle Data Associations
mainBrick 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);Important: Do not import brick_rest_generators into end applications
mainThebrick_rest_generatorspackage is strictly a build-time dependency. It is designed to be used within Brick build domains to provide (de)serialization logic forRestProvider. 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.Use OfflineFirstWithGraphqlRepository for GraphQL integration
mainThe
OfflineFirstWithGraphqlRepositorysimplifies integrating GraphQL with anOfflineFirstRepository. 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
OfflineFirstdomain.You can override default behavior on a per-request basis using the
policy:parameter. This is supported for the following methods:deletegetgetBatchedsubscribeupsert
Example policy usage:
get<Person>(policy: OfflineFirstUpsertPolicy.localOnly)get<Person>(policy: OfflineFirstUpsertPolicy.localOnly)Brick vs State Management
mainBrick 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.How the code generation process works
mainThe
brick_offline_first_with_supabase_buildpackage automates the creation of adapters and database schemas through several steps:- Discovery: Finds classes annotated with
@ConnectOfflineFirstWithSupabasethat extendOfflineFirstWithSupabaseModel. - Field Expansion: Extracts fields and creates
SupabaseFieldsandSqliteFieldsinstances to ensure fields are processed in declaration order. - Generator Creation: Creates
SupabaseSerialize,SupabaseDeserialize,SqliteSerialize, andSqliteDeserializegenerators. - Transformation Logic: The generators produce code to transform field types (e.g., converting
List<Future<int>>into a format suitable for Supabase). - Adapter Generation: Wraps the transformation logic into functions like
MODELToSupabase()and saves the result toadapters/MODELNAME.g.dart. - Model Dictionary: A central
brick.g.dartis generated containing a model dictionary for all annotated classes. - Schema & Migrations: The
SqliteSchemaGeneratordetects changes in your data structure. If differences are found, it generates a new migration file (e.g.,db/migrations/VERSION_migration.dart) and updatesdb/schema.g.dart.
- Discovery: Finds classes annotated with
Configure OfflineFirstDeletePolicy
mainWhen deleting data, you can specify a
OfflineFirstDeletePolicyto 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);