Creating a Data Source
To create a Data Source for storing our notes in a local SQL database, we need to create the Data Source
itself and a few Contract classes. The Contract classes will define the structure, helper methods, and constants for our SQLite database and its tables.
1. Creating the Contracts
In the note_app/modules/infrastructure/lib/data_sources/sqlbrite/contracts/
directory, create the following contract files:
1.1. Create a contract for our note database
Create a contract class named notes_database_contract.dart
to define the name and version of our database:
/// {@template NotesDatabaseContract}
/// A contract class for the notes SQL database.
/// {@endtemplate}
class NotesDatabaseContract {
/// {@macro NotesDatabaseContract}
NotesDatabaseContract._();
/// The name of the database.
static const databaseName = 'notes.db';
/// The current version of the database.
static const databaseVersion = 1;
}
1.2. Create a contract for our note table
Create a contract class named note_entry_contract.dart
for our notes table to define its structure and available query commands:
/// {@template NoteEntryContract}
/// A contract class for the note entries table of the notes database.
/// {@endtemplate}
class NoteEntryContract {
/// {@macro NoteEntryContract}
NoteEntryContract._();
/// The name of the database table.
static const String tableName = 'noteEntries';
/// The name of the ID column.
static const String idField = 'id';
/// The name of the title column.
static const String titleField = 'title';
/// The name of the content column.
static const String contentField = 'content';
/// The name of the last updated time column.
static const String updateTimestampField = 'updateTimestamp';
/// A SQL query for creating the note entries table.
static const String createTableQuery = '''
CREATE TABLE $tableName (
$idField TEXT PRIMARY KEY,
$titleField TEXT NOT NULL,
$contentField TEXT NOT NULL,
$updateTimestampField INTEGER NOT NULL
)
''';
/// A SQL query for inserting one or more new note entries into the database.
static String insertQuery([int count = 1]) {
final buffer = StringBuffer('''
INSERT INTO $tableName (
$idField,
$titleField,
$contentField,
$updateTimestampField
) VALUES (?, ?, ?, ?)
''');
for (var i = 1; i < count; i++) {
buffer.write('''
, (?, ?, ?, ?)
''');
}
return buffer.toString();
}
/// A where clause for selecting a note entry by its ID.
static String whereId = '$idField = ?';
/// A where clause for ordering the note entries by their update timestamp in
/// descending order.
static String orderByUpdateTimestampDesc = '$updateTimestampField DESC';
}
2. Create the Data Source
Finally, create the Data Source
to allow for CRUD (create, read, update, delete) operations on our local database.
Under the note_app/modules/infrastructure/lib/data_sources/sqlbrite
directory, create a file called sqlbrite_data_source.dart
and copy the following code:
import 'dart:async';
import 'dart:math';
import 'package:flutter_lorem/flutter_lorem.dart';
import 'package:infrastructure/data_sources/sqlbrite/contracts/note_entry_contract.dart';
import 'package:infrastructure/data_sources/sqlbrite/contracts/notes_database_contract.dart';
import 'package:infrastructure/data_sources/sqlbrite/note_entry_model.dart';
import 'package:sqlbrite/sqlbrite.dart';
/// {@template SqlbriteDataSource}
/// A data source that uses [BriteDatabase] to interact with the local SQLite
/// database.
/// {@endtemplate}
class SqlbriteDataSource {
/// A reference to the [BriteDatabase] instance used to interact with the
/// underlying database
///
/// [initialize] must be called before accessing this property.
late final BriteDatabase _database;
/// Initializes the database.
///
/// Must be called at the start of the app.
Future<void> initialize() async {
// Open the database
final database = await openDatabase(
NotesDatabaseContract.databaseName,
version: NotesDatabaseContract.databaseVersion,
onCreate: (db, version) async {
// Create the note entries table when the database has been created
await db.execute(NoteEntryContract.createTableQuery);
await _generateData(db);
},
);
_database = BriteDatabase(database, logger: null);
}
/// Saves the new note entry to the database.
Future<void> createNoteEntry({
required String title,
required String content,
}) {
final noteEntry = NoteEntryModel(
id: _generateId(),
title: title,
content: content,
updateTimestamp: DateTime.now().millisecondsSinceEpoch,
);
return _database.insert(NoteEntryContract.tableName, noteEntry.toJson());
}
/// Produces a random ID for the note entry.
String _generateId() {
final rnd = Random.secure();
return List<int>.generate(20, (index) => rnd.nextInt(10)).join();
}
/// Updates the note entry with the given [id].
Future<void> updateNoteEntry({
required String id,
required String? title,
required String? content,
}) {
return _database.update(
NoteEntryContract.tableName,
{
if (title != null) NoteEntryContract.titleField: title,
if (content != null) NoteEntryContract.contentField: content,
NoteEntryContract.updateTimestampField:
DateTime.now().millisecondsSinceEpoch,
},
where: NoteEntryContract.whereId,
whereArgs: [id],
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
/// Deletes the note entry with the given [id].
Future<void> deleteNoteEntry({required String id}) {
return _database.delete(
NoteEntryContract.tableName,
where: NoteEntryContract.whereId,
whereArgs: [id],
);
}
/// Fetches a list of note entries from the database sorted by the update
/// timestamp in descending order.
///
/// An [offset] can be provided to skip a number of entries ideal for
/// implementing pagination.
Future<List<NoteEntryModel>> fetchNoteEntries({
required int limit,
int? offset,
}) async {
final result = await _database.query(
NoteEntryContract.tableName,
orderBy: NoteEntryContract.orderByUpdateTimestampDesc,
limit: limit,
offset: offset,
);
return result.map<NoteEntryModel>(NoteEntryModel.fromJson).toList();
}
/// Streams a list of note entries sorted by the update timestamp in
/// descending order with the number of entries specified by the given
/// [limit].
///
/// The stream will emit a new list of note entries whenever a change is made
/// to the database.
Stream<List<NoteEntryModel>> watchRecentNoteEntries({required int limit}) {
return _database
.createQuery(
NoteEntryContract.tableName,
orderBy: NoteEntryContract.orderByUpdateTimestampDesc,
limit: limit,
offset: 0,
)
.mapToList((row) => NoteEntryModel.fromJson(row));
}
/// A method for generating initial test data for the database.
Future<int> _generateData(Database db) async {
final rnd = Random();
final currentTime = DateTime.now().millisecondsSinceEpoch;
final mockNoteEntries = <NoteEntryModel>[
for (var i = 0; i < 100; i++)
NoteEntryModel(
id: _generateId(),
title: lorem(
paragraphs: 1,
words: rnd.nextInt(2) + 1,
).replaceAll('.', ''),
content: lorem(
paragraphs: 1,
words: rnd.nextInt(10) + 1,
),
updateTimestamp: currentTime - (i * 3600),
),
];
return db.rawInsert(
NoteEntryContract.insertQuery(mockNoteEntries.length),
mockNoteEntries
.expand((e) => [
e.id,
e.title,
e.content,
e.updateTimestamp,
])
.toList(),
);
}
}
Last updated