Spring-style entities
Declare a primary key, non-null columns, uniques, checks, defaults and foreign keys as plain getters. DataAccess turns them into SQL.
b012_data turns a plain Dart class into a persistent entity. CRUD statements, transactions, projections, file I/O — all generated from six simple conventions. No hand-written SQL. No repository boilerplate. Just Dart.
pubspec.yaml
b012_data: ^2.0.3
b012_data keeps the surface area narrow on purpose: a class is the schema, an instance is the row, and two singletons do the rest.
Declare a primary key, non-null columns, uniques, checks, defaults and foreign keys as plain getters. DataAccess turns them into SQL.
Insert, bulk-insert, read-by-predicate, project single or multiple columns, update, delete, count — all typed, all generic, all transaction-aware.
Native sqflite on Android, iOS and macOS. Transparent fallback to sqflite_common_ffi on Linux and Windows — the same code everywhere.
DiscData reads, writes, appends and resolves binary files as text, Base64, Uint8List or Flutter Image. Pair it with a row and you have media, solved.
Add the dependency, pull it in, and you are ready. A default database named sqlf_easy.db is created on first import.
Add the package to your pubspec.yaml:
dependencies:
b012_data: ^2.0.3
Then run:
flutter pub get
Before calling any method that needs the documents directory (databasesPath, filesPath, …), make sure the Flutter bindings are initialized:
import 'disc_example.dart' as disc;
import 'sqlite_example.dart' as sqlite;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// SQLite CRUD walk-through (Person entity).
await sqlite.runSqliteDemo();
// File system / binary file walk-through (Images entity).
await disc.runDiscDemo();
}
An entity is a plain Dart class that exposes six well-known members. DataAccess reads them once, then generates every statement it needs.
A MapEntry whose key is the column name and whose value is true to use AUTOINCREMENT.
MapEntry<String, bool> get pKeyAuto => const MapEntry('idPers', false);
Non-nullable columns, uniques, check constraints, default values and foreign keys — each expressed as a getter.
List<String> get notNulls => ['firstName', 'lastName', 'sex', 'dateOfBirth'];
// List<String> get uniques => ['email'];
// Map<String, String> get checks => {'email': 'length(email) > 4'};
// Map<String, String> get defaults => {'profession': 'NULL'};
// Map<String, List<String>> get fKeys => {'profession': ['Profession', 'idProf']};
Used by the package to instantiate an entity from the fromMap callback below.
Person([this.idPers, this.firstName, this.lastName, this.sex, this.dateOfBirth]);
toMap requiredFor non-nullable fields the ColumnType fallback can be omitted — the value will never be null at insertion time.
Map<String, dynamic> toMap() => {
'idPers': idPers ?? ColumnType.String,
'firstName': firstName ?? ColumnType.String,
'lastName': lastName ?? ColumnType.String,
'sex': sex ?? ColumnType.bool,
'dateOfBirth': dateOfBirth ?? ColumnType.DateTime,
};
fromMap constructor requiredDeserialization from a single SQLite row.
Person.fromMap(Map<String, Object?> json, {bool isInt = true}) {
idPers = json['idPers'] as String?;
firstName = json['firstName'] as String?;
lastName = json['lastName'] as String?;
sex = boolean(json['sex'], isInt: isInt);
dateOfBirth = dateTime(json['dateOfBirth'] as String?);
}
fromMap requiredWraps the named constructor so generic getters can call it off any instance.
Person fromMap(Map<String, Object?> json) => Person.fromMap(json);
Every call below is the high-level equivalent of a Spring Data JPA repository method, driven exclusively through DataAccess.instance — no SQL string is ever written by hand.
Start by inspecting the schema that will be generated from your entity, then check whether the table already exists:
// Inspect the CREATE TABLE statement generated for the Person entity.
debugPrint('${DataAccess.instance.showCreateTable(Person())}\n\n');
// Does the Person table already exist in the database?
final bool personTableExists =
await DataAccess.instance.checkIfEntityTableExists<Person>();
debugPrint('Person table exists? $personTableExists\n');
insertObjet inserts a single entity. insertObjetList wraps a list of entities into a single atomic transaction. Both return a bool.
// CREATE — insert a single Person row.
final bool inserted = await DataAccess.instance.insertObjet(
Person(newKey, 'KEBE', 'Birane', true, DateTime(2000, 8, 5)),
);
// CREATE (bulk) — insert several Persons inside a single transaction.
final bool personsListInserted =
await DataAccess.instance.insertObjetList(<Person>[
Person(newKey, 'Mbaye', 'Aliou', true, DateTime(1999, 5, 1)),
Person(newKey, 'Cisse', 'Fatou', false, DateTime(2000, 7, 9)),
]);
Predicates are plain SQL strings passed as afterWhere. Booleans are converted to 0/1 for SQLite automatically, so sex = true is equivalent to sex = 1.
// READ — fetch a single row matching an SQL predicate.
final Person? birane = await DataAccess.instance.get<Person>(
Person(),
"firstName = 'Birane' AND lastName = 'KEBE'",
);
// READ — fetch every row.
final List<Person>? everyone =
await DataAccess.instance.getAll<Person>(Person());
// READ — fetch rows matching a predicate.
final List<Person>? men = await DataAccess.instance.getAllSorted<Person>(
Person(),
'sex = true',
);
Projection — single column or a set of columns:
// READ (projection) — pull a single column across the whole table.
final List<String> firstNames =
await DataAccess.instance.getAColumnFrom<String, Person>('firstName');
final List<String> womenFirstNames = await DataAccess.instance
.getAColumnFrom<String, Person>('firstName', afterWhere: 'sex = false');
// READ (projection) — pull several columns at once.
final List<Map<String, Object?>> nameRows = await DataAccess.instance
.updateSomeColumnsOf<Person>('firstName, lastName');
final List<Map<String, Object?>> womenNameRows =
await DataAccess.instance.updateSomeColumnsOf<Person>(
'firstName, lastName',
afterWhere: 'sex = 0',
);
columnsToUpdate + whereColumns.Here: set firstName = 'developer' and lastName = '2022' where firstName = 'Birane' AND lastName = 'KEBE'.
final bool updated = await DataAccess.instance.updateSomeColumnsOf<Person>(
<String>['firstName', 'lastName'],
<String>['firstName', 'lastName'],
<Object>['developer', '2022', 'Birane', 'KEBE'],
);
// DELETE — remove every row matching the predicate.
final bool deletedFatou = await DataAccess.instance.deleteObjet<Person>(
"firstName = 'Fatou'",
);
// COUNT — total rows and filtered rows.
final int total = await DataAccess.instance.countElementsOf<Person>();
final int males = await DataAccess.instance.countElementsOf<Person>(
afterWhere: 'sex = true',
);
The DiscData singleton reads, writes, appends and checks files. It returns text, Base64, Uint8List or a Flutter Image widget — and it can resolve a file name stored in SQLite back to raw bytes.
The pattern is simple: keep metadata (ids, timestamps, names) in SQLite, keep the binary blob on disk, and resolve one from the other with a single call.
class Images {
String? imageID;
String? imageName;
DateTime? dateSave;
DateTime? dateLastUpdate;
MapEntry<String, bool> get pKeyAuto => const MapEntry('imageID', false);
List<String> get notNulls =>
const ['imageName', 'dateSave', 'dateLastUpdate'];
Images([this.imageID, this.imageName, this.dateSave, this.dateLastUpdate]);
Map<String, dynamic> toMap() => {
'imageID': imageID ?? ColumnType.String,
'imageName': imageName ?? ColumnType.String,
'dateSave': dateSave ?? ColumnType.DateTime,
'dateLastUpdate': dateLastUpdate ?? ColumnType.DateTime,
};
Images.fromMap(Map<String, Object?> json, {bool isInt = true}) {
imageID = json['imageID'] as String?;
imageName = json['imageName'] as String?;
dateSave = dateTime(json['dateSave'] as String?);
dateLastUpdate = dateTime(json['dateLastUpdate'] as String?);
}
Images fromMap(Map<String, Object?> json) => Images.fromMap(json);
}
final String databasesPath = await DiscData.instance.databasesPath; // SQLite files
final String filesPath = await DiscData.instance.filesPath; // user files
final String rootPath = await DiscData.instance.rootPath; // documents dir
// Save a text file into the default `files` directory.
final String? savedName = await DiscData.instance.saveDataToDisc(
'contents of test.txt',
DataType.text,
takeThisName: 'test.txt',
);
// Save an Image (Base64) to the default `files` directory.
final String? testImage = await DiscData.instance.saveDataToDisc(
testImageAsBase64,
DataType.base64,
takeThisName: 'testImage.png',
);
// Store a matching metadata row for that image.
final bool imageRecordCreated = await DataAccess.instance.insertObjet(
Images('img0001', 'testImage.png', DateTime.now(), DateTime.now()),
);
// Exists?
final bool exists = await DiscData.instance.checkFileExists(fileName: 'test.txt');
// As a UTF-8 string.
final String? asString = await DiscData.instance.readFileAsString(fileName: 'test.txt');
// As Base64 — handy to ship blobs over JSON.
final String? asBase64 = await DiscData.instance.readFileAsBase64(fileName: 'testImage.png');
// As raw bytes — images, audio, video, PDFs, anything binary.
final Uint8List? asBytes = await DiscData.instance.readFileAsBytes(fileName: 'testImage.png');
// Straight into a Flutter `Image` widget.
final Image? asImage = await DiscData.instance.getImageFromDisc(imageName: 'testImage.png');
Given a table Images with a column imageName that holds a file name on disk, fetch the raw bytes of the row where imageID = 'img0001':
final Uint8List? entityBytes = await DiscData.instance
.getEntityFileOnDisc<Uint8List, Images>(
'imageName', 'imageID', 'img0001');
Two subtleties that save debugging time later.
Most entity-oriented methods throw DatabaseException('no such table: …') when the underlying table does not exist. The exceptions are updateWholeObject, updateSomeColumnsOf, getAColumnFromWithTableName and getAColumnFrom, which catch the error and only log it via debugPrint. Wrap the other methods in try/catch if you are not sure the table already exists.
insertObjet and insertObjetList internally convert your entity map using mapToUse(entity.toMap()). Call mapToUse(entity.toMap(), forDB: false) yourself when you want a plain Dart map (for JSON serialization or display) instead of an SQLite-ready one.
The example/ folder splits the two concerns into self-contained, runnable files so each API can be explored in isolation.
Spring Boot-style walk-through built around the Person entity and DataAccess.instance.
flutter run -t example/sqlite_example.dart
Binary files (text, Base64, raw bytes, Image) built around DiscData.instance and the Images entity.
flutter run -t example/disc_example.dart
Runs both demos in sequence.
flutter run -t example/main.dart
Same code, everywhere Flutter runs — native SQLite on mobile, FFI on desktop.
Stop writing repositories, data-access objects and migration glue. Declare an entity, import the singletons, and move on to the part of your app that matters.