Build a Notepad App in Flutter

Demola Malomo - May 16 '22 - - Dev Community

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:

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


Enter fullscreen mode Exit fullscreen mode

The complete source code is also available on the dev branch of the same repository.

Folder Structure

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


Enter fullscreen mode Exit fullscreen mode

Then run the project using the command below:



    flutter run


Enter fullscreen mode Exit fullscreen mode

The command above will run the application on the selected device.

demo
demo
demo

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

Appwrite running

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.

create project
enter project name

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.

navigate to Database Tab
click on create project
enter project name & 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.

modify permission

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.

select attribute
create field

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.

add document
input fields and 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.

Add Platform
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

ios

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.

change deployment 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

android

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>


Enter fullscreen mode Exit fullscreen mode

We also need to modify the highlighted code with our Appwrite’s Project ID. Navigate to the Settings menu to get the Project ID.

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


Enter fullscreen mode Exit fullscreen mode

add dependency

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";
    }


Enter fullscreen mode Exit fullscreen mode

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";
    }


Enter fullscreen mode Exit fullscreen mode

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";
    }


Enter fullscreen mode Exit fullscreen mode

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};
      }
    }


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Creates a Note class with required properties
  • Adds a constructor with unrequired and required parameters
  • Create a fromJson and toJson 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 with client and db properties to connect to the Appwrite instance and the database, respectively
  • Creates an _init method that configures the client and the db instances using the AppConstants defined earlier and initializes the method in the class constructor
  • Creates a getAllNotes method that uses the configured database’s listDocuments function to get a list of notes and converts the returned JSON to a list using the Note.fromJson method
  • Creates a createNote method that takes in the title and note parameter and uses the createDocument function to create a note which creates an instance of the note using the parameters, passes in the unique() flag as the documentId; which tells Appwrite to auto-generate a unique ID, and passes in the new note by converting the Dart object to JSON using the newNote.toJson() method
  • Creates a getANote method that takes in an id as a parameter and uses the getDocument function to get the matching note and converts the returned JSON to a Dart object using the Note.fromJson method
  • Creates a updateNote method that takes in the title, note, and id parameter and uses the updateDocument function to update a note; this creates an instance of the note using the title and note parameter, passes in the id as the documentId, and passes in the updated note by converting the Dart object to JSON using the newNote.toJson() method
  • Creates a deleteNote method that takes in an id as a parameter and uses the deleteDocument 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 the getAllNotes 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 the NoteCard

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 and note 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 the createNote service to create a note, navigate appropriately, set states, and uses the snackbar 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 the getANote 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 the updateNote service to update a matching note, navigate appropriately, set states, and uses the snackbar 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 an id parameter and uses the deleteNote service to delete a note, refreshes the screen using pushReplacement method, and uses the snackbar to show the action performed
  • Passes in the deleteNote as an argument to the NoteCard

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 an id parameter and adds it as a required parameter to the constructor
  • Modifies the delete button to call the onDelete function and pass in the id

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


Enter fullscreen mode Exit fullscreen mode

running app

We can also validate the entries by navigating to the Database section of Appwrite’s management console.

data on Appwrite

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:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .