Flutter InstantDB
Typed Layer

Code Generation

Generate typed tables from annotated models with flutter_instantdb_generator

Instead of hand-writing InstantTable classes, you can annotate a plain model class and let the build-time generator (Phase 6b) emit a typed table, columns, a fromRow mapper, and getAll / watchAll query helpers.

Annotations

  • @InstantModel(entityType) — marks a class as a model. entityType is the namespace the generated table queries (e.g. 'todos').
  • @InstantField(name) — overrides the stored attribute name for a field. Without it, the field name is used as the attribute name.
  • @InstantLink() — marks a relation field; see Relations.
import 'package:flutter_instantdb/flutter_instantdb.dart';

part 'sample.instant.dart';

@InstantModel('gadgets')
class Gadget {
  final String id;
  final String label;
  const Gadget({required this.id, required this.label});
}

Flat models (primitive fields) are fully supported. Relation/nested fields use @InstantLink (see Relations); non-nullable relations are rejected by the generator with guidance.

The generator package

The generator ships as a separate dev-only package, flutter_instantdb_generator, built on build_runner / source_gen. Add it (plus build_runner) to your dev_dependencies:

dev_dependencies:
  build_runner: ^2.4.13
  flutter_instantdb_generator: ^0.1.0

It is configured with auto_apply: dependents and build_to: source, so it runs on any package that depends on it and writes .instant.dart files next to your sources.

Running the generator

Make sure your model file has a part 'your_file.instant.dart'; directive, then run:

dart run build_runner build

(Use dart run build_runner watch to regenerate on save.)

Generated output

For the model above, the generator emits a ${Model}Table with a Col<T> per field, a fromRow, a scalar-only toMap, a tx(db) convenience, plus a ${Model}QueryX extension carrying getAll / watchAll and a ${Model}TxX extension for whole-model writes:

class GadgetTable extends InstantModelTable<GadgetTable, Gadget> {
  GadgetTable() : super('gadgets');

  final id = const Col<String>('id');
  final label = const Col<String>('label');

  @override
  Gadget fromRow(Map<String, dynamic> m) => Gadget(
        id: m['id'] as String,
        label: m['label'] as String,
      );

  Map<String, dynamic> toMap(Gadget m) => {
        'id': m.id,
        'label': m.label,
      };

  TypedTx<GadgetTable> tx(InstantDB db) => db.txFor(this);
}

extension GadgetQueryX on TypedQuery<GadgetTable> {
  Future<List<Gadget>> getAll(InstantDB db) async =>
      (await db.queryOnceTyped(this))
          .documents
          .map(GadgetTable().fromRow)
          .toList();

  ReadonlySignal<List<Gadget>> watchAll(InstantDB db) {
    final src = db.queryTyped(this);
    return computed(
        () => src.value.documents.map(GadgetTable().fromRow).toList());
  }
}

Using the generated table

The generated table behaves like any typed table, and getAll / watchAll return typed List<Model> instead of raw maps:

// One-shot, typed list
final gadgets = await GadgetTable().query().getAll(db);
// gadgets is List<Gadget>

// Reactive, typed list
final signal = GadgetTable()
    .query()
    .where((t) => t.label.ilike('%pro%'))
    .watchAll(db);

Watch((context) {
  final gadgets = signal.value; // List<Gadget>
  return Text('${gadgets.length} gadgets');
});

For whole-model writes (createModel / updateModel / mergeModel) and the table.tx(db) sugar, see Transactions.

On this page