This is the GitHub repository of the MyNotes application for the Free Flutter Course (https://www.youtube.com/playlist?list=PL6yRaaP0WPkVtoeNIGqILtRAgd3h2CNpT)
import 'package:flutter/foundation.dart';
import 'package:flutter_application_1/constants/routes.dart';
import 'package:flutter_application_1/services/crud/crud_exceptions.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart'
show getApplicationDocumentsDirectory, MissingPlatformDirectoryException;
import 'package:path/path.dart' show join;
class NotesService {
Database? _db;
// to create singleton class only one instace of this is created in whole project
static final NotesService _shared = NotesService._sharedInstance();
NotesService._sharedInstance() {
_notesStreamController = StreamController<List>.broadcast(
onListen: () {
_notesStreamController.sink.add(_notes);
},
);
}
factory NotesService() => _shared;
// _notes is cached data where CRUD operation done
List _notes = [];
// manipulation of data in pipe is through _notesStreamController
// if you listen to changes in pipe and do hot reload then error occurs thats why broadcast here which closes the current listening channel
// before listening them again you must close the previos one
late final StreamController<List> _notesStreamController;
Stream<List> get allNotes => _notesStreamController.stream;
// _cachedNotes() show warning because it is private and not been used and other are public
Future _cachedNotes() async {
final allNotes = await getAllNotes();
_notes = allNotes.toList();
_notesStreamController.add(_notes);
}
Future updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
// make sure note exists
await getNote(id: note.id);
// update db
final updatesCount = await db.update(noteTable, {
textColumn: text,
isSyncedWithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
Future<Iterable> getAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final notes = await db.query(noteTable);
return notes.map((noteRow) => DatabaseNote.fromRow(noteRow));
}
Future getNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final notes = await db.query(
noteTable,
limit: 1,
where: 'id=?',
whereArgs: [id],
);
if (notes.isEmpty) {
throw CouldNotFindNote();
} else {
final note = DatabaseNote.fromRow(notes.first);
_notes.removeWhere((note) => note.id == id);
_notes.add(note);
_notesStreamController.add(_notes);
return note;
}
}
Future deleteAllNote() async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final numberOfDeletions = await db.delete(noteTable);
_notes = [];
_notesStreamController.add(_notes);
return numberOfDeletions;
}
Future deleteNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
noteTable,
where: 'id=?',
whereArgs: [id],
);
if (deletedCount == 0) {
throw CouldNotDeleteNote();
} else {
// safeguard added please check
final countBefore = _notes.length;
_notes.removeWhere((note) => note.id == id);
if (_notes.length != countBefore) {
_notesStreamController.add(_notes);
}
}
}
Future createNote({required DatabaseUser owner}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
// make sure owner exists in the database with correct id
await getUser(email: owner.email);
const text = '';
// Check if the user_id is correct
print('Creating note for user_id: ${owner.id}');
// create the note
final noteId = await db.insert(noteTable, {
userIdColumn: owner.id,
textColumn: text,
isSyncedWithCloudColumn: 0, // Assuming default value for new note
});
final note = DatabaseNote(
id: noteId,
userId: owner.id,
text: text,
isSyncedWithCloud: true,
);
_notes.add(note);
_notesStreamController.add(_notes);
return note;
}
Future getUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
print('Database=>$db');
print('Fetching user for email: $email');
final results = await db.query(
userTable,
limit: 1,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
print('Results for user query: $results');
if (results.isEmpty) {
print('Could not find user with email: $email');
throw CouldNotFindUser();
} else {
final row = DatabaseUser.fromRow(results.first);
print('Found user: $row');
return row;
}
}
Future createUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
print('Results in createUser =>$results');
if (results.isNotEmpty) {
throw UserAlreadyExists();
}
final userId = await db.insert(userTable, {
emailColumn: email.toLowerCase(),
});
print('userId=>$userId');
return DatabaseUser(
id: userId,
email: email,
);
}
Future deleteUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
userTable,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
if (deletedCount != 1) {
throw CouldNotDeleteUser();
}
}
Database _getDatabaseOrThrow() {
final db = _db;
// print('Database\n');
print(db?.database);
// print('Database:$db');
if (db == null) {
throw DatabaseIsNotOpen();
} else {
return db;
}
}
Future close() async {
final db = _db;
if (db == null) {
throw DatabaseIsNotOpen();
} else {
await db.close();
_db = null;
}
}
Future _ensureDbIsOpen() async {
if (_db == null) {
await open();
}
}
// Add this function to handle database migrations
// Future _migrate(Database db, int oldVersion, int newVersion) async {
// if (oldVersion < 2) {
// // Assuming version 2 introduces the is_synced_with_cloud column
// await db.execute('ALTER TABLE note ADD COLUMN is_synced_with_cloud INTEGER NOT NULL DEFAULT 0');
// }
// }
Future _ensureColumnExists(
Database db, String table, String column) async {
final result = await db.rawQuery('PRAGMA table_info($table)');
print('result=>$result');
final columnExists =
result.any((element) => element['name'] == column);
print('column exist=>$columnExists');
if (!columnExists) {
await db.execute(
'ALTER TABLE $table ADD COLUMN $column INTEGER NOT NULL DEFAULT 0');
}
}
Future open() async {
if (_db != null) {
throw DatabaseAlreadyOpenException();
}
try {
final docsPath = await getApplicationDocumentsDirectory();
final dbPath = join(docsPath.path, dbName);
final db = await openDatabase(dbPath, version: 1);
// allow you to change the behavior of input parameter so that they do not confirm the signature of parameter in super class
@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;
DatabaseNote(
{required this.id,
required this.userId,
required this.text,
required this.isSyncedWithCloud});
DatabaseNote.fromRow(Map<String, Object?> map)
: id = map[idColumn] as int,
userId = map[userIdColumn] as int,
text = map[textColumn] as String,
isSyncedWithCloud = (map[isSyncedWithCloudColumn] as int? ?? 0) == 1;
@override
String toString() =>
'Note,ID=$id, userId= $userId, isSyncedWithCloudColumn=$isSyncedWithCloud,text= $text';
@override
bool operator ==(covariant DatabaseNote other) => id == other.id;
@override
int get hashCode => id.hashCode;
}
const dbName = 'notes.db';
const noteTable = 'note';
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 "user" (
"id" INTEGER NOT NULL,
"email" TEXT NOT NULL UNIQUE,
PRIMARY KEY("id" AUTOINCREMENT)
);''';
const createNoteTable = '''CREATE TABLE IF NOT EXISTS "note" (
"id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"text" TEXT,
"is_synced_with_cloud" INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY("user_id") REFERENCES "user"("id"),
PRIMARY KEY("id" AUTOINCREMENT)
);''';
@override
AuthUser? get currentUser {
final user = FirebaseAuth.instance.currentUser;
print('current user =>$user');
if (user != null) {
print('convert fibase user to AuthUser=>${AuthUser.fromFirebase(user)}');
return AuthUser.fromFirebase(user);
} else {
return null;
}
}
notes_view.dart=>
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter_application_1/constants/routes.dart'; import 'package:flutter_application_1/services/crud/crud_exceptions.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart' show getApplicationDocumentsDirectory, MissingPlatformDirectoryException; import 'package:path/path.dart' show join;
class NotesService { Database? _db; // to create singleton class only one instace of this is created in whole project static final NotesService _shared = NotesService._sharedInstance(); NotesService._sharedInstance() { _notesStreamController = StreamController<List>.broadcast(
onListen: () {
_notesStreamController.sink.add(_notes);
},
);
}
factory NotesService() => _shared;
// _notes is cached data where CRUD operation done List _notes = [];
// manipulation of data in pipe is through _notesStreamController
// if you listen to changes in pipe and do hot reload then error occurs thats why broadcast here which closes the current listening channel
// before listening them again you must close the previos one
late final StreamController<List> _notesStreamController;
Stream<List> get allNotes => _notesStreamController.stream;
Future getOrCreateUser({
required String email,
}) async {
try {
print('inside getorcreate user');
final user = await getUser(email: email);
print('User=>$user');
return user;
} on CouldNotFindUser {
final createdUser = await createUser(email: email);
print('Created User=>$createdUser');
return createdUser;
} catch (e) {
rethrow;
}
}
// _cachedNotes() show warning because it is private and not been used and other are public Future _cachedNotes() async {
final allNotes = await getAllNotes();
_notes = allNotes.toList();
_notesStreamController.add(_notes);
}
Future updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
// make sure note exists
await getNote(id: note.id);
// update db
final updatesCount = await db.update(noteTable, {
textColumn: text,
isSyncedWithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
Future<Iterable> getAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final notes = await db.query(noteTable);
return notes.map((noteRow) => DatabaseNote.fromRow(noteRow));
}
Future getNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final notes = await db.query(
noteTable,
limit: 1,
where: 'id=?',
whereArgs: [id],
);
if (notes.isEmpty) {
throw CouldNotFindNote();
} else {
final note = DatabaseNote.fromRow(notes.first);
_notes.removeWhere((note) => note.id == id);
_notes.add(note);
_notesStreamController.add(_notes);
return note;
}
}
Future deleteAllNote() async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final numberOfDeletions = await db.delete(noteTable);
_notes = [];
_notesStreamController.add(_notes);
return numberOfDeletions;
}
Future deleteNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
noteTable,
where: 'id=?',
whereArgs: [id],
);
if (deletedCount == 0) {
throw CouldNotDeleteNote();
} else {
// safeguard added please check
final countBefore = _notes.length;
_notes.removeWhere((note) => note.id == id);
if (_notes.length != countBefore) {
_notesStreamController.add(_notes);
}
}
}
Future createNote({required DatabaseUser owner}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
}
Future getUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
print('Database=>$db');
print('Fetching user for email: $email');
final results = await db.query(
userTable,
limit: 1,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
print('Results for user query: $results');
if (results.isEmpty) {
print('Could not find user with email: $email');
throw CouldNotFindUser();
} else {
final row = DatabaseUser.fromRow(results.first);
print('Found user: $row');
return row;
}
}
Future createUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
print('Results in createUser =>$results');
if (results.isNotEmpty) {
throw UserAlreadyExists();
}
final userId = await db.insert(userTable, {
emailColumn: email.toLowerCase(),
});
print('userId=>$userId');
return DatabaseUser(
id: userId,
email: email,
);
}
Future deleteUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseOrThrow();
final deletedCount = await db.delete(
userTable,
where: 'email=?',
whereArgs: [email.toLowerCase()],
);
if (deletedCount != 1) {
throw CouldNotDeleteUser();
}
}
Database _getDatabaseOrThrow() { final db = _db; // print('Database\n'); print(db?.database); // print('Database:$db'); if (db == null) { throw DatabaseIsNotOpen(); } else { return db; } }
Future close() async {
final db = _db;
if (db == null) {
throw DatabaseIsNotOpen();
} else {
await db.close();
_db = null;
}
}
Future _ensureDbIsOpen() async {
if (_db == null) {
await open();
}
}
// Add this function to handle database migrations
// Future _migrate(Database db, int oldVersion, int newVersion) async {
// if (oldVersion < 2) {
// // Assuming version 2 introduces the
is_synced_with_cloud
column // await db.execute('ALTER TABLE note ADD COLUMN is_synced_with_cloud INTEGER NOT NULL DEFAULT 0'); // } // }Future _ensureColumnExists(
Database db, String table, String column) async {
final result = await db.rawQuery('PRAGMA table_info($table)');
print('result=>$result');
final columnExists =
result.any((element) => element['name'] == column);
print('column exist=>$columnExists');
if (!columnExists) {
await db.execute(
'ALTER TABLE $table ADD COLUMN $column INTEGER NOT NULL DEFAULT 0');
}
}
Future open() async {
if (_db != null) {
throw DatabaseAlreadyOpenException();
}
try {
final docsPath = await getApplicationDocumentsDirectory();
final dbPath = join(docsPath.path, dbName);
final db = await openDatabase(dbPath, version: 1);
print('SOmething wrong happend $err'); }
} }
@immutable class DatabaseUser { final int id; final String email;
const DatabaseUser({required this.id, required this.email}); DatabaseUser.fromRow(Map<String, Object?> map) : id = map[idColumn] as int, email = map[emailColumn] as String;
@override String toString() => 'Person,ID =$id, email=$email';
// allow you to change the behavior of input parameter so that they do not confirm the signature of parameter in super class @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;
DatabaseNote( {required this.id, required this.userId, required this.text, required this.isSyncedWithCloud}); DatabaseNote.fromRow(Map<String, Object?> map) : id = map[idColumn] as int, userId = map[userIdColumn] as int, text = map[textColumn] as String, isSyncedWithCloud = (map[isSyncedWithCloudColumn] as int? ?? 0) == 1; @override String toString() => 'Note,ID=$id, userId= $userId, isSyncedWithCloudColumn=$isSyncedWithCloud,text= $text'; @override bool operator ==(covariant DatabaseNote other) => id == other.id;
@override int get hashCode => id.hashCode; }
const dbName = 'notes.db'; const noteTable = 'note'; 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 "user" ( "id" INTEGER NOT NULL, "email" TEXT NOT NULL UNIQUE, PRIMARY KEY("id" AUTOINCREMENT) );'''; const createNoteTable = '''CREATE TABLE IF NOT EXISTS "note" ( "id" INTEGER NOT NULL, "user_id" INTEGER NOT NULL, "text" TEXT, "is_synced_with_cloud" INTEGER NOT NULL DEFAULT 0, FOREIGN KEY("user_id") REFERENCES "user"("id"), PRIMARY KEY("id" AUTOINCREMENT) );''';
firebase_authProvider.dart
import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth, FirebaseAuthException; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter_application_1/firebase_options.dart'; import 'package:flutter_application_1/services/auth/auth_provider.dart';
import 'package:flutter_application_1/services/auth/auth_user.dart'; import 'package:flutter_application_1/services/auth/auth_exceptions.dart';
class FirebaseAuthProvider implements AuthProvider { // check2 @override Future initialize() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
@override Future createUser({
required String email,
required String password,
}) async {
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email,
password: password,
);
final user = currentUser;
if (user != null) {
print('User created => $user');
return user;
}
// ignore: curly_braces_in_flow_controlstructures
else {
throw UserNotFoundAuthException();
}
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
throw WeakPasswordAuthException();
} else if (e.code == 'email-already-in-use') {
throw EmailAlreadyInUseAuthException();
} else if (e.code == 'invalid-email') {
throw InvalidEmailAuthException();
} else {
throw GenericAuthException();
}
} catch () {
throw GenericAuthException();
}
}
@override AuthUser? get currentUser { final user = FirebaseAuth.instance.currentUser; print('current user =>$user'); if (user != null) { print('convert fibase user to AuthUser=>${AuthUser.fromFirebase(user)}'); return AuthUser.fromFirebase(user); } else { return null; } }
@override Future logIn({
required String email,
required String password,
}) async {
try {
await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
final user = currentUser;
print('login user=> $user');
if (user != null) {
return user;
}
// ignore: curly_braces_in_flow_controlstructures
else {
throw UserNotFoundAuthException();
}
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
throw UserNotFoundAuthException();
} else if (e.code == 'wrong-password') {
throw WrongPasswordAuthException();
} else {
throw GenericAuthException();
}
} catch () {
throw GenericAuthException();
}
}
@override Future logOut() async {
final user = FirebaseAuth.instance.currentUser;
print('logout user=>$user');
if (user != null) {
await FirebaseAuth.instance.signOut();
} else {
throw UserNotLoggedInAuthException();
}
}
@override Future sendEmailVerification() async {
final user = FirebaseAuth.instance.currentUser;
print('Send email verification=>$user');
if (user != null) {
await user.sendEmailVerification();
} else {
throw UserNotLoggedInAuthException();
}
}
}
notes_view.dart
import 'package:flutter/material.dart'; import 'package:flutter_application_1/services/auth/auth_service.dart'; import 'package:flutter_application_1/services/crud/notes_service.dart';
class NewNoteView extends StatefulWidget { const NewNoteView({super.key});
@override State createState() => _NewNoteViewState();
}
class _NewNoteViewState extends State {
@override
initState() {
_notesService = NotesService();
_textController = TextEditingController();
super.initState();
}
DatabaseNote? _note; late final NotesService _notesService; late final TextEditingController _textController;
void _textControllerListener() async { final note = _note; print('note=>$note'); if (note == null) { return; } final text = _textController.text; print('Text=>$text'); await _notesService.updateNote( note: note, text: text, ); }
void _setupTextControllerListener() { _textController.removeListener(_textControllerListener); _textController.addListener(_textControllerListener); } @override void dispose() { _deleteNoteIfTextIsEmpty(); _saveNoteIfTextNotEmpty(); _textController.dispose(); super.dispose(); } // step1 Future createNewNote() async {
print('inside createNewNote()');
final existingNote = _note;
print('existing note=>$existingNote');
if (existingNote != null) {
return existingNote;
}
// if currentUser is null then we want app to crash so ! it is safe to use
final currentUser = AuthService.firebase().currentUser!;
final email = currentUser.email!;
print('Email=>$email');
final owner = await _notesService.getUser(email: email);
print('Owner=>$owner');
final note = await _notesService.createNote(owner: owner);
print('note here=>$note');
return note;
}
void _deleteNoteIfTextIsEmpty() async { final note = _note; if (_textController.text.isEmpty && note != null) { _notesService.deleteNote(id: note.id); } }
void _saveNoteIfTextNotEmpty() async { final note = _note; final text = _textController.text; if (note != null && text.isNotEmpty) { await _notesService.updateNote( note: note, text: text, ); } }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('New Note'), ), body: FutureBuilder( future: createNewNote(), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.done: // print('Data:$snapshot.data'); if (snapshot.hasData) { _note = snapshot.data as DatabaseNote; } print('new_note_view=>$_note'); _setupTextControllerListener(); return TextField( controller: _textController, keyboardType: TextInputType.multiline, maxLines: null, decoration: const InputDecoration( hintText: 'Start typing your text...', ), ); default: return const CircularProgressIndicator(); } }, ), ); } }
please find out the error it show Results=>[] and stuck in debug mode where CouldNotFindUser