Nice to have you in this second part of the series of Build a note-taking app with Flutter + Firebase. If you haven’t read the previous article, please find it here.
In part I, we’ve built the first screen for the notebook app, Flutter Keep. In this article, we’re going to create a note editor with reversible operations supported, and explore the magical Hero animations.
The note editor
There’re many types of notes in Google Keep, including plain-text notes, audio notes, and checklists, with optional image attachments. However, in this example, we’ll focus on the plain-text editor to keep things simple.
The following is a preview of what we’re going to build:
In essence, it composites of two text fields, one for the title, another for the plain-text content. Plus, a top
AppBar and a
ModalBottomSheet provides actions to update either the state or the color of the note.
So let’s start with a
In the editor state, we keep a copy of the original note (could be an empty one) to check whether the editor is dirty.
And don’t forget to make it accessible from the
Ok, go on building the widget:
We’re going to save the note to FireStore before the editor is closed, by using a
WillPopScope widget, which fires the
onWillPop callback right before the current screen is being dismissed.
ChangeNotifierProvider is to provide the
Note object being edited to descendant widgets to keep them aligned with the latest state. For example, the editor should update the background color when the user picks a different one in the
Note class has to extend the
ChangeNotifier, and to
notifyListeners whenever its state has changed:
We’ll see how it works.
Actions like archiving and picking a theme color are organized in a bottom sheet.
One thing that may be confusing is when we
showModalBottomSheet, we have to provide the note object again, or the descendant widgets (of the bottom sheet) won’t be able to retrieve it via
Provider.of. (It’s a different widget tree from the editor body)
To understand how a
ChangeNotifierProvider works, we take the color picker as an example.
What does the
LinearColorPicker do? It renders a horizontal list of available tints for a note:
When one of the tints is selected, the picker updates the
color property of the note, and that is it.
An ancestor widget (of the
ColorPicker) we’ve built previously, which watching the note, gets notified and then refreshes the screen so that we can see the whole editor is tinted with the color we pick.
The other actions, such as deletion and archiving, share the same mechanism.
We can now edit a note by updating the properties including the state. But how about the UX? What if users delete a note by accident?
For dangerous operations like deleting or archiving, a
SnackBar could be used to provide a reverse action, e.g., restoring or unarchiving, in addition to a prompt message.
To implement reversible operations more cleanly, we’re going to apply the Command Pattern.
First, we define the command interface, which is responsible for applying an action to a note.
For this notebook app, actions considered reversible are all about mutating the state of a note. So only one concrete command is needed. However, nothing prevents you from extending it to other situations.
Next, we produce and consume commands, for example:
Done, we’ve made the dangerous operations reversible!
Now we have a working note editor, let’s take a step further, by adding a beautiful transition animation between the
HomeScreen and the
We can see that a note item grows to the size of the entire screen, from where it located in the grid. In Flutter, that’s called a Hero Animation.
The usage is simple. First, we wrap the note item in the grid or list with a
Then wrap the editor widget too:
In the above snippets, the two tags passed to the
Hero widgets must be identical.
DefaultTextStyle widgets are applied to avoid the big underlined text during screen transition on the iOS platform.
Now we can leave the rest to the Flutter framework.
What noticeable is that the standard screen transition animation is platform-specific. You could make a custom transition as you need, but it is beyond the scope of this article. Please refer to this cookbook.
In an example app, we don’t bother to apply patterns like BLOC. But there are still ways to keep the code clean and avoid boilerplates.
For example, we can move the reversible operation handling procedure to a stand-alone mixin, to make a cleaner separation between UI and logic, and also make it reusable.
Whenever you need to handle commands, just mix it in:
In addition, with Dart SDK
2.7.0 or later, we can leverage extension methods to do things magical.
We can augment the
Note model with FireStore related functionalities:
Which makes persisting a note as easy as a method call:
We can even add properties and methods to an enumeration, which is impossible in the declaration of enumerated types:
That saves us a lot of repeated code!
Wrapping it up, we’ve delivered a working plain-text note editor in this iteration. We’ve even added features like reversible actions and Hero transitions. Please find the complete code example in this GitHub repo.
In the next part, I’d like to introduce how to query different subsets of notes from FireStore, and the issue of composite indexes.
Thank you for reading! 🙌