> allOffsets,
71 | ) {
72 | final object = Note();
73 | object.id = id;
74 | object.imagePath = reader.readString(offsets[0]);
75 | object.text = reader.readString(offsets[1]);
76 | return object;
77 | }
78 |
79 | P _noteDeserializeProp(
80 | IsarReader reader,
81 | int propertyId,
82 | int offset,
83 | Map> allOffsets,
84 | ) {
85 | switch (propertyId) {
86 | case 0:
87 | return (reader.readString(offset)) as P;
88 | case 1:
89 | return (reader.readString(offset)) as P;
90 | default:
91 | throw IsarError('Unknown property with id $propertyId');
92 | }
93 | }
94 |
95 | Id _noteGetId(Note object) {
96 | return object.id;
97 | }
98 |
99 | List> _noteGetLinks(Note object) {
100 | return [];
101 | }
102 |
103 | void _noteAttach(IsarCollection col, Id id, Note object) {
104 | object.id = id;
105 | }
106 |
107 | extension NoteQueryWhereSort on QueryBuilder {
108 | QueryBuilder anyId() {
109 | return QueryBuilder.apply(this, (query) {
110 | return query.addWhereClause(const IdWhereClause.any());
111 | });
112 | }
113 | }
114 |
115 | extension NoteQueryWhere on QueryBuilder {
116 | QueryBuilder idEqualTo(Id id) {
117 | return QueryBuilder.apply(this, (query) {
118 | return query.addWhereClause(IdWhereClause.between(
119 | lower: id,
120 | upper: id,
121 | ));
122 | });
123 | }
124 |
125 | QueryBuilder idNotEqualTo(Id id) {
126 | return QueryBuilder.apply(this, (query) {
127 | if (query.whereSort == Sort.asc) {
128 | return query
129 | .addWhereClause(
130 | IdWhereClause.lessThan(upper: id, includeUpper: false),
131 | )
132 | .addWhereClause(
133 | IdWhereClause.greaterThan(lower: id, includeLower: false),
134 | );
135 | } else {
136 | return query
137 | .addWhereClause(
138 | IdWhereClause.greaterThan(lower: id, includeLower: false),
139 | )
140 | .addWhereClause(
141 | IdWhereClause.lessThan(upper: id, includeUpper: false),
142 | );
143 | }
144 | });
145 | }
146 |
147 | QueryBuilder idGreaterThan(Id id,
148 | {bool include = false}) {
149 | return QueryBuilder.apply(this, (query) {
150 | return query.addWhereClause(
151 | IdWhereClause.greaterThan(lower: id, includeLower: include),
152 | );
153 | });
154 | }
155 |
156 | QueryBuilder idLessThan(Id id,
157 | {bool include = false}) {
158 | return QueryBuilder.apply(this, (query) {
159 | return query.addWhereClause(
160 | IdWhereClause.lessThan(upper: id, includeUpper: include),
161 | );
162 | });
163 | }
164 |
165 | QueryBuilder idBetween(
166 | Id lowerId,
167 | Id upperId, {
168 | bool includeLower = true,
169 | bool includeUpper = true,
170 | }) {
171 | return QueryBuilder.apply(this, (query) {
172 | return query.addWhereClause(IdWhereClause.between(
173 | lower: lowerId,
174 | includeLower: includeLower,
175 | upper: upperId,
176 | includeUpper: includeUpper,
177 | ));
178 | });
179 | }
180 | }
181 |
182 | extension NoteQueryFilter on QueryBuilder {
183 | QueryBuilder idEqualTo(Id value) {
184 | return QueryBuilder.apply(this, (query) {
185 | return query.addFilterCondition(FilterCondition.equalTo(
186 | property: r'id',
187 | value: value,
188 | ));
189 | });
190 | }
191 |
192 | QueryBuilder idGreaterThan(
193 | Id value, {
194 | bool include = false,
195 | }) {
196 | return QueryBuilder.apply(this, (query) {
197 | return query.addFilterCondition(FilterCondition.greaterThan(
198 | include: include,
199 | property: r'id',
200 | value: value,
201 | ));
202 | });
203 | }
204 |
205 | QueryBuilder idLessThan(
206 | Id value, {
207 | bool include = false,
208 | }) {
209 | return QueryBuilder.apply(this, (query) {
210 | return query.addFilterCondition(FilterCondition.lessThan(
211 | include: include,
212 | property: r'id',
213 | value: value,
214 | ));
215 | });
216 | }
217 |
218 | QueryBuilder idBetween(
219 | Id lower,
220 | Id upper, {
221 | bool includeLower = true,
222 | bool includeUpper = true,
223 | }) {
224 | return QueryBuilder.apply(this, (query) {
225 | return query.addFilterCondition(FilterCondition.between(
226 | property: r'id',
227 | lower: lower,
228 | includeLower: includeLower,
229 | upper: upper,
230 | includeUpper: includeUpper,
231 | ));
232 | });
233 | }
234 |
235 | QueryBuilder imagePathEqualTo(
236 | String value, {
237 | bool caseSensitive = true,
238 | }) {
239 | return QueryBuilder.apply(this, (query) {
240 | return query.addFilterCondition(FilterCondition.equalTo(
241 | property: r'imagePath',
242 | value: value,
243 | caseSensitive: caseSensitive,
244 | ));
245 | });
246 | }
247 |
248 | QueryBuilder imagePathGreaterThan(
249 | String value, {
250 | bool include = false,
251 | bool caseSensitive = true,
252 | }) {
253 | return QueryBuilder.apply(this, (query) {
254 | return query.addFilterCondition(FilterCondition.greaterThan(
255 | include: include,
256 | property: r'imagePath',
257 | value: value,
258 | caseSensitive: caseSensitive,
259 | ));
260 | });
261 | }
262 |
263 | QueryBuilder imagePathLessThan(
264 | String value, {
265 | bool include = false,
266 | bool caseSensitive = true,
267 | }) {
268 | return QueryBuilder.apply(this, (query) {
269 | return query.addFilterCondition(FilterCondition.lessThan(
270 | include: include,
271 | property: r'imagePath',
272 | value: value,
273 | caseSensitive: caseSensitive,
274 | ));
275 | });
276 | }
277 |
278 | QueryBuilder imagePathBetween(
279 | String lower,
280 | String upper, {
281 | bool includeLower = true,
282 | bool includeUpper = true,
283 | bool caseSensitive = true,
284 | }) {
285 | return QueryBuilder.apply(this, (query) {
286 | return query.addFilterCondition(FilterCondition.between(
287 | property: r'imagePath',
288 | lower: lower,
289 | includeLower: includeLower,
290 | upper: upper,
291 | includeUpper: includeUpper,
292 | caseSensitive: caseSensitive,
293 | ));
294 | });
295 | }
296 |
297 | QueryBuilder imagePathStartsWith(
298 | String value, {
299 | bool caseSensitive = true,
300 | }) {
301 | return QueryBuilder.apply(this, (query) {
302 | return query.addFilterCondition(FilterCondition.startsWith(
303 | property: r'imagePath',
304 | value: value,
305 | caseSensitive: caseSensitive,
306 | ));
307 | });
308 | }
309 |
310 | QueryBuilder imagePathEndsWith(
311 | String value, {
312 | bool caseSensitive = true,
313 | }) {
314 | return QueryBuilder.apply(this, (query) {
315 | return query.addFilterCondition(FilterCondition.endsWith(
316 | property: r'imagePath',
317 | value: value,
318 | caseSensitive: caseSensitive,
319 | ));
320 | });
321 | }
322 |
323 | QueryBuilder imagePathContains(
324 | String value,
325 | {bool caseSensitive = true}) {
326 | return QueryBuilder.apply(this, (query) {
327 | return query.addFilterCondition(FilterCondition.contains(
328 | property: r'imagePath',
329 | value: value,
330 | caseSensitive: caseSensitive,
331 | ));
332 | });
333 | }
334 |
335 | QueryBuilder imagePathMatches(
336 | String pattern,
337 | {bool caseSensitive = true}) {
338 | return QueryBuilder.apply(this, (query) {
339 | return query.addFilterCondition(FilterCondition.matches(
340 | property: r'imagePath',
341 | wildcard: pattern,
342 | caseSensitive: caseSensitive,
343 | ));
344 | });
345 | }
346 |
347 | QueryBuilder imagePathIsEmpty() {
348 | return QueryBuilder.apply(this, (query) {
349 | return query.addFilterCondition(FilterCondition.equalTo(
350 | property: r'imagePath',
351 | value: '',
352 | ));
353 | });
354 | }
355 |
356 | QueryBuilder imagePathIsNotEmpty() {
357 | return QueryBuilder.apply(this, (query) {
358 | return query.addFilterCondition(FilterCondition.greaterThan(
359 | property: r'imagePath',
360 | value: '',
361 | ));
362 | });
363 | }
364 |
365 | QueryBuilder textEqualTo(
366 | String value, {
367 | bool caseSensitive = true,
368 | }) {
369 | return QueryBuilder.apply(this, (query) {
370 | return query.addFilterCondition(FilterCondition.equalTo(
371 | property: r'text',
372 | value: value,
373 | caseSensitive: caseSensitive,
374 | ));
375 | });
376 | }
377 |
378 | QueryBuilder textGreaterThan(
379 | String value, {
380 | bool include = false,
381 | bool caseSensitive = true,
382 | }) {
383 | return QueryBuilder.apply(this, (query) {
384 | return query.addFilterCondition(FilterCondition.greaterThan(
385 | include: include,
386 | property: r'text',
387 | value: value,
388 | caseSensitive: caseSensitive,
389 | ));
390 | });
391 | }
392 |
393 | QueryBuilder textLessThan(
394 | String value, {
395 | bool include = false,
396 | bool caseSensitive = true,
397 | }) {
398 | return QueryBuilder.apply(this, (query) {
399 | return query.addFilterCondition(FilterCondition.lessThan(
400 | include: include,
401 | property: r'text',
402 | value: value,
403 | caseSensitive: caseSensitive,
404 | ));
405 | });
406 | }
407 |
408 | QueryBuilder textBetween(
409 | String lower,
410 | String upper, {
411 | bool includeLower = true,
412 | bool includeUpper = true,
413 | bool caseSensitive = true,
414 | }) {
415 | return QueryBuilder.apply(this, (query) {
416 | return query.addFilterCondition(FilterCondition.between(
417 | property: r'text',
418 | lower: lower,
419 | includeLower: includeLower,
420 | upper: upper,
421 | includeUpper: includeUpper,
422 | caseSensitive: caseSensitive,
423 | ));
424 | });
425 | }
426 |
427 | QueryBuilder textStartsWith(
428 | String value, {
429 | bool caseSensitive = true,
430 | }) {
431 | return QueryBuilder.apply(this, (query) {
432 | return query.addFilterCondition(FilterCondition.startsWith(
433 | property: r'text',
434 | value: value,
435 | caseSensitive: caseSensitive,
436 | ));
437 | });
438 | }
439 |
440 | QueryBuilder textEndsWith(
441 | String value, {
442 | bool caseSensitive = true,
443 | }) {
444 | return QueryBuilder.apply(this, (query) {
445 | return query.addFilterCondition(FilterCondition.endsWith(
446 | property: r'text',
447 | value: value,
448 | caseSensitive: caseSensitive,
449 | ));
450 | });
451 | }
452 |
453 | QueryBuilder textContains(String value,
454 | {bool caseSensitive = true}) {
455 | return QueryBuilder.apply(this, (query) {
456 | return query.addFilterCondition(FilterCondition.contains(
457 | property: r'text',
458 | value: value,
459 | caseSensitive: caseSensitive,
460 | ));
461 | });
462 | }
463 |
464 | QueryBuilder textMatches(String pattern,
465 | {bool caseSensitive = true}) {
466 | return QueryBuilder.apply(this, (query) {
467 | return query.addFilterCondition(FilterCondition.matches(
468 | property: r'text',
469 | wildcard: pattern,
470 | caseSensitive: caseSensitive,
471 | ));
472 | });
473 | }
474 |
475 | QueryBuilder textIsEmpty() {
476 | return QueryBuilder.apply(this, (query) {
477 | return query.addFilterCondition(FilterCondition.equalTo(
478 | property: r'text',
479 | value: '',
480 | ));
481 | });
482 | }
483 |
484 | QueryBuilder textIsNotEmpty() {
485 | return QueryBuilder.apply(this, (query) {
486 | return query.addFilterCondition(FilterCondition.greaterThan(
487 | property: r'text',
488 | value: '',
489 | ));
490 | });
491 | }
492 | }
493 |
494 | extension NoteQueryObject on QueryBuilder {}
495 |
496 | extension NoteQueryLinks on QueryBuilder {}
497 |
498 | extension NoteQuerySortBy on QueryBuilder {
499 | QueryBuilder sortByImagePath() {
500 | return QueryBuilder.apply(this, (query) {
501 | return query.addSortBy(r'imagePath', Sort.asc);
502 | });
503 | }
504 |
505 | QueryBuilder sortByImagePathDesc() {
506 | return QueryBuilder.apply(this, (query) {
507 | return query.addSortBy(r'imagePath', Sort.desc);
508 | });
509 | }
510 |
511 | QueryBuilder sortByText() {
512 | return QueryBuilder.apply(this, (query) {
513 | return query.addSortBy(r'text', Sort.asc);
514 | });
515 | }
516 |
517 | QueryBuilder sortByTextDesc() {
518 | return QueryBuilder.apply(this, (query) {
519 | return query.addSortBy(r'text', Sort.desc);
520 | });
521 | }
522 | }
523 |
524 | extension NoteQuerySortThenBy on QueryBuilder {
525 | QueryBuilder thenById() {
526 | return QueryBuilder.apply(this, (query) {
527 | return query.addSortBy(r'id', Sort.asc);
528 | });
529 | }
530 |
531 | QueryBuilder thenByIdDesc() {
532 | return QueryBuilder.apply(this, (query) {
533 | return query.addSortBy(r'id', Sort.desc);
534 | });
535 | }
536 |
537 | QueryBuilder thenByImagePath() {
538 | return QueryBuilder.apply(this, (query) {
539 | return query.addSortBy(r'imagePath', Sort.asc);
540 | });
541 | }
542 |
543 | QueryBuilder thenByImagePathDesc() {
544 | return QueryBuilder.apply(this, (query) {
545 | return query.addSortBy(r'imagePath', Sort.desc);
546 | });
547 | }
548 |
549 | QueryBuilder thenByText() {
550 | return QueryBuilder.apply(this, (query) {
551 | return query.addSortBy(r'text', Sort.asc);
552 | });
553 | }
554 |
555 | QueryBuilder thenByTextDesc() {
556 | return QueryBuilder.apply(this, (query) {
557 | return query.addSortBy(r'text', Sort.desc);
558 | });
559 | }
560 | }
561 |
562 | extension NoteQueryWhereDistinct on QueryBuilder {
563 | QueryBuilder distinctByImagePath(
564 | {bool caseSensitive = true}) {
565 | return QueryBuilder.apply(this, (query) {
566 | return query.addDistinctBy(r'imagePath', caseSensitive: caseSensitive);
567 | });
568 | }
569 |
570 | QueryBuilder distinctByText(
571 | {bool caseSensitive = true}) {
572 | return QueryBuilder.apply(this, (query) {
573 | return query.addDistinctBy(r'text', caseSensitive: caseSensitive);
574 | });
575 | }
576 | }
577 |
578 | extension NoteQueryProperty on QueryBuilder {
579 | QueryBuilder idProperty() {
580 | return QueryBuilder.apply(this, (query) {
581 | return query.addPropertyName(r'id');
582 | });
583 | }
584 |
585 | QueryBuilder imagePathProperty() {
586 | return QueryBuilder.apply(this, (query) {
587 | return query.addPropertyName(r'imagePath');
588 | });
589 | }
590 |
591 | QueryBuilder textProperty() {
592 | return QueryBuilder.apply(this, (query) {
593 | return query.addPropertyName(r'text');
594 | });
595 | }
596 | }
597 |
--------------------------------------------------------------------------------
/06. CRUD Local Database/README.md:
--------------------------------------------------------------------------------
1 | # 06. CRUD Local Database
2 |
3 | [Previous](/05.%20Form/) | [Main Page](/) | [Next](/07.%20Relational%20Table/)
4 |
5 | ## Content Outline
6 |
7 | - [Setup](#setup)
8 | - [Note](#note)
9 | - [Note Database](#note-database)
10 | - [Initialization](#initialization)
11 | - [Create](#create)
12 | - [Read](#read)
13 | - [Update](#update)
14 | - [Delete](#delete)
15 | - [CRUD Page](#crud-page)
16 |
17 | ## Setup
18 |
19 | In this module, we will try to create a Note CRUD application using local database storage. First, we will prepare the main pages: `main.dart` and `notes_page.dart`.
20 |
21 | ```dart
22 | // main.dart
23 | import 'package:flutter/material.dart';
24 |
25 | void main() async {
26 |
27 | runApp(const MyApp());
28 | }
29 |
30 | class MyApp extends StatelessWidget {
31 | const MyApp({super.key});
32 |
33 | // This widget is the root of your application.
34 | @override
35 | Widget build(BuildContext context) {
36 | return const MaterialApp(
37 | debugShowCheckedModeBanner: false,
38 | home: NotesPage()
39 | );
40 | }
41 | }
42 |
43 | ```
44 |
45 | ```dart
46 | // pages/notes_page.dart
47 | import 'package:flutter/material.dart';
48 |
49 | class NotesPage extends StatefulWidget {
50 | const NotesPage({super.key});
51 |
52 | @override
53 | State createState() => _NotesPageState();
54 | }
55 |
56 | class _NotesPageState extends State {
57 |
58 |
59 | @override
60 | Widget build(BuildContext context) {
61 |
62 | return Scaffold(
63 | appBar: AppBar(title: const Text('Notes')),
64 | floatingActionButton: FloatingActionButton(
65 | onPressed: createNote,
66 | child: const Icon(Icons.add),
67 | ),);
68 | }
69 | }
70 | ```
71 |
72 | Next, we will prepare which local database to use. This time, we will try using Isar. You can adjust it with another local database. First, we add the dependencies as follows:
73 |
74 | ```sh
75 | flutter pub add isar isar_flutter_libs path_provider
76 | flutter pub add -d isar_generator build_runner
77 | flutter pub add provider
78 | ```
79 |
80 | ## Note
81 |
82 | Once the dependencies are ready, we will create the `Note` class using Isar.
83 |
84 | ```dart
85 | // models/note.dart
86 | import 'package:isar/isar.dart';
87 |
88 | // this line is needed to generate file
89 | // then run dart run build_runner build
90 | part 'note.g.dart';
91 |
92 | @Collection()
93 | class Note {
94 | Id id = Isar.autoIncrement;
95 | late String text;
96 | }
97 | ```
98 |
99 | In `Note`, there are only two properties: `id` and `text` as the collection. We need to include `part 'note.g.dart';` so that this file can be modified to run in Flutter. Once Note is ready, please run `dart run build_runner build` in the terminal.
100 |
101 | ## Note Database
102 |
103 | Next, we will create the database. Prepare `note_database.dart`.
104 |
105 | ```dart
106 | // models/note_database.dart
107 | import 'package:crud_local_database_app/models/note.dart';
108 | import 'package:flutter/cupertino.dart';
109 | import 'package:isar/isar.dart';
110 | import 'package:path_provider/path_provider.dart';
111 | import 'dart:io';
112 |
113 | class NoteDatabase extends ChangeNotifier{
114 | static late Isar isar;
115 |
116 | // INIT
117 |
118 | // list
119 | final List currentNotes = [];
120 |
121 | // create
122 |
123 | // read
124 |
125 | // update
126 |
127 | // delete
128 |
129 | }
130 | ```
131 |
132 | We will implement each part for initializing the database, and the create, read, update, and delete features.
133 |
134 | ### Initialization
135 |
136 | Initialization is needed so that the Isar database can be registered in the device directory location. Please add this code in the INIT section.
137 |
138 | ```dart
139 | // INIT
140 | static Future initialize() async {
141 | if (Platform.isAndroid) { // Check if it's Android
142 | final dir = await getApplicationDocumentsDirectory();
143 | isar = await Isar.open([NoteSchema], directory: dir.path);
144 | } else {
145 | // Handle other platforms or provide a default directory
146 | final dir = getTemporaryDirectory(); // Example for other platforms
147 | isar = await Isar.open([NoteSchema], directory: (await dir).path);
148 | }
149 | }
150 |
151 | ```
152 |
153 | ### Create
154 |
155 | Create is a method to add or create a new object (in this case: Note).
156 |
157 | ```dart
158 | // create
159 | Future addNote(String textFromUser) async {
160 | // create a new object
161 | final newNote = Note()..text = textFromUser;
162 |
163 | // save to db
164 | await isar.writeTxn(() => isar.notes.put(newNote));
165 |
166 | // re-read from db
167 | fetchNotes();
168 | }
169 | ```
170 |
171 | This function will process `textFromUser` into the `text` property of `Note`. `isar.writeTxn` is a function to run a write transaction to store `newNote` into Isar’s `notes`.
172 |
173 | ### Read
174 |
175 | `fetchNotes` is a function to retrieve note data from Isar into the `currentNotes` variable, which will be used in the app interface. Here is the code.
176 |
177 | ```dart
178 | // read
179 | Future fetchNotes() async {
180 | List fetchedNotes = await isar.notes.where().findAll();
181 | currentNotes.clear();
182 | currentNotes.addAll(fetchedNotes);
183 | notifyListeners();
184 | }
185 | ```
186 |
187 | This function will get all notes into the `fetchedNotes` variable and then store them in `currentNotes`, and signal that `currentNotes` has been updated.
188 |
189 | ### Update
190 |
191 | Next is the function to update a note. Here is the code:
192 |
193 | ```dart
194 | // update
195 | Future updateNote(int id, String newText) async {
196 | final existingNote = await isar.notes.get(id);
197 | if (existingNote != null) {
198 | existingNote.text = newText;
199 | await isar.writeTxn(() => isar.notes.put(existingNote));
200 | await fetchNotes();
201 | }
202 | }
203 | ```
204 |
205 | This function will retrieve a `Note` by its `id`. If available, it will update the note using `writeTxn`. Don’t forget to also update `currentNotes` using the `fetchNotes` function.
206 |
207 | ### Delete
208 |
209 | This function is to delete a note based on its id. Don’t forget to also update `currentNotes` using the `fetchNotes` function.
210 |
211 | ```dart
212 | // delete
213 | Future deleteNote(int id) async {
214 | await isar.writeTxn(() => isar.notes.delete(id));
215 | await fetchNotes();
216 | }
217 | ```
218 |
219 | Complete code as follows:
220 |
221 | ```dart
222 | import 'package:crud_local_database_app/models/note.dart';
223 | import 'package:flutter/cupertino.dart';
224 | import 'package:isar/isar.dart';
225 | import 'package:path_provider/path_provider.dart';
226 | import 'dart:io';
227 |
228 | class NoteDatabase extends ChangeNotifier{
229 | static late Isar isar;
230 |
231 | // INIT
232 | static Future initialize() async {
233 | if (Platform.isAndroid) { // Check if it's Android
234 | final dir = await getApplicationDocumentsDirectory();
235 | isar = await Isar.open([NoteSchema], directory: dir.path);
236 | } else {
237 | // Handle other platforms or provide a default directory
238 | final dir = getTemporaryDirectory(); // Example for other platforms
239 | isar = await Isar.open([NoteSchema], directory: (await dir).path);
240 | }
241 | }
242 |
243 | // list
244 | final List currentNotes = [];
245 |
246 | // create
247 | Future addNote(String textFromUser) async {
248 | // create a new object
249 | final newNote = Note()..text = textFromUser;
250 |
251 | // save to db
252 | await isar.writeTxn(() => isar.notes.put(newNote));
253 |
254 | // re-read from db
255 | fetchNotes();
256 | }
257 | // read
258 | Future fetchNotes() async {
259 | List fetchedNotes = await isar.notes.where().findAll();
260 | currentNotes.clear();
261 | currentNotes.addAll(fetchedNotes);
262 | notifyListeners();
263 | }
264 | // update
265 | Future updateNote(int id, String newText) async {
266 | final existingNote = await isar.notes.get(id);
267 | if (existingNote != null) {
268 | existingNote.text = newText;
269 | await isar.writeTxn(() => isar.notes.put(existingNote));
270 | await fetchNotes();
271 | }
272 | }
273 | // delete
274 | Future deleteNote(int id) async {
275 | await isar.writeTxn(() => isar.notes.delete(id));
276 | await fetchNotes();
277 | }
278 | }
279 | ```
280 |
281 | ## CRUD Page
282 |
283 | Next, how do we call the database functions in the pages? This can be done using the following commands:
284 |
285 | ```dart
286 | context.read() // returns but no listening
287 | context.watch() // returns and listening
288 | ```
289 |
290 | These functions can directly call functions from the `NoteDatabase` class.
291 |
292 | We also need to prepare `final textController = TextEditingController();` to store the user’s text input.
293 |
294 | Let’s first create the page structure:
295 |
296 | ```dart
297 | @override
298 | Widget build(BuildContext context) {
299 | // note database
300 | final noteDatabase = context.watch();
301 |
302 | // current notes
303 | List currentNotes = noteDatabase.currentNotes;
304 |
305 | return Scaffold(
306 | appBar: AppBar(title: const Text('Notes')),
307 | floatingActionButton: FloatingActionButton(
308 | onPressed: createNote,
309 | child: const Icon(Icons.add),
310 | ),
311 | body: ListView.builder(
312 | itemCount: currentNotes.length,
313 | itemBuilder: (context, index) {
314 | // get individual note
315 | final note = currentNotes[index];
316 |
317 | // list tile UI
318 | return ListTile(
319 | title: Text(note.text),
320 | trailing: Row(
321 | mainAxisSize: MainAxisSize.min,
322 | children: [
323 | // edit button
324 | IconButton(
325 | onPressed: () => updateNote(note),
326 | icon: const Icon(Icons.edit)),
327 | // delete button
328 | IconButton(
329 | onPressed: () => deleteNote(note.id),
330 | icon: const Icon(Icons.delete))
331 | ],
332 | ),
333 | );
334 | },
335 | ));
336 | }
337 | ```
338 |
339 | We need `currentNotes` to get the existing notes from the database. Next, we will complete the `createNote`, `updateNote`, `deleteNote`, and `readNote` functions.
340 |
341 | The `createNote` function will create a form and add a note to `NoteDatabase` using the `addNote` function.
342 |
343 | ```dart
344 | // create a note
345 | void createNote() {
346 | showDialog(
347 | context: context,
348 | builder: (context) => AlertDialog(
349 | content: TextField(
350 | controller: textController,
351 | ),
352 | actions: [
353 | MaterialButton(
354 | onPressed: () {
355 | // add to db
356 | context.read().addNote(textController.text);
357 |
358 | // clear controller
359 | textController.clear();
360 |
361 | Navigator.pop(context);
362 | },
363 | child: const Text("Create"),
364 | )
365 | ],
366 | ),
367 | );
368 | }
369 | ```
370 |
371 | The `updateNote` function will take the `Note` to be edited by showing a form and then updating it using the `update` function.
372 |
373 | ```dart
374 | // update a note
375 | void updateNote(Note note) {
376 | textController.text = note.text;
377 | showDialog(
378 | context: context,
379 | builder: (context) => AlertDialog(
380 | title: Text("Update Note"),
381 | content: TextField(controller: textController),
382 | actions: [
383 | MaterialButton(
384 | onPressed: () {
385 | context
386 | .read()
387 | .updateNote(note.id, textController.text);
388 | // clear controller
389 | textController.clear();
390 |
391 | Navigator.pop(context);
392 | },
393 | child: const Text("Update"))
394 | ],
395 | ));
396 | }
397 | ```
398 |
399 | The `deleteNote` function will directly delete the selected note from the database using the `deleteNote` method.
400 |
401 | ```dart
402 | void deleteNote(int id) {
403 | context.read().deleteNote(id);
404 | }
405 | ```
406 |
407 | Lastly, there's the `readNotes` function which is used to retrieve data from the database into `currentNotes`.
408 |
409 | ```dart
410 | void readNotes() {
411 | context.read().fetchNotes(); // Use read instead of watch
412 | }
413 |
414 | ```
415 |
416 | This function will be called at the start of the program execution. You can use `initState()` as follows:
417 |
418 | ```dart
419 | @override
420 | void initState() {
421 | super.initState();
422 | readNotes();
423 | }
424 |
425 | ```
426 |
427 | The complete code as below:
428 |
429 | ```dart
430 | // pages/notes_page.dart
431 |
432 | import 'package:crud_local_database_app/models/note.dart';
433 | import 'package:crud_local_database_app/models/note_database.dart';
434 | import 'package:flutter/material.dart';
435 | import 'package:provider/provider.dart';
436 |
437 | class NotesPage extends StatefulWidget {
438 | const NotesPage({super.key});
439 |
440 | @override
441 | State createState() => _NotesPageState();
442 | }
443 |
444 | class _NotesPageState extends State {
445 | // text controller to access what the user typed
446 | final textController = TextEditingController();
447 |
448 | @override
449 | void initState() {
450 | super.initState();
451 | readNotes();
452 | }
453 |
454 | // create a note
455 | void createNote() {
456 | showDialog(
457 | context: context,
458 | builder: (context) => AlertDialog(
459 | content: TextField(
460 | controller: textController,
461 | ),
462 | actions: [
463 | MaterialButton(
464 | onPressed: () {
465 | // add to db
466 | context.read().addNote(textController.text);
467 |
468 | // clear controller
469 | textController.clear();
470 |
471 | Navigator.pop(context);
472 | },
473 | child: const Text("Create"),
474 | )
475 | ],
476 | ),
477 | );
478 | }
479 |
480 | // read notes
481 | void readNotes() {
482 | context.read().fetchNotes(); // Use read instead of watch
483 | }
484 |
485 | // update a note
486 | void updateNote(Note note) {
487 | textController.text = note.text;
488 | showDialog(
489 | context: context,
490 | builder: (context) => AlertDialog(
491 | title: Text("Update Note"),
492 | content: TextField(controller: textController),
493 | actions: [
494 | MaterialButton(
495 | onPressed: () {
496 | context
497 | .read()
498 | .updateNote(note.id, textController.text);
499 | // clear controller
500 | textController.clear();
501 |
502 | Navigator.pop(context);
503 | },
504 | child: const Text("Update"))
505 | ],
506 | ));
507 | }
508 |
509 | // delete a note
510 | void deleteNote(int id) {
511 | context.read().deleteNote(id);
512 | }
513 |
514 | @override
515 | Widget build(BuildContext context) {
516 | // note database
517 | final noteDatabase = context.watch();
518 |
519 | // current notes
520 | List currentNotes = noteDatabase.currentNotes;
521 |
522 | return Scaffold(
523 | appBar: AppBar(title: const Text('Notes')),
524 | floatingActionButton: FloatingActionButton(
525 | onPressed: createNote,
526 | child: const Icon(Icons.add),
527 | ),
528 | body: ListView.builder(
529 | itemCount: currentNotes.length,
530 | itemBuilder: (context, index) {
531 | // get individual note
532 | final note = currentNotes[index];
533 |
534 | // list tile UI
535 | return ListTile(
536 | title: Text(note.text),
537 | trailing: Row(
538 | mainAxisSize: MainAxisSize.min,
539 | children: [
540 | // edit button
541 | IconButton(
542 | onPressed: () => updateNote(note),
543 | icon: const Icon(Icons.edit)),
544 | // delete button
545 | IconButton(
546 | onPressed: () => deleteNote(note.id),
547 | icon: const Icon(Icons.delete))
548 | ],
549 | ),
550 | );
551 | },
552 | ));
553 | }
554 | }
555 | ```
556 |
557 | In the `main` function, we need to initialize the Isar database at the beginning as follows:
558 |
559 | ```dart
560 | import 'package:crud_local_database_app/models/note_database.dart';
561 | import 'package:crud_local_database_app/pages/notes_page.dart';
562 | import 'package:provider/provider.dart';
563 | import 'package:flutter/material.dart';
564 |
565 |
566 | void main() async {
567 | WidgetsFlutterBinding.ensureInitialized();
568 | await NoteDatabase.initialize();
569 |
570 | runApp(
571 | ChangeNotifierProvider(
572 | create: (context) => NoteDatabase(),
573 | child: const MyApp(),
574 | )
575 | );
576 | }
577 |
578 | class MyApp extends StatelessWidget {
579 | const MyApp({super.key});
580 |
581 | // This widget is the root of your application.
582 | @override
583 | Widget build(BuildContext context) {
584 | return const MaterialApp(
585 | debugShowCheckedModeBanner: false,
586 | home: NotesPage()
587 | );
588 | }
589 | }
590 |
591 | ```
592 |
593 | ### Result:
594 |
595 | 
596 |
597 | If there is an error while running the app, try to replace `subproject` on `android/build.gradle` with this code.
598 |
599 | ```kts
600 | subprojects {
601 | afterEvaluate { project ->
602 | if (project.plugins.hasPlugin("com.android.application") ||
603 | project.plugins.hasPlugin("com.android.library")) {
604 | project.android {
605 | compileSdkVersion 34
606 | buildToolsVersion "34.0.0"
607 | }
608 | }
609 | if (project.hasProperty("android")) {
610 | project.android {
611 | if (namespace == null) {
612 | namespace project.group
613 | }
614 | }
615 | }
616 | }
617 | project.buildDir = "${rootProject.buildDir}/${project.name}"
618 | project.evaluationDependsOn(':app')
619 | }
620 | ```
621 |
622 | You can learn with other databases like Hive and ObjectBox with these references:
623 |
624 | - web article: https://blog.logrocket.com/comparing-hive-other-flutter-app-database-options & https://github.com/o-ifeanyi/db_benchmarks
625 | - Flutter Hive Database Tutorial: https://www.youtube.com/watch?v=FB9GpmL0Qe0
626 | - Flutter ObjectBox Database Tutorial: English version
627 | - Flutter SQFlite Database Tutorial: https://www.youtube.com/watch?v=bihC6ou8FqQ
628 | - Flutter Isar Database Tutorial : https://www.youtube.com/watch?v=jVgQ5esp-PE
629 |
--------------------------------------------------------------------------------