Brick is an all-in-one data manager for Flutter that handles querying and uploading between Supabase and local caches like SQLite. Using Brick, developers can focus on implementing the application without worrying about translating or storing their data.
Most significantly, Brick focuses on offline-first data parity: an app should function the same with or without connectivity.
The worst version of your app is always the unusable one. People use their phones on subways, airplanes, and on sub-3G connections. Building for offline-first provides the best user experience when you can’t guarantee steady bandwidth.
Even if you’re online-only, Brick’s round trip time is drastically shorter because all data from Supabase is stored in a local cache. When you query the same data again, your app retrieves the local copy, reducing the time and expense of a round trip. And, if SQLite isn’t performant enough, Brick also offers a third cache in memory. When requests are made while the app is offline, they’ll be continually retried until the app comes online, ensuring that your local state syncs up to your remote state.
Of course, you can always opt-out of the cache on a request-by-request basis for sensitive or must-be-fresh data.
Brick synthesizes your remote data to your local data through code generation. From a Supabase table, create Dart fields that match the table’s columns:
// Your model definition can live anywhere in lib/**/* as long as it has the .model.dart suffix
// Assume this file is saved at my_app/lib/src/users/user.model.dart
class User extends OfflineFirstWithSupabaseModel {
final String name;
// Be sure to specify an index that **is not** auto incremented in your table.
// An offline-first strategy requires distributed clients to create
// indexes without fear of collision.
@Supabase(unique: true)
@Sqlite(index: true, unique: true)
final String id;
User({
String? id,
required this.name,
}) : this.id = id ?? const Uuid().v4();
}
When some (or all) of your models have been defined, generate the code:
dart run build_runner build
This will generate adapters to serialize/deserialize to and from Supabase. Migrations for SQLite are also generated for any new, dropped, or changed columns. Check these migrations after they are generated - Brick is smart, but not as smart as you.
After every model change, run this command to ensure your adapters will serialize/deserialize the way they need to.
Your application does not need to touch SQLite or Supabase directly. By interacting with this single entrypoint, Brick makes the hard choices under the hood about where to fetch and when to cache while the application code remains consistent in online or offline modes.
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
databaseFactory: databaseFactory,
);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey,
httpClient: client,
);
final provider = SupabaseProvider(
Supabase.instance.client,
modelDictionary: supabaseModelDictionary,
);
_instance = Repository._(
supabaseProvider: provider,
sqliteProvider: SqliteProvider(
'my_repository.sqlite',
databaseFactory: databaseFactory,
modelDictionary: sqliteModelDictionary,
),
migrations: migrations,
offlineRequestQueue: queue,
// Specify class types that should be cached in memory
memoryCacheProvider: MemoryCacheProvider(),
);
}
}
import 'package:my_app/brick/repository.dart';
import 'package:sqflite/sqflite.dart' show databaseFactory;
Future<void> main() async {
await Repository.configure(databaseFactory);
// .initialize() does not need to be invoked within main()
// It can be invoked from within a state manager or within
// an initState()
await Repository().initialize();
runApp(MyApp());
}
Which databaseFactory is used depends on your platform. For unit testing, use import 'package:sqflite_common_ffi/sqflite_ffi.dart' show databaseFactory. Please see SQFlite’s docs for specific installation and usage instructions on web, Linux, or Windows.
The fun part. Brick’s DSL queries are written once and transformed for local and remote integration. For example, to retrieve all users with the name “Thomas”:
Beyond async requests, you can subscribe to a stream of updated local data from anywhere in your app (for example, if you pull-to-refresh a list of users, all listeners will be notified of the new data):
final Stream<List<User>> usersStream = Repository().subscribe<User>(query: Query.where('name', 'Thomas'));
This does not leverage Supabase’s channels by default; if Supabase updates, your app will not be notified. This opt-in feature is currently under active development.
Upserting
After a model has been created, it can uploaded to Supabase without serializing it to JSON first:
Brick manages a lot. It can be overwhelming at times. But it’s been used in production across thousands of devices for more than five years, so it’s got a sturdy CV. There’s likely an existing solution to a seemingly novel problem. Please reach out to the community or package maintainers with any questions.