mynotes: Implement CRUD operations using sqlite

This commit is contained in:
Evan Ferrao 2024-07-30 12:33:05 +05:30
parent 68854afae6
commit 93164afa7c
No known key found for this signature in database
GPG Key ID: F01DEB4D7CFC9B52
6 changed files with 421 additions and 28 deletions

View File

@ -4,7 +4,8 @@ import 'package:flutter/material.dart';
@immutable
class AuthUser {
final bool isEmailVerified;
AuthUser({required this.isEmailVerified});
const AuthUser({required this.isEmailVerified});
factory AuthUser.fromFirebase(User user) => AuthUser(isEmailVerified: user.emailVerified);
factory AuthUser.fromFirebase(User user) =>
AuthUser(isEmailVerified: user.emailVerified);
}

View File

@ -0,0 +1,17 @@
class DatabaseAlreadyOpenException implements Exception {}
class UnableToGetDocumentsDirectory implements Exception {}
class DatebaseNotOpen implements Exception {}
class CouldNotDeleteUser implements Exception {}
class UserAlreadyExists implements Exception {}
class CouldNotFindUser implements Exception {}
class CouldNotDeleteNote implements Exception {}
class CouldNotFindNote implements Exception {}
class CouldNotUpdateNote implements Exception {}

View File

@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:mynotes/services/crud/crud_crudexceptions.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
class NotesService {
Database? _db;
Future<DatabaseNote> updateNote({
required DatabaseNote note,
required String text,
}) async {
final db = _getDatabaseOrThrow();
await getNote(id: note.id);
final updatesCount = await db.update(noteTable, {
textColumn: text,
isSyncedWithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
return await getNote(id: note.id);
}
}
Future<Iterable<DatabaseNote>> getAllNote({required int id}) async {
final db = _getDatabaseOrThrow();
final notes = await db.query(
noteTable,
);
return notes.map((noteRow) => DatabaseNote.fromRow(noteRow));
// where is noteRow coming from?
// The noteRow variable is a parameter of the map method. The map method is called on the notes list, which is a list of rows returned by the query method. The map method takes a function as an argument and applies that function to each element of the list. In this case, the function is an anonymous function that takes a row as a parameter and returns a DatabaseNote object created from that row.
// what is the type of noteRow?
// The type of noteRow is Map<String, Object?>. This is the type of the rows returned by the query method. Each row is a map where the keys are column names and the values are the corresponding values in the row.
// but noteRow is not defined in the function getAllNote?
// The noteRow variable is a parameter of the anonymous function passed to the map method. The map method applies the function to each element of the list and passes the element as an argument to the function. In this case, the element is a row returned by the query method, and the function creates a DatabaseNote object from that row.
}
Future<DatabaseNote> getNote({required int id}) async {
final db = _getDatabaseOrThrow();
final notes = await db.query(
noteTable,
limit: 1,
where: '$idColumn = ?',
whereArgs: [id],
);
if (notes.isEmpty) {
throw CouldNotFindNote();
} else {
return DatabaseNote.fromRow(notes.first);
}
}
Future<int> deleteAllNotes() async {
final db = _getDatabaseOrThrow();
return await db.delete(noteTable);
}
Future<void> deleteNote({required int id}) async {
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
noteTable,
where: '$idColumn = ?',
whereArgs: [id],
);
if (deletedCount == 0) {
throw CouldNotDeleteNote();
}
}
Future<DatabaseNote> createNote({required DatabaseUser owner}) async {
final db = _getDatabaseOrThrow();
final dbUser = await getUser(email: owner.email);
// make sure owner exists in the database with the correct id
if (dbUser != owner) {
throw CouldNotFindUser();
}
const text = '';
// create the note
final noteId = await db.insert(noteTable, {
userIdColumn: owner.id,
textColumn: text,
isSyncedWithCloudColumn: 1,
});
final note = DatabaseNote(
id: noteId,
userId: owner.id,
text: text,
isSyncedWithCloud: true,
);
return note;
}
Future<DatabaseUser> getUser({required String email}) async {
final db = _getDatabaseOrThrow();
final user = await db.query(
userTable,
limit: 1,
where: '$emailColumn = ?',
whereArgs: [email.toLowerCase()],
);
if (user.isEmpty) {
throw UserAlreadyExists();
} else {
return DatabaseUser.fromRow(user.first);
}
}
Future<DatabaseUser> createUser({required String email}) async {
final db = _getDatabaseOrThrow();
final results = await db.query(
userTable,
limit: 1,
where: '$emailColumn = ?',
whereArgs: [email.toLowerCase()],
);
if (results.isNotEmpty) {
throw UserAlreadyExists();
}
final userId = await db.insert(
userTable,
{emailColumn: email.toLowerCase()},
);
return DatabaseUser(id: userId, email: email);
}
Future<void> deleteUser({required String email}) async {
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
userTable,
where: '$emailColumn = ?',
whereArgs: [email.toLowerCase()],
);
if (deletedCount != 1) {
throw CouldNotDeleteUser();
}
}
Database _getDatabaseOrThrow() {
final db = _db;
if (db == null) {
throw DatebaseNotOpen();
} else {
return db;
}
}
Future<void> close() async {
final db = _db;
if (db == null) {
throw DatebaseNotOpen();
} else {
await db.close();
_db = null;
}
}
Future<void> open() async {
if (_db != null) {
throw DatabaseAlreadyOpenException();
}
try {
final docsPath = await getApplicationDocumentsDirectory();
final dbPath = join(docsPath.path, dbName);
final db = await openDatabase(dbPath);
_db = db;
// Create the user tables if they don't exist
await db.execute(createUserTable);
// Create the notes tables if they don't exist
await db.execute(createnoteTable);
} on MissingPlatformDirectoryException {
throw UnableToGetDocumentsDirectory();
}
}
}
@immutable
class DatabaseUser {
final int id;
final String email;
const DatabaseUser({
required this.id,
required this.email,
});
DatabaseUser.fromRow(Map<String, Object?> map)
// why Object? and not Object
// The Object? type allows the value to be null. If the value is null, the map[idColumn] as int expression will throw an exception. If the value is not null, the map[idColumn] as int expression will return the value as an int.
: id = map[idColumn] as int,
email = map[emailColumn] as String;
// where is the fromRow method?
// The fromRow method is a factory constructor that creates a DatabaseUser object from a row in the database.
// What is a factory constructor?
// A factory constructor is a constructor that returns an instance of a class. It is used to create objects of a class without exposing the constructor to the outside world.
// so can i call fromRow anything? or does it have to be fromRow?
// You can call the factory constructor anything you want. The name fromRow is a convention used to indicate that the method creates an object from a row in a database.
// can i name it fromMyRow or fromDatabaseRow?
// Yes, you can name it fromMyRow. The name of the factory constructor is up to you, but it is a good practice to use a name that describes what the method does.
@override
String toString() => 'Person, ID = $id, email = $email';
@override
bool operator ==(covariant DatabaseUser other) => id == other.id;
@override
int get hashCode => id.hashCode;
}
class DatabaseNote {
final int id;
final int userId;
final String text;
final bool isSyncedWithCloud;
const DatabaseNote({
required this.id,
required this.userId,
required this.text,
required this.isSyncedWithCloud,
});
DatabaseNote.fromRow(Map<String, Object?> map)
// what does fromRow do?
// The fromRow method is a factory constructor that creates a DatabaseNote object from a row in the database.
: id = map[idColumn] as int,
userId = map[userIdColumn] as int,
text = map[textColumn] as String,
isSyncedWithCloud =
(map[isSyncedWithCloudColumn] as int) == 1 ? true : false;
// isSyncedWithCloud = map[isSyncedWithCloudColumn] as bool;
// what is the difference between the two lines above?
// The first line converts the value of the isSyncedWithCloudColumn to a boolean value by checking if it is equal to 1. If it is equal to 1, it sets the value to true; otherwise, it sets it to false.
//The second line assumes that the value of the isSyncedWithCloudColumn is already a boolean value and assigns it directly to the isSyncedWithCloud property.
@override
String toString() =>
'Note, ID = $id, userId = $userId, isSyncedWithCloud = $isSyncedWithCloud, text = $text';
}
const dbName = 'notes.db';
const noteTable = 'notes';
const userTable = 'user';
const idColumn = 'id';
const emailColumn = 'email';
const userIdColumn = 'user_id';
const textColumn = 'text';
const isSyncedWithCloudColumn = 'is_synced_with_cloud';
const createUserTable = '''
CREATE TABLE IF NOT EXISTS $userTable (
$idColumn INTEGER NOT NULL,
$emailColumn TEXT NOT NULL UNIQUE,
PRIMARY KEY ($idColumn AUTOINCREMENT)
);
''';
const createnoteTable = '''
CREATE TABLE IF NOT EXISTS $noteTable (
$idColumn INTEGER NOT NULL,
$userIdColumn INTEGER NOT NULL,
$textColumn TEXT,
$isSyncedWithCloudColumn INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY ($userIdColumn) REFERENCES $userTable($idColumn),
PRIMARY KEY ($idColumn AUTOINCREMENT)
);
''';

View File

@ -9,10 +9,14 @@ import cloud_firestore
import firebase_analytics
import firebase_auth
import firebase_core
import path_provider_foundation
import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: a315d1c444402c3fa468de626d33a1c666041c87e9e195e8fb355b7084aefcc1
sha256: b46f62516902afb04befa4b30eb6a12ac1f58ca8cb25fb9d632407259555dd3d
url: "https://pub.dev"
source: hosted
version: "1.3.38"
version: "1.3.39"
analyzer:
dependency: transitive
description:
@ -69,26 +69,26 @@ packages:
dependency: "direct main"
description:
name: cloud_firestore
sha256: "1232370ad04c21c699d0e73b2dc2e1c3b49258f89a16f0119036fd3c6e8aa2f5"
sha256: "240c1c3598e62ad58ee665b6df9c65172d2fbe4742770c21ed060e013cb8e037"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "5.1.0"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: cfc64ae4a48bbb0ff6730b04f2d4653043c7a9b9008a991b1f2012a534b79e26
sha256: "5b5a9c2b5a85bf995f12e7447c4197d7ad659533d642d3d904ccbb509f83d62a"
url: "https://pub.dev"
source: hosted
version: "6.2.8"
version: "6.2.9"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: "3db4e4c10feae18d80da86982781578c8e76cb6a43cf83110d8d6a62af9a952a"
sha256: "898e9f65548df65ca7b2cff9f32cf233424ceccff1d870b8819ea2b8050d5b39"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.0.3"
collection:
dependency: transitive
description:
@ -137,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
@ -149,58 +157,58 @@ packages:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "726596f4ac3352978238274c33234435e61bdb811484ea3d6a2b857bf47a2715"
sha256: "2017da2cb0745fa912e13aadfc94e49691796cf6b7ed37edbca2a2388713da11"
url: "https://pub.dev"
source: hosted
version: "11.1.0"
version: "11.2.0"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: db445c727aa38038f91a3c6f6873d045a6740c79d03c0b6c61959e0c6ecfd771
sha256: "4ea00fe31ff74c92c613e082193b55bd21f0653b536dd77cc1ba4cf60771d214"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.2.0"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "49a8a5ca0bf7fd7541e4b0915b8eb99816edc7a610d5078b89ae013a813fd567"
sha256: e5ce84f0c4e6fb8f8ca673d009445938a61845d8fa6b42c11913d5bbdbedf300
url: "https://pub.dev"
source: hosted
version: "0.5.8"
version: "0.5.9"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: "087fdcb54b0af6f4c5c756e1db4f90e9b65871b9b3a75fabaa0e0ee578301669"
sha256: a41b56878fa6aef3ea52962329b47eee333672d4b0ecc406e071b9fc729f242c
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.1.2"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: "8fac689f71ac3489a785579e99a4bad24a93ad3d78c313fb786ee517012d25f1"
sha256: d1c68097588f3b75ef79a22102ff96c311735c254353bccf6824d19f1a7e86b9
url: "https://pub.dev"
source: hosted
version: "7.4.1"
version: "7.4.2"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: "486b2527fcfcab01278378d8d4791f4f7bee8a9f15bf35e801ba08fbdd84c234"
sha256: e66ec0ae5697ee39ccd4865d6887cb0df220dd4ea0b21404910c68ca4c1a731a
url: "https://pub.dev"
source: hosted
version: "5.12.3"
version: "5.12.4"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "1e06b0538ab3108a61d895ee16951670b491c4a94fce8f2d30e5de7a5eca4b28"
sha256: "5159984ce9b70727473eb388394650677c02c925aaa6c9439905e1f30966a4d5"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.2.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -213,10 +221,10 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d"
sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712"
url: "https://pub.dev"
source: hosted
version: "2.17.2"
version: "2.17.3"
flutter:
dependency: "direct main"
description: flutter
@ -377,13 +385,69 @@ packages:
source: hosted
version: "2.1.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e"
url: "https://pub.dev"
source: hosted
version: "2.2.7"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
@ -469,6 +533,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
url: "https://pub.dev"
source: hosted
version: "2.3.3+1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
stack_trace:
dependency: transitive
description:
@ -493,6 +573,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@ -581,6 +669,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
yaml:
dependency: transitive
description:
@ -591,4 +687,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.4.1 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.22.0"

View File

@ -39,6 +39,9 @@ dependencies:
firebase_auth: ^5.1.1
cloud_firestore: ^5.0.2
firebase_analytics: ^11.1.0
sqflite: ^2.3.3+1
path_provider: ^2.1.3
path: ^1.9.0
dev_dependencies:
flutter_test: