Note-taking is an essential part of learning. It involves users writing down everything they hear and read. Research has shown that note-takers remember more essential ideas and retain knowledge.
In this post, we will learn how to create a note-taking mobile application using Flutter. This application doesn’t require a custom backend server.
Prerequisites
To fully grasp the concepts presented in this tutorial, we require the following:
- Basic understanding of Dart and Flutter
- Flutter SDK installed
- Xcode with developer account (for Mac users)
- Either IOS Simulator, Android Studio, or Chrome web browser to run our application
- Docker installation
Appwrite is a development platform that provides a powerful API and management console for building backend servers for web and mobile applications.
Getting Started
In this post, we will focus on implementations only. The project UI has already been set up. Design resources used are also available here.
To get started, we need to clone the project by navigating to the desired directory and running the command below:
git clone https://github.com/Mr-Malomz/note_app.git && cd note_app
The complete source code is also available on the dev branch of the same repository.
Folder Structure
Let’s go over some of the key directories and files:
-
screens:
a folder to store the screens of our application -
utils:
a folder to store reusable classes -
widgets:
a folder to store building blocks of our application -
main.dart:
the entry point of our application ## Running the Project
First, we need to install the project dependencies by running the command below:
flutter pub get
Then run the project using the command below:
flutter run
The command above will run the application on the selected device.
Setting up Appwrite
Appwrite is a development platform that provides a powerful API and management console for building backend servers for web and mobile applications. We’ll use Appwrite to manage all backend logic including storage.
To set up our backend services using Appwrite, we first need to start up Docker, navigate to the desired directory, and then install Appwrite on our machines using any of the applicable commands below:
Unix command (Mac/Linux PC)
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.13.4
Windows command (Windows PC)
docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.13.4
Powershell (PCs running Powershell)
docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.13.4
The command will ask us some questions on how to configure our application. We can answer the questions as shown below:
Choose your server HTTP port: (default: 80): <PRESS ENTER>
Choose your server HTTPS port: (default: 443): <PRESS ENTER>
Choose a secret API key, make sure to make a backup of your key in a secure location (default: 'your-secret-key'): <PRESS ENTER>
Enter your Appwrite hostname (default: 'localhost'): <PRESS ENTER>
Enter a DNS A record hostname to serve as a CNAME for your custom domains.
You can use the same value as used for the Appwrite hostname. (default: 'localhost'): <PRESS ENTER>
The selected options will install and run Appwrite on our machine. We test our application by opening the URL below on our browser.
PS: Installation and running might take some time.
http://localhost:80
Creating a new project
To create a project, we need to create a new account by signing up. On the console, click on the Create Project button, input flutter_appwrite
as the name, and click Create.
Next, we need to create a database to save our notes. Navigate to the Database tab, click on Add Collection, input flutter_appwrite_col
as the collection name, and click on Create.
Appwrite has an advanced yet flexible way to manage access to users, teams, or roles to access specific resources. We will modify the permission role:all
to enable access from any application. Then click on Update to save changes.
Add Attributes
Attributes are fields that our database will have. Navigate to the Attributes tab, click on Add Attributes, add a string attribute for both title and note fields, mark as required, and click on Create.
Add Sample Data
To get a feel of our database, we can add sample data by navigating to the Documents tab, clicking on Add Document, inputting the required fields, and clicking on Create.
Integrating Appwrite with Flutter.
To add support for our Flutter app, navigate to the Home menu, click on Add Platform button, and select New Flutter App.
Depending on the device we are running our Flutter application on, we can modify it as shown below:
IOS
To get our Bundle ID, we can navigate using the path below, open the project.pbxproj
file, and search for PRODUCT_BUNDLE_IDENTIFIER
.
ios > Runner.xcodeproj > project.pbxproj
Next, open the project directory on Xcode, open the Runner.xcworkspace
folder in the app's iOS folder, select the Runner project in the Xcode project navigator, select Runner target in the main menu sidebar, and select IOS 11 in the deployment info’s target.
Android
To get our package name, we can navigate using the path below, open the AndroidManifest.xml
file, and copy the package
value.
android > app > src > debug > AndroidManifest.xml
Next, we need to modify the AndroidManifext.xml
as shown below:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.note_app">
<uses-permission android:name="android.permission.INTERNET"/>
<application ...>
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" android:exported="true">
<intent-filter android:label="flutter_web_auth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="appwrite-callback-[PROJECT_ID]" />
</intent-filter>
</activity>
</application>
</manifest>
We also need to modify the highlighted code with our Appwrite’s Project ID. Navigate to the Settings menu to get the Project ID.
We can learn more about Appwrite’s Flutter platform-specific support here.
Install Appwrite Flutter SDK
Next, we need to install the required dependency by navigating to the root directory, open the pubspec.yaml
file, and then add the Appwrite’s SDK to the dependency
section.
appwrite: ^4.0.2
PS: An editor like Visual Studio Code automatically installs the dependencies for us when we save the file. We might need to stop our project and run *flutter pub get*
to install the dependency manually for other editors.
Connecting Appwrite with Flutter
Next, we need to navigate the utils
folder inside the lib
directory and create a setup.dart
file, and add the snippet below:
class AppConstant {
final String projectId = "REPLACE WITH YOUR PROJECT ID";
final String endpoint = "REPLACE WITH YOUR ENPOINT";
final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
}
Navigate to the settings menu for the project and database to copy the Project ID, API Endpoint, and Collection ID.
For the endpoint
property, we need to modify it to work with our system's local network address. We can adjust accordingly:
IOS
Navigate to the Network section, copy the IP address, and modify as shown below:
class AppConstant {
final String projectId = "REPLACE WITH YOUR PROJECT ID";
final String endpoint = "http://192.168.1.195/v1";
final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
}
Android
We can connect our Android emulator to the system’s IP using 10.0.2.2
IP address.
class AppConstant {
final String projectId = "REPLACE WITH YOUR PROJECT ID";
final String endpoint = "http://10.0.2.2/v1";
final String collectionId = "REPLACE WITH YOUR COLLECTION ID";
}
Creating a Model
Next, we need to create a model to convert the response sent from Appwrite to a Dart object. The model will also cater to JSON serialization. To do this, create a models
folder inside the lib
directory; in this folder, create a note_model.dart
file and add the snippet below:
class Note {
final String? $id;
final String title;
final String note;
Note({this.$id, required this.title, required this.note});
factory Note.fromJson(Map<dynamic, dynamic> json) {
return Note($id: json['\$id'], title: json['title'], note: json['note']);
}
Map<dynamic, dynamic> toJson() {
return {'title': title, 'note': note};
}
}
The snippet above does the following:
- Creates a
Note
class with required properties - Adds a constructor with unrequired and required parameters
- Create a
fromJson
andtoJson
method for JSON serialization
PS: factory
in dart lets us return an existing class instance instead of creating a new one. This process helps us improve application performance.
Creating a Service
One of the best practices when writing reusable and maintainable code is to use a service. A service helps separate the application core logic from the UI. To do this, we need to navigate to the utils
folder, and in this folder, create a note_service.dart
file and add the snippet below:
The snippet above does the following:
- Imports the required dependencies
- Creates a
NoteService
class withclient
anddb
properties to connect to the Appwrite instance and the database, respectively - Creates an
_init
method that configures theclient
and thedb
instances using theAppConstants
defined earlier and initializes the method in the class constructor - Creates a
getAllNotes
method that uses the configured database’slistDocuments
function to get a list of notes and converts the returned JSON to a list using theNote.fromJson
method - Creates a
createNote
method that takes in thetitle
andnote
parameter and uses thecreateDocument
function to create a note which creates an instance of the note using the parameters, passes in theunique()
flag as thedocumentId
; which tells Appwrite to auto-generate a unique ID, and passes in the new note by converting the Dart object to JSON using thenewNote.toJson()
method - Creates a
getANote
method that takes in anid
as a parameter and uses thegetDocument
function to get the matching note and converts the returned JSON to a Dart object using theNote.fromJson
method - Creates a
updateNote
method that takes in thetitle
,note
, andid
parameter and uses theupdateDocument
function to update a note; this creates an instance of the note using thetitle
andnote
parameter, passes in theid
as thedocumentId
, and passes in the updated note by converting the Dart object to JSON using thenewNote.toJson()
method - Creates a
deleteNote
method that takes in anid
as a parameter and uses thedeleteDocument
function to delete the matching note
PS: The question mark *?*
and bang *!*
operator used tells the compiler to relax the non-null constraint error (Meaning the parameter can be null)
Consuming the Service
With that done, we can start using the service to perform the required operation.
Get All Notes
To get the list of notes, we need to navigate to the screens
folder, open home.dart
and modify as shown below:
The snippet above does the following:
- Imports the required dependencies
- Creates a
notes
,_isLoading
, and_isError
properties to manage the application state - Creates a
_getNoteList
method to get the list of notes using thegetAllNotes
service and sets states accordingly - Uses the
initState
method to call the_getNoteList
method when the object is inserted into the tree - Conditionally renders the notes based on current states and pass in the current index of the
notes
as an argument to theNoteCard
The compiler will complain about a missing constructor property on the NoteCard
widget, which we will fix in the next step.
Next, we need to update the NoteCard
widget by navigating to the widgets
folder, and in this folder, open card.dart
file and modify the snippet to the following:
The snippet above does the following:
- Imports the model class
- Creates a
note
property and adds it as a required parameter to the constructor - Modifies the UI widget to show the
title
andnote
text dynamically
Create Note
To create a note, we need to modify manage_note.dart
file in the screens
folder to the following:
First, we need to import required dependencies and create a _title
and _note
variable to control inputs.
Next, we need to create a _createNote
method that uses the createNote
service to create a note, navigate appropriately, set states, and uses the snackbar
to show the action performed.
Finally, we need to modify the form widgets by adding controllers to control text on both input fields (Line 31 & Line 74) and call the _createNote
function when the save button is pressed.
The snippet above does the following:
- Imports required dependencies
- Creates a
_title
and_note
variable to control the inputs - Creates a
_createNote
method that uses thecreateNote
service to create a note, navigate appropriately, set states, and uses thesnackbar
to show the action performed - Line 88 & Line 131: add controllers to control text on both input fields
- Calls the
_createNote
function when the save button is pressed
Get A Note
To get a note, we need to modify card.dart
file in the widgets
folder to the following:
Line 45 and Line 60 above adds an id
argument to the the ManageNote
screen.
The compiler will complain about a missing constructor property on the ManageNote
screen, which we will fix in the next step.
Next, modify the manage_note.dart
file inside the screens
folder by passing in the unique id
of each note. We will use the id
specified to get the details of a note.
The snippet above does the following:
- Modifies the constructor to have an
id
property and create an_isError
variable - Creates a
_getANote
method to get a specific note using thegetANote
service, update inputs, and set states accordingly - Uses the
initState
method to check if it is an edit request or view request and call the_getANote
method when the object is inserted into the tree - Conditionally render the form based on current states
Update Note
To update a note, we need to modify the same manage_note.dart
file to the following:
The snippet above does the following:
- Creates an
_updateNote
method that uses theupdateNote
service to update a matching note, navigate appropriately, set states, and uses thesnackbar
to show the action performed - Line 110 - Line 114: Checks if it is an “edit operation” or a “create operation” and use the corresponding function
Delete Note
To delete a note, we need to modify home.dart
file in the screens
folder to the following:
The snippet above does the following:
- Creates a
_deleteNote
method that takes in anid
parameter and uses thedeleteNote
service to delete a note, refreshes the screen usingpushReplacement
method, and uses thesnackbar
to show the action performed - Passes in the
deleteNote
as an argument to theNoteCard
The compiler will complain about a missing constructor property on the NoteCard
widget, which we will fix in the next step.
Next, we need to update the NoteCard
widget by navigating to the widgets
folder, and in this folder, open card.dart
file and modify the snippet to the following:
The snippet above does the following:
- Creates an
onDelete
function property that takes in anid
parameter and adds it as a required parameter to the constructor - Modifies the delete button to call the
onDelete
function and pass in theid
Complete home.dart code:
Complete manage_note.dart code:
Complete card.dart code:
With that done, we restart the application using the code editor or run the command below:
flutter run
We can also validate the entries by navigating to the Database section of Appwrite’s management console.
Conclusion
This post discussed how to create a note-taking app using Flutter and Appwrite. The Appwrite platform ships with services that speed up development processes. Try it out today and focus on what matters while Appwrite takes care of the tricky part.
These resources might be helpful: