A simple notes app with Firebase Firestore and Provider in Flutter 2.0 (Part 2) — Creating the basic app state

Prashant Goyal
13 min readMar 24, 2021

Welcome back everyone to Part 2 of this series where I create a simple version of Google Keep in Flutter 2.0 using Firebase. If you missed the first part, here is a link to it: Part 1.

In this part, we will create the models for the notes and certain classes that will allow us to handle the notes saved by the user much more efficiently. Getting straight to it, we will begin by creating 3 new classes:
1. NoteModel: This will be the class that will contain data for each individual note.
2. NotesModel: This class will serve as a repository for all the notes that the user has and allow operations to manage the repository like adding a note, editing etc. Also, it will allow us to search for the notes based on a search query that the user provides.
3. AppState: This will be our go-to class for all the operations that we want to perform on the notes. Along with managing the notes, this class will also manage offline storage, online sync with Firebase and how those two interoperate when the user switches from offline to online. This will also keep track of the app’s theme using SharedPreferences.

After handling these 3 tasks, we will start working on the design for the small “Note Box” that appears on the home screen’s GridView also. Great! Now we have the goals for this article. Before beginning, I request everyone reading this article to mention any shortcomings in the way the app is structured in the comments here. All feedback will definitely be taken into account for future projects. With that said, let’s get to it!

A blueprint for every note

Let us begin with the easiest first. The NoteModel class. As described above, this will handle notes at the individual note level. To begin, we will create the following file: lib\models\note_model.dart . After this, let us put the following code into this file:

class NoteModel {
late String id;
String? noteTitle;
String noteContent;
int noteLabel;

NoteModel(
{this.noteTitle,
required this.noteContent,
this.noteLabel = 0,
this.id = "0"}) {
// ID must be editable because the note object can be created from DB.
if (this.id == "0") {
// This means a new note is created.
var uuid = Uuid();
id = uuid.v4();
}
}
}

This seems to be very simple. We have mandated that for every NoteModel object, the id, noteContent and noteLabel fields will always have a non-null value. And that makes sense. Right? Any note will have a unique id attached to it and must have some content. No need to keep track of an empty note with no content. Also, we can assume that a note will by default have a pre-assigned label. The only thing that is not required is a title. This is very similar to the functionality of Google Keep and modified slightly to suit our purposes.

Why an ID?

We need a unique id for each note. This will be helpful in the later stages when we add offline persistence and online sync functionality. Without a unique id, it will be difficult to keep track of notes in Firestore.
For generating a unique id, we are using the UUID package.

Keeping a track of every note the user creates

Now that we have dealt with the notes at the most basic level, let’s go up a level and start writing code to handle multiple notes. Since a user is able to add multiple notes, we need to have a list of nodes with the functionality to add more notes, delete notes etc. For this purpose, let's create the NotesModel class. It will have the following path: lib/models/noted_model.dart and the following code:

class NotesModel {
List<NoteModel> _notes = [];

int get notesCount => _notes.length;

NoteModel getNote(int index) => _notes[index];

void deleteNote({required NoteModel note}) {
NoteModel toRemove = _notes.firstWhere((element) => element.id == note.id);
_notes.remove(toRemove);
}

void saveNote(NoteModel note, [int? editIndex]) {
if (editIndex != null) {
// The note is being edited and not created.
_notes[editIndex].noteTitle = note.noteTitle;
_notes[editIndex].noteContent = note.noteContent;
_notes[editIndex].noteLabel = note.noteLabel;
} else {
_notes.add(note);
}
}

List<int> searchNotes(String searchQuery) {
/// Search for notes based on the contents/titles of the notes.
/// Since the whole rendering of note on NoteScreen is dependant upon
/// the index of the note, this method will return a list of indexes
/// of the relevant notes. Empty List if no match found.

List<int> relevantIndexes = [];
_notes.asMap().forEach((index, note) {
if (note.noteTitle!.contains(searchQuery) ||
note.noteContent.contains(searchQuery)) {
relevantIndexes.add(index);
}
});

return relevantIndexes;
}
}

This code is also simple enough. But, let us take a look at what it does. We will obviously keep a track of every NoteModel object in the _notes member which will be private to the class. It can be operated upon using only the methods that the class will expose.
There is a getter called notesCount that will return the number of notes in the _notes list. As we have already seen, one use case is the GridView on the HomeScreen. There, the widget requires the number of notes.
This class also has a simple getter getNotefor getting a note given its index.
The deleteNote() method takes a NoteModel object which it then uses to compare with each note in the _notes list and removes that particular note.
The saveNote() method also takes a NoteModel object along with an optional parameter called editIndex . This is because since the user is able to both edit and add new notes, the saveNote() method will handle both the functionality. If there is a value to the editIndex parameter, this means a note has to be edited, otherwise add a note to the list.
The final method is the searchNotes() method. This is simply to search for a note/group of notes based on the input search query. It will go through all the notes and check if either the title or the content of the note contains the search query and return a list of all the indexes where there is a match. This method has a very basic matching functionality for searching notes and this can be easily modified to implement more advanced searching like ignoring the case, handle whitespaces and whatever is required.

A singular point to manage everything

As is written earlier, we will have a single class to manage the notes in the phone’s memory, local storage and Firestore. At any point in time, the notes will be in two places out of the three mentioned here. The notes will always be in the phone’s memory when the app is running. Along with that, they will either be in a local SQLite database or in Firebase Firestore. Hence, for the sake of convenience, it seemed logical to just implement the code to handle all these operations in a single place instead of handling them all over the app whenever a note is edited, deleted etc.

To begin, we need to add some constants to the lib/constants.dart file. These are some SharedPreferences keys that will store the user’s theme preference and email for Firestore sync. The email will be used later. Here is what to add to the constants.dart file:

// Shared Preferences
final String kEmailKeySharedPreferences = 'userEmail';
final String kThemeKeySharedPreferences = 'appTheme';

Now, let’s start with creating another dart file at the following path: lib/app_state.dart . This will be the AppState class which will extend the ChangeNotifier class for the Provider module. Right now, we only need the following code in it:

import 'package:flutter/cupertino.dart';
import 'package:note_now/constants.dart';
import 'package:note_now/models/note_model.dart';
import 'package:note_now/models/notes_model.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AppState extends ChangeNotifier {
0 NotesModel notesModel = NotesModel();

bool isDarkTheme = true;

Future<void> initialization() async {
SharedPreferences prefs = await SharedPreferences.getInstance();

if (prefs.getBool(kThemeKeySharedPreferences) == null) {
// The first time app is run. Hence, set the theme to dark by default
prefs.setBool(kThemeKeySharedPreferences, isDarkTheme);
} else {
isDarkTheme = prefs.getBool(kThemeKeySharedPreferences)!;
}
}

void setThemeBoolean(bool isUsingDarkTheme) async {
// Call this whenever user toggles between light and dark theme.
this.isDarkTheme = isUsingDarkTheme;
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(kThemeKeySharedPreferences, isDarkTheme);
notifyListeners();
}

void saveNote(NoteModel newNote, [int? index]) {
// Right now, just add to NotesModel object. Handle DB and Firestore later.
this.notesModel.saveNote(newNote, index);
}

void deleteNote(NoteModel note) {
// Handle DB and Firestore later
this.notesModel.deleteNote(note: note);
}
}

The class has an object of the NotesModel class to keep a store of all the user’s notes in device memory. Apart from that, there is a boolean variable isDarkTheme . This is by default true because we expect the app to be in dark mode (I like it that way). There is the initialization() method that right now handles getting the SharedPreferences instance. The user’s theme choice will be stored in SharedPreferences and whenever the app is run, it checks if there is value for them in SharedPreferences. If not, then the app is run for the first time and set that to the dark theme by default. Otherwise, just read and set the app theme as required.
Then there is the setThemeBoolean() method that takes in a boolean variable. This simply updates the theme based on the user’s choice. This will be required later but since we are dealing with the theme right now, it seemed the right choice to just implement it straight away. The toggle and theme-specific colour changes will be done at the later portion of the series though.
Next, we have are the saveNote() and the deleteNote() methods. They simply, save and delete a note from the notesModel object of the AppState.

Now, let’s implement the first use case of our Provider package. This will simply be checking the theme settings and setting the app theme appropriately. For that, we need to open the main.dart file and make the following changes:

  1. Add an object for the AppState class. Use the late keyword to specify that it’s value will be initialized at a later point of time but before it is used.
late AppState appState;

2. Modify the main() function to be asynchronous, initialize the appState object and invoke it’s initialization() method.

void main() async {
WidgetsFlutterBinding.ensureInitialized();

/// [AppState] handles all the important stuff for firebase, offline sql
/// storage and the [NotesModel] object in the memory.
appState = AppState();
await appState.initialization();

runApp(MyApp());
}

3. Since we are using the Provider package, we will wrap the whole MaterialApp inside a ChangeNotifierProvider widget. This will allow us to listen to any changes in the appState object from anywhere within the application.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => appState,
child: Builder(
builder: (BuildContext context) => MaterialApp(
title: 'NoteNow', // This is the name of the application.
theme: Provider.of<AppState>(context).isDarkTheme
? ThemeData.dark().copyWith(
primaryColor: kDarkThemeBackgroundColor,
)
: ThemeData.light()
.copyWith(primaryColor: kLightThemeBackgroundColor),
home: HomeScreen(),
),
),
);
}
}

As of now, the app will by default start in dark mode since we have set that to be the case. But, just for experimenting purpose, we can modify that to see that this works and the app now supports light mode too by just toggling the isDarkTheme property of the AppState class.

Designing the “NoteBox”

We now have an AppState that will allow us to control the state of user’s notes from anywhere within the application. The next logical step seems to be … you are right! We should now start adding functionality to create notes. We will begin by creating a widget for the HomeScreen widget that will show some of the content of the notes to the user. This is because if we directly go to creating a note functionality, we won’t have any visual feedback of how the notes are appearing once they are added. Hence, we should first go and create widgets for the GridView on the HomeScreen and then, go to add, edit and delete notes functionality.

Here comes the “NoteBox”

I have been writing the word “NoteBox” many times now. This is exactly what we will call the widget that shows the note contents on the HomeScreen’s GridView. Here is a look at what we are going to create:

The NoteBoxes that will be shown on the HomeScreen

Just a look at any of the box tells us exactly how can we create them. All we need is a Container with rounded borders. Inside a container, we will have a column that will contain:
1. Title text
2. A divider
3. The content text
We will also take care of the theming right now. We will keep building the groundwork for theming so that as soon we add a toggle for switching themes, everything will be ready for it. Apart from theming, the data for NoteBox’s text widgets will also be directly from the AppState. Let’s get to it!

We will begin by creating a new file for the widget at this path: lib/widgets/note_box.dart . The file will have the following content:

class NoteBox extends StatelessWidget {
Color? labelColor;
final text;
final String? title;

NoteBox({required this.labelColor, required this.text, this.title});

@override
Widget build(BuildContext context) {
return Material(
elevation: 0,
borderRadius: BorderRadius.circular(6),
child: Consumer<AppState>(
builder: (context, appState, child) => Container(
clipBehavior: Clip.antiAlias,
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.8),
color: appState.isDarkTheme ? kNoteBackgroundColor : Colors.white,
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Row(
children: [
Icon(
Icons.star,
color: labelColor,
size: 14,
),
Flexible(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6),
child: Text(this.title ?? "Note",
maxLines: 1,
style: appState.isDarkTheme
? kNoteBoxNoteStyle.copyWith(
color: Colors.white)
: kNoteBoxNoteStyle,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start),
),
),
],
),
),
Divider(
color: appState.isDarkTheme ? Colors.white : Colors.grey,
thickness: 0.5,
),
],
),
Text(
text,
style: TextStyle(height: 1.2),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
maxLines: 8,
)
],
),
),
),
);
}
}

We have the complete widget is wrapped in a Material widget. This is done because is we need to play with the elevation, we can do it easily here. I liked it without any elevation. The title text is going to be a single line widget and the note’s content can have a maximum of eight lines. Extra content will be displayed when the user goes to the NoteScreen(you might have gotten an idea of what's coming).
Another important thing to notice is that certain elements have been coloured according to the theme of the application.

The IDE is now showing some red underlines. That is because we need to add some stuff to the lib/constants.dart file. It seemed better to slightly differentiate the colour of the NoteBox from the app’s background colour. Hence, that needs to be specified. Also, the TextStyle for title is also dependant upon the theme and hence, that needs to be specified too. We can do that easily by simply specifying the following lines in the lib/constants.dart file:

// Constants for the notes
const kNoteBackgroundColor = Color(0xFF353535);
const kNoteBoxNoteStyle =
TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500, letterSpacing: 1.1);

Integrating the NoteBox with the GridView

The only thing remaining with the NoteBox is to begin using it in the GridView on the HomeScreen. This will require some minor changes as we will now use the Provider package to access the AppState and get access to all the notes. We will also cover the case of when there are no notes to show (as is the case currently) and show a centred text widget stating the same. To begin, we need to navigate to lib/home_screen.dart and find the StaggeredGridView.countBuilder() and replace it with the following content:

Provider.of<AppState>(context).notesModel.notesCount != 0
? Consumer<AppState>(
builder: (context, appState, child) {
return StaggeredGridView.countBuilder(
crossAxisSpacing: 12,
mainAxisSpacing: 12,
padding:
EdgeInsets.symmetric(horizontal: 14, vertical: 8),
crossAxisCount: 4,
itemCount: appState.notesModel.notesCount,
physics: BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) =>
Hero(
tag: 'note_box_$index',
child: GestureDetector(
onTap: () {},
child: NoteBox(
title: appState.notesModel
.getNote(index)
.noteTitle !=
null
? appState.notesModel
.getNote(index)
.noteTitle
: "Note",
text: appState.notesModel
.getNote(index)
.noteContent,
labelColor: kLabelToColor[appState.notesModel
.getNote(index)
.noteLabel],
),
),
),
staggeredTileBuilder: (int index) =>
StaggeredTile.fit(2),
);
},
)
: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 50),
child: Text(
"You have not added any notes.\n\nPlease login again if you are a returning user.",
style: TextStyle(fontSize: 18),
),
),
)

Before building and running the application, we need to make a small albeit important addition to the constants.dart file. Open the file and add a map to it which maps the note’s label integer to a unique colour. This will be a map of 9 colours. Here is the map:

// Label colors
const Map<int, Color> kLabelToColor = {
0: Colors.lightBlueAccent,
1: Colors.redAccent,
2: Colors.purpleAccent,
3: Colors.greenAccent,
4: Colors.yellowAccent,
5: Colors.blueAccent,
6: Colors.orangeAccent,
7: Colors.pinkAccent,
8: Colors.tealAccent
};

We can now build and run the app. If we run the application now, we get the screen shown here. The text also includes the case when a returning user will be able to get their old notes from Firestore when they log in. This will come later on.

We could now add a dummy note manually to the AppState’s notesModel property and get a peek at what we built. I did that (just for testing purposes) and here is what we have on our hands right now:

The app with the NoteBox added

Our application’s progress

We have laid some very solid groundwork for our application in this part. By defining the AppState, making separate models for our notes at individual and collective levels, we have greatly simplified our future goals of integrating the offline and online storage functionality. As you will see, even the way of creating, deleting and editing a note will be way easier now.
We have also started using the Provider model to access the notes in the HomeScreen GridView. All in all, we have laid a very solid foundation to continue and build our app upon.

This is all for this one folks. In the next one, we will start creating the actual screen where we will show a note that has already been created. We will add the edit, delete and create note functionality from right there too. If at any point you need to have a look at the complete code, just visit the NoteNow Github Repository to do so.

Thank you for reading and stay tuned for the next part.

--

--