<b012_data>
pub.dev ↗ GitHub ↗
Flutter package v2.0.3

b012_data, Object–Relational Mapping, the Spring Boot way for flutter.

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.

$ add to pubspec.yaml
b012_data: ^2.0.3
  • 01Zero SQL to maintain
  • 025 platforms, one API
  • 03Metadata in SQLite, blobs on disk
§ 00 · Overview

Four ideas. One package.

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.

[01]

Spring-style entities

Declare a primary key, non-null columns, uniques, checks, defaults and foreign keys as plain getters. DataAccess turns them into SQL.

pKeyAuto · notNulls · uniques · fKeys
[02]

Every CRUD, for free

Insert, bulk-insert, read-by-predicate, project single or multiple columns, update, delete, count — all typed, all generic, all transaction-aware.

insertObjet · get · getAll · update · delete
[03]

Five platforms, one API

Native sqflite on Android, iOS and macOS. Transparent fallback to sqflite_common_ffi on Linux and Windows — the same code everywhere.

Android · iOS · macOS · Linux · Windows
[04]

Bytes on disk, metadata in SQLite

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.

readFileAs* · saveDataToDisc · getEntityFileOnDisc
§ 01 · Setup

Installation

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();
}
§ 02 · Conventions

Declaring an entity

An entity is a plain Dart class that exposes six well-known members. DataAccess reads them once, then generates every statement it needs.

  1. 1

    Primary key required

    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);
  2. 2

    Constraints optional

    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']};
  3. 3

    Unnamed constructor required

    Used by the package to instantiate an entity from the fromMap callback below.

    Person([this.idPers, this.firstName, this.lastName, this.sex, this.dateOfBirth]);
  4. 4

    Serialization toMap required

    For 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,
    };
  5. 5

    Named fromMap constructor required

    Deserialization 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?);
    }
  6. 6

    Instance-side fromMap required

    Wraps the named constructor so generic getters can call it off any instance.

    Person fromMap(Map<String, Object?> json) => Person.fromMap(json);
§ 03 · Runtime

The CRUD walk-through

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');
a. Create

Single row, then a whole list — inside one transaction.

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)),
]);
b. Read

One row, many rows, a single column, or several columns.

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',
);
c. Update

Values follow the order 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'],
);
d. Delete & count

Remove rows matching a predicate, then count what’s left.

// 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',
);
§ 04 · Disk

Files on disk

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);
}
a. System paths

Three directories exposed by the package.

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
b. Write

One call writes text, Base64 or raw bytes.

// 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()),
);
c. Read

As a string, Base64, bytes — or a Flutter widget.

// 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');
d. Database-backed resolution

Metadata in SQLite, blobs on disk, one call to bind them.

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');
§ 05 · Edge cases

Things to know

Two subtleties that save debugging time later.

01

Missing-table exceptions

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.

02

Dart map vs. SQLite-ready map

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.

§ 06 · Sandbox

Full worked example

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
  • example/main.dart Entry point

    Runs both demos in sequence.

    flutter run -t example/main.dart
§ 07 · Reach

One import. Five targets.

Same code, everywhere Flutter runs — native SQLite on mobile, FFI on desktop.

§ 08 · Ship it

Add a line. Delete a thousand.

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.