Flutter note app

Create a Sleek Note app With Flutter


According to the official Flutter website Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. This basically implies that flutter is an open-source development platform built primarily to enable developers to create android and ios applications, as well as web apps without having to manage different codes for each use case. 

Flutter employs the use of Dart Programming Language and it is pre-loaded with some awesome Material Design packages which makes apps created with Flutter have an incredible feels to it.

In case you haven’t setup a supported IDE and installed flutter on your computer, click here to get it done, using the official documentation. 

In today’s tutorial, we will be creating a cool looking material flutter note app, just like in the image below.

flutter note app


We’re going to start by creating a new project and this tutorial makes use of Android Studio. 


If you have flutter properly installed on your computer, there should be an option to Start a new Flutter project for our flutter note app. When you open Android Studio, just like the photo below.

flutter note app

Next, select the Flutter Application below.

flutter note app

After that configure your application with a name and also set your applications package name. Follow the necessary prompts and click on Finish when done. This will be followed by android studio generating the necessary files needed for our flutter note app. Once this is complete. 

Create a new package under the lib folder and name it Model. Under our model package we are going to create a new file and this will be called Note.dart


We start by creating the object of our note application, considering the fact that Dart employs the OOP design, similar to what we have in java and kotlin.

import 'dart:convert';
import 'package:flutter/material.dart';


class Note {
  int id;
  String title;
  String content;
  DateTime date_created;
  DateTime date_last_edited;
  Color note_color;
  int is_archived = 0;

  Note(this.id, this.title, this.content, this.date_created, this.date_last_edited,this.note_color);


  Map<String, dynamic> toMap(bool forUpdate) {
    var data = {
//      'id': id,  since id is auto incremented in the database we don't need to send it to the insert query.
      'title': utf8.encode(title),
      'content': utf8.encode( content ),
      'date_created': epochFromDate( date_created ),
      'date_last_edited': epochFromDate( date_last_edited ),
      'note_color': note_color.value,
      'is_archived': is_archived  //  for later use for integrating archiving
    };
    if(forUpdate){
      data["id"] = this.id;
    }
    return data;
  }

// Converting the date time object into int representing seconds passed after midnight 1st Jan, 1970 UTC
  int epochFromDate(DateTime dt) {
    return dt.millisecondsSinceEpoch ~/ 1000 ;
  }

  void archiveThisNote() {
    is_archived = 1;
  }

// overriding toString() of the note class to print a better debug description of this custom class
  @override toString() {
    return {
      'id': id,
      'title': title,
      'content': content ,
      'date_created': epochFromDate( date_created ),
      'date_last_edited': epochFromDate( date_last_edited ),
      'note_color': note_color.toString(),
      'is_archived':is_archived
    }.toString();
  }

}

From the foregoing, our note class contains, the variables that will be stored for a single note in our database. Each note has an id, a title and content, and also the date created and last edited is also saved, as well as  a value for the color and an int to show whether the note is archived or not.


We also have a toMap function which takes in a boolean “forupdate” and returns a map. The reason for this is because we do not want to send the id to the database when creating a note, as the id is auto incremented by default. 


The method creates a map of the note attributes using key value pairs and maps the values to their respective pairs. We also check to see if the boolean “forupdate” is true, is this is true then the function assumes we are simply fetching an already saved note from the database and adds the id to the map, but for new notes this is not the case. 
The second method “epochfromDate” is basically for Converting the date time object into int representing seconds passed after midnight 1st Jan, 1970 UTC.
The next method “archiveThisNote” is only called whenever a user archives a note, and the value of “is_archived” is set to 1. By default this value is kept at 0.

The next step is to create a new file for our SQLite Class which will contain a list of the sqlite queries and initialization of our database. Create a new file inside our Model package and name it “SqliteHandler.dart”.


But before we proceed with writing codes for our SqliteHandler class. Please note that there are several packages that are being used in this project and you should add the your “pubspec.yaml”.

Below are a list of the packages and click here to learn how to add packages to your flutter project. This is what the dependency block of our pubspec.yaml file.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  flutter_staggered_grid_view: ^0.2.7
  auto_size_text: ^1.1.2
  sqflite:
  path:
  intl: ^0.15.7
  share: ^0.6.1

In our SqliteHandler class we will start by adding the following imports at the top of our class


import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqlite_api.dart';
import 'dart:async';
import 'Note.dart';

For the sake of brevity here’s the rest of the code to complete this class. Some methods have comments above them to explain briefly what they do.

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqlite_api.dart';
import 'dart:async';
import 'Note.dart';

class NotesDBHandler {

  final databaseName = "notes.db";
  final tableName = "notes";


  final fieldMap = {
    "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
    "title": "BLOB",
    "content": "BLOB",
    "date_created": "INTEGER",
    "date_last_edited": "INTEGER",
    "note_color": "INTEGER",
    "is_archived": "INTEGER"
  };


  static Database _database;


  Future<Database> get database async {
    if (_database != null)
      return _database;

    _database = await initDB();
    return _database;
  }


  initDB() async {
    var path = await getDatabasesPath();
    var dbPath = join(path, 'notes.db');
    // ignore: argument_type_not_assignable
    Database dbConnection = await openDatabase(
        dbPath, version: 1, onCreate: (Database db, int version) async {
      print("executing create query from onCreate callback");
      await db.execute(_buildCreateQuery());
    });

    await dbConnection.execute(_buildCreateQuery());
    _buildCreateQuery();
    return dbConnection;
  }


// build the create query dynamically using the column:field dictionary.
  String _buildCreateQuery() {
    String query = "CREATE TABLE IF NOT EXISTS ";
    query += tableName;
    query += "(";
    fieldMap.forEach((column, field){
      print("$column : $field");
      query += "$column $field,";
    });


    query = query.substring(0, query.length-1);
    query += " )";

    return query;

  }

  static Future<String> dbPath() async {
    String path = await getDatabasesPath();
    return path;
  }

  Future<int> insertNote(Note note, bool isNew) async {
    // Get a reference to the database
    final Database db = await database;
    print("insert called");

    // Insert the Notes into the correct table.
    await db.insert('notes',
      isNew ? note.toMap(false) : note.toMap(true),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );

    if (isNew) {
      // get latest note which isn't archived, limit by 1
      var one = await db.query("notes", orderBy: "date_last_edited desc",
          where: "is_archived = ?",
          whereArgs: [0],
          limit: 1);
      int latestId = one.first["id"] as int;
      return latestId;
    }
    return note.id;
  }


  Future<bool> copyNote(Note note) async {
    final Database db = await database;
    try {
      await db.insert("notes",note.toMap(false), conflictAlgorithm: ConflictAlgorithm.replace);
    } catch(Error) {
      print(Error);
      return false;
    }
    return true;
  }


  Future<bool> archiveNote(Note note) async {
    if (note.id != -1) {
      final Database db = await database;

      int idToUpdate = note.id;

      db.update("notes", note.toMap(true), where: "id = ?",
          whereArgs: [idToUpdate]);
    }
  }

  Future<bool> deleteNote(Note note) async {
    if(note.id != -1) {
      final Database db = await database;
      try {
        await db.delete("notes",where: "id = ?",whereArgs: [note.id]);
        return true;
      } catch (Error){
        print("Error deleting ${note.id}: ${Error.toString()}");
        return false;
      }
    }
  }


  Future<List<Map<String,dynamic>>> selectAllNotes() async {
    final Database db = await database;
    // query all the notes sorted by last edited
    var data = await db.query("notes", orderBy: "date_last_edited desc",
        where: "is_archived = ?",
        whereArgs: [0]);

    return data;

  }



}

fieldmap: This creates an object with all the database columns names which will be used in our sql create statement.

get database: This is background process that ensures that our db class is a singleton.

initDB: Creates the name and file for the database by getting the default path and appending “note.db” to it

insertNote: checks whether the note is a new note or and old note using a ternary conditional statement, based on this evaluation, the toMap function in our “Note.dart” file is called passed with true or false. This method has a return value which is the id of the note. 


The next class inside our Model will be the Utility.dart file. After creating it, add the following code to handle things like extracting our date from a date object and parsing it into a string. 

import 'package:intl/intl.dart';
import 'package:flutter/material.dart';

class CentralStation {
  static bool _updateNeeded ;

  static final fontColor = Color(0xff595959);
  static final  borderColor = Color(0xffd3d3d3) ;

  static init() {
    if (_updateNeeded == null)
      _updateNeeded = true;
  }
  static bool get updateNeeded {
    init();
    if (_updateNeeded) {
      return true;
    } else {
      return false;
    }
  }

  static set updateNeeded(value){
    _updateNeeded = value;
  }

  static String stringForDatetime(DateTime dt){

    var dtInLocal = dt.toLocal();
    //DateTime.fromMillisecondsSinceEpoch( 1490489845  * 1000).toLocal(); //year:  1490489845 //>day: 1556152819  //month:  1553561845  //<day: 1556174419
    var now = DateTime.now().toLocal();
    var dateString = "Edited ";

    var diff = now.difference(dtInLocal);

    if(now.day == dtInLocal.day){                                               // creates format like: 12:35 PM,
      var todayFormat = DateFormat("h:mm a");
      dateString += todayFormat.format(dtInLocal);
    } else if ( (diff.inDays) == 1 || (diff.inSeconds < 86400 && now.day != dtInLocal.day)) {
      var yesterdayFormat = DateFormat("h:mm a");
      dateString +=  "Yesterday, " + yesterdayFormat.format(dtInLocal) ;
    } else if(now.year == dtInLocal.year && diff.inDays > 1){
      var monthFormat = DateFormat("MMM d");
      dateString +=  monthFormat.format(dtInLocal);
    } else {
      var yearFormat = DateFormat("MMM d y");
      dateString += yearFormat.format(dtInLocal);
    }

    return dateString;
  }

}

Create two more packages inside your lib folder and name them View and ViewControllers

Inside the View package create a new file and name it ColorSlider.dart

import 'package:flutter/material.dart';

class ColorSlider extends StatefulWidget {
  final void Function(Color)  callBackColorTapped ;
  final Color noteColor ;
  ColorSlider({@required this.callBackColorTapped, @required this.noteColor});
  @override
  _ColorSliderState createState() => _ColorSliderState();
}

class _ColorSliderState extends State<ColorSlider> {

  final colors = [
    Color(0xffffffff), // classic white
    Color(0xfff28b81), // light pink
    Color(0xfff7bd02), // yellow
    Color(0xfffbf476), // light yellow
    Color(0xffcdff90), // light green
    Color(0xffa7feeb), // turquoise
    Color(0xffcbf0f8), // light cyan
    Color(0xffafcbfa), // light blue
    Color(0xffd7aefc), // plum
    Color(0xfffbcfe9), // misty rose
    Color(0xffe6c9a9), // light brown
    Color(0xffe9eaee)  // light gray
  ];

  final Color borderColor = Color(0xffd3d3d3);
  final Color foregroundColor = Color(0xff595959);

  final _check = Icon(Icons.check);

  Color noteColor;
  int indexOfCurrentColor;
  @override void initState() {

    super.initState();
    this.noteColor = widget.noteColor;
    indexOfCurrentColor = colors.indexOf(noteColor);
  }



  @override
  Widget build(BuildContext context) {

    return ListView(
      scrollDirection: Axis.horizontal,
      children:
      List.generate(colors.length, (index)
      {
        return
          GestureDetector(
              onTap: ()=> _colorChangeTapped(index),
              child: Padding(
                  padding: EdgeInsets.only(left: 6, right: 6),
                  child:Container(
                      child: new CircleAvatar(
                        child: _checkOrNot(index),
                        foregroundColor: foregroundColor,
                        backgroundColor: colors[index],
                      ),
                      width: 38.0,
                      height: 38.0,
                      padding: const EdgeInsets.all(1.0), // border width
                      decoration: new BoxDecoration(
                        color: borderColor, // border color
                        shape: BoxShape.circle,
                      )
                  ) )
          );

      })
      ,);
  }


  void _colorChangeTapped(int indexOfColor) {
    setState(() {
      noteColor = colors[indexOfColor];
      indexOfCurrentColor = indexOfColor;
      widget.callBackColorTapped(colors[indexOfColor]);
    });

  }

  Widget _checkOrNot(int index){
    if (indexOfCurrentColor == index) {
      return _check;
    }
    return null;
  }

}

In the build method which returns a widget, we inflate a Listview widget with a set of colors, which are defined in the colors array. The listview widget contains a scrollDirection widget which simply helps us add another property to our container widget telling it that we want this listview to scroll horizontally. 

Also the Gesture Detector widget has an ontap() property which calls the function _colorChangeTapped(index) whenever a color is being tapped on and this function gets the index of the color and assigns that value to the notecolor variable.

We are going to go ahead and create a bottom sheet which will have this color slider. In another tutorial, we are going to explore how to fully customize and implement a bottom sheet in flutter. For now simply create new dart file under the Views directory and name it MoreOptionsSheet.dart.

import 'package:flutter/material.dart';
import 'ColorSlider.dart';
import '../Model/Utility.dart';


enum moreOptions { delete, share, copy }

class MoreOptionsSheet extends StatefulWidget {
  final Color color;
  final DateTime date_last_edited;
  final void Function(Color) callBackColorTapped;

  final void Function(moreOptions) callBackOptionTapped;

  const MoreOptionsSheet(
      {Key key,
        this.color,
        this.date_last_edited,
        this.callBackColorTapped,
        this.callBackOptionTapped})
      : super(key: key);

  @override
  _MoreOptionsSheetState createState() => _MoreOptionsSheetState();
}

class _MoreOptionsSheetState extends State<MoreOptionsSheet> {
  var note_color;

  @override
  void initState() {
    note_color = widget.color;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: this.note_color,
      child: new Wrap(
        children: <Widget>[
          new ListTile(
              leading: new Icon(Icons.delete),
              title: new Text('Delete permanently'),
              onTap: () {
                Navigator.of(context).pop();
                widget.callBackOptionTapped(moreOptions.delete);
              }),
          new ListTile(
              leading: new Icon(Icons.content_copy),
              title: new Text('Duplicate'),
              onTap: () {
                Navigator.of(context).pop();
                widget.callBackOptionTapped(moreOptions.copy);
              }),
          new ListTile(
              leading: new Icon(Icons.share),
              title: new Text('Share'),
              onTap: () {
                Navigator.of(context).pop();
                widget.callBackOptionTapped(moreOptions.share);
              }),
          new Padding(
            padding: EdgeInsets.only(left: 10, right: 10),
            child: SizedBox(
              height: 44,
              width: MediaQuery.of(context).size.width,
              child: ColorSlider(
                callBackColorTapped: _changeColor,
                // call callBack from notePage here
                noteColor: note_color, // take color from local variable
              ),
            ),
          ),
          new Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              SizedBox(
                height: 44,
                child: Center(
                    child: Text(CentralStation.stringForDatetime(
                        widget.date_last_edited))),
              )
            ],
            mainAxisAlignment: MainAxisAlignment.center,
          ),
          new ListTile()
        ],
      ),
    );
  }

  void _changeColor(Color color) {
    setState(() {
      this.note_color = color;
      widget.callBackColorTapped(color);
    });
  }
}

This class simply has four main fields which includes two variable, one for the color selected by the user and a date field, it also has two functions which are simple expressions, one is responsible for creating the share, copy and delete options by using an enum interface.

The other one is a function which calls the _changeColor method that takes a color parameter and sets it as the note color.
The moreoptions sheet is a statefull widget and hence extends the statefull widget class, the statefull widget class has three important methods that always has to be overidden, The createState, initState and the build function.


For the scope of this article we will focus on the build function, which returns a widget. We build our display by using a Container Widget that takes in an array of tiles as an array in its child property.

Our array of widgets is made up of four tiles, which are the copy, share, delete and the color slider pallete. Each of these share similar characteristics which includes an icon, a text and a callback that performs the action when it is being clicked on.

Inside our ViewControllers package we are going to create a new file called NotePage.dart and add the following codes. Check the comments for a brief overview of each functions.

import 'package:flutter/material.dart';
import '../Model/Note.dart';
import '../Model/SqliteHandler.dart';
import 'dart:async';
import '../Model/Utility.dart';
import '../Views/MoreOptionsSheet.dart';
import 'package:share/share.dart';
import 'package:flutter/services.dart';

class NotePage extends StatefulWidget {
  final Note noteInEditing;

  //constructor that takes a Note object
  NotePage(this.noteInEditing);


  @override
  _NotePageState createState() => _NotePageState();
}

class _NotePageState extends State<NotePage> {

  final _titleController = TextEditingController();
  final _contentController = TextEditingController();
  var note_color;
  bool _isNewNote = false;
  final _titleFocus = FocusNode();
  final _contentFocus = FocusNode();

  String _titleFrominitial ;
  String _contentFromInitial;
  DateTime _lastEditedForUndo;



  var _editableNote;

  // the timer variable responsible to call persistData function every 5 seconds and cancel the timer when the page pops.
  Timer _persistenceTimer;

  final GlobalKey<ScaffoldState> _globalKey = new GlobalKey<ScaffoldState>();

  @override
  void initState() {
    _editableNote = widget.noteInEditing;
    _titleController.text = _editableNote.title;
    _contentController.text = _editableNote.content;
    note_color = _editableNote.note_color;
    _lastEditedForUndo = widget.noteInEditing.date_last_edited;

    _titleFrominitial = widget.noteInEditing.title;
    _contentFromInitial = widget.noteInEditing.content;


    if (widget.noteInEditing.id == -1) {
      _isNewNote = true;
    }
    _persistenceTimer = new Timer.periodic(Duration(seconds: 5), (timer) {
      // call insert query here
      print("5 seconds passed");
      print("editable note id: ${_editableNote.id}");
      _persistData();
    });
  }

  @override
  Widget build(BuildContext context) {

    if(_editableNote.id == -1 && _editableNote.title.isEmpty) {
      FocusScope.of(context).requestFocus(_titleFocus);
    }

    return WillPopScope(
      child: Scaffold(
        key: _globalKey,
        appBar: AppBar(brightness: Brightness.light,
          leading: BackButton(
            color: Colors.black,
          ),
          actions: _archiveAction(context),
          elevation: 1,
          backgroundColor: note_color,
          title: _pageTitle(),
        ),
        body: _body(context),
      ),
      onWillPop: _readyToPop,
    );
  }

  Widget _body(BuildContext ctx) {
    return

      Container(
          color: note_color,
          padding: EdgeInsets.only(left: 16, right: 16, top: 12),
          child:

          SafeArea(child:
          Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              Flexible(
                child: Container(
                  padding: EdgeInsets.all(5),
//          decoration: BoxDecoration(border: Border.all(color: CentralStation.borderColor,width: 1 ),borderRadius: BorderRadius.all(Radius.circular(10)) ),
                  child: EditableText(
                      onChanged: (str) => {updateNoteObject()},
                      maxLines: null,
                      controller: _titleController,
                      focusNode: _titleFocus,
                      style: TextStyle(
                          color: Colors.black,
                          fontSize: 22,
                          fontWeight: FontWeight.bold),
                      cursorColor: Colors.blue,
                      backgroundCursorColor: Colors.blue),
                ),
              ),

              Divider(color: CentralStation.borderColor,),

              Flexible( child: Container(
                  padding: EdgeInsets.all(5),
//    decoration: BoxDecoration(border: Border.all(color: CentralStation.borderColor,width: 1),borderRadius: BorderRadius.all(Radius.circular(10)) ),
                  child: EditableText(
                    onChanged: (str) => {updateNoteObject()},
                    maxLines: 300, // line limit extendable later
                    controller: _contentController,
                    focusNode: _contentFocus,
                    style: TextStyle(color: Colors.black, fontSize: 20),
                    backgroundCursorColor: Colors.red,
                    cursorColor: Colors.blue,
                  )
              )
              )

            ],
          ),
            left: true,right: true,top: false,bottom: false,
          )
      )



    ;
  }

  //returns a new text with "New Note" or "Edit Note" based on the value of _editableNote.id
  Widget _pageTitle() {
    return Text(_editableNote.id == -1 ? "New Note" : "Edit Note");
  }



  List<Widget> _archiveAction(BuildContext context) {
    List<Widget> actions = [];
    if (widget.noteInEditing.id != -1) {
      actions.add(Padding(
        padding: EdgeInsets.symmetric(horizontal: 12),
        child: InkWell(
          child: GestureDetector(
            onTap: () => _undo(),
            child: Icon(
              Icons.undo,
              color: CentralStation.fontColor,
            ),
          ),
        ),
      ));
    }
    actions += [
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 12),
        child: InkWell(
          child: GestureDetector(
            onTap: () => _archivePopup(context),
            child: Icon(
              Icons.archive,
              color: CentralStation.fontColor,
            ),
          ),
        ),
      ),
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 12),
        child: InkWell(
          child: GestureDetector(
            onTap: () => bottomSheet(context),
            child: Icon(
              Icons.more_vert,
              color: CentralStation.fontColor,
            ),
          ),
        ),
      ),
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 12),
        child: InkWell(
          child: GestureDetector(
            onTap: () => { _saveAndStartNewNote(context)  },
            child: Icon(
              Icons.add,
              color: CentralStation.fontColor,
            ),
          ),
        ),
      )
    ];
    return actions;
  }

  //responsible for opening the moreOptionsSheet Class and its widgets
  void bottomSheet(BuildContext context) {
    showModalBottomSheet(
        context: context,
        builder: (BuildContext ctx) {
          return MoreOptionsSheet(
            color: note_color,
            callBackColorTapped: _changeColor,
            callBackOptionTapped: bottomSheetOptionTappedHandler,
            date_last_edited: _editableNote.date_last_edited,
          );
        });
  }

  //saves data as the user makes changes and saves and updates this value whenever it changes
  void _persistData() {
    updateNoteObject();

    if (_editableNote.content.isNotEmpty) {
      var noteDB = NotesDBHandler();

      if (_editableNote.id == -1) {
        Future<int> autoIncrementedId =
        noteDB.insertNote(_editableNote, true); // for new note
        // set the id of the note from the database after inserting the new note so for next persisting
        autoIncrementedId.then((value) {
          _editableNote.id = value;
        });
      } else {
        noteDB.insertNote(
            _editableNote, false); // for updating the existing note
      }
    }
  }

// this function will ne used to save the updated editing value of the note to the local variables as user types
  void updateNoteObject() {
    _editableNote.content = _contentController.text;
    _editableNote.title = _titleController.text;
    _editableNote.note_color = note_color;
    print("new content: ${_editableNote.content}");
    print(widget.noteInEditing);
    print(_editableNote);

    print("same title? ${_editableNote.title == _titleFrominitial}");
    print("same content? ${_editableNote.content == _contentFromInitial}");


    if (!(_editableNote.title == _titleFrominitial &&
        _editableNote.content == _contentFromInitial) ||
        (_isNewNote)) {
      // No changes to the note
      // Change last edit time only if the content of the note is mutated in compare to the note which the page was called with.
      _editableNote.date_last_edited = DateTime.now();
      print("Updating date_last_edited");
      CentralStation.updateNeeded = true;
    }
  }

  //Handles callbacks on the MoreOptionsSheet
  void bottomSheetOptionTappedHandler(moreOptions tappedOption) {
    print("option tapped: $tappedOption");
    switch (tappedOption) {
      case moreOptions.delete:
        {
          if (_editableNote.id != -1) {
            _deleteNote(_globalKey.currentContext);
          } else {
            _exitWithoutSaving(context);
          }
          break;
        }
      case moreOptions.share:
        {
          if (_editableNote.content.isNotEmpty) {
            Share.share("${_editableNote.title}\n${_editableNote.content}");
          }
          break;
        }
      case moreOptions.copy : {
        _copy();
        break;
      }
    }
  }

  //deletes a saved note from the database when the user selects delete from the bottom sheet
  void _deleteNote(BuildContext context) {
    if (_editableNote.id != -1) {
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text("Confirm ?"),
              content: Text("This note will be deleted permanently"),
              actions: <Widget>[
                FlatButton(
                    onPressed: ()  {
                      _persistenceTimer.cancel();
                      var noteDB = NotesDBHandler();
                      Navigator.of(context).pop();
                      noteDB.deleteNote(_editableNote);
                      CentralStation.updateNeeded = true;

                      Navigator.of(context).pop();

                    },
                    child: Text("Yes")),
                FlatButton(
                    onPressed: () => {Navigator.of(context).pop()},
                    child: Text("No"))
              ],
            );
          });
    }
  }

  //responsible for responding whenever the user selects on a color by changing the color and saving the color
  //value to the database
  void _changeColor(Color newColorSelected) {
    print("note color changed");
    setState(() {
      note_color = newColorSelected;
      _editableNote.note_color = newColorSelected;
    });
    if (_editableNote.id != -1) {
      var noteDB = NotesDBHandler();
      _editableNote.note_color = note_color;
      noteDB.insertNote(_editableNote, false);
    }
    CentralStation.updateNeeded = true;
  }



  //this function is called whenever the user clicks on the plus icon to add a new note from
  //an already existing note.
  void _saveAndStartNewNote(BuildContext context){
    _persistenceTimer.cancel();
    var emptyNote = new Note(-1, "", "", DateTime.now(), DateTime.now(), Colors.white);
    Navigator.of(context).pop();
    Navigator.push(context, MaterialPageRoute(builder: (ctx) => NotePage(emptyNote)));

  }

  Future<bool> _readyToPop() async {
    _persistenceTimer.cancel();
    //show saved toast after calling _persistData function.

    _persistData();
    return true;
  }

  //build a pop up for whenever a user clicks on the archive icon,
  // this prompt asks the user if he is sure before proceeding to archive the note

  void _archivePopup(BuildContext context) {
    if (_editableNote.id != -1) {
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text("Confirm ?"),
              content: Text("This note will be archived"),
              actions: <Widget>[
                FlatButton(
                    onPressed: () => _archiveThisNote(context),
                    child: Text("Yes")),
                FlatButton(
                    onPressed: () => {Navigator.of(context).pop()},
                    child: Text("No"))
              ],
            );
          });
    } else {
      _exitWithoutSaving(context);
    }
  }

  //this function is called whenever a user clicks on a new note but no value is entered
  void _exitWithoutSaving(BuildContext context) {
    _persistenceTimer.cancel();
    CentralStation.updateNeeded = false;
    Navigator.of(context).pop();
  }

  //responsible for archiving the note
  void _archiveThisNote(BuildContext context) {
    Navigator.of(context).pop();
    // set archived flag to true and send the entire note object in the database to be updated
    _editableNote.is_archived = 1;
    var noteDB = NotesDBHandler();
    noteDB.archiveNote(_editableNote);
    // update will be required to remove the archived note from the staggered view
    CentralStation.updateNeeded = true;
    _persistenceTimer.cancel(); // shutdown the timer

    Navigator.of(context).pop(); // pop back to staggered view
    // TODO: OPTIONAL show the toast of deletion completion
    Scaffold.of(context).showSnackBar(new SnackBar(content: Text("deleted")));
  }


  //this function duplicates a note with the selected id whenever
  //copy is tapped on from the bottom sheet
  void _copy(){
    var noteDB = NotesDBHandler();
    Note copy = Note(-1,
        _editableNote.title,
        _editableNote.content,
        DateTime.now(),
        DateTime.now(),
        _editableNote.note_color) ;


    var status = noteDB.copyNote(copy);
    status.then((query_success){
      if (query_success){
        CentralStation.updateNeeded = true;
        Navigator.of(_globalKey.currentContext).pop();
      }
    });
  }


//undo changes made to the text using FLutter's TextController method
  void _undo() {
    _titleController.text = _titleFrominitial;// widget.noteInEditing.title;
    _contentController.text = _contentFromInitial;// widget.noteInEditing.content;
    _editableNote.date_last_edited = _lastEditedForUndo;// widget.noteInEditing.date_last_edited;
  }
}

Inside the View package create another file and name it StaggeredTiles.dart and add the code below

import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import '../ViewControllers/NotePage.dart';
import '../Model/Note.dart';
import '../Model/Utility.dart';

class MyStaggeredTile extends StatefulWidget {
  final Note note;
  MyStaggeredTile(this.note);
  @override
  _MyStaggeredTileState createState() => _MyStaggeredTileState();
}



class _MyStaggeredTileState extends State<MyStaggeredTile> {

  String _content ;
  double _fontSize ;
  Color tileColor ;
  String title;

  @override
  Widget build(BuildContext context) {

    _content = widget.note.content;
    _fontSize = _determineFontSizeForContent();
    tileColor = widget.note.note_color;
    title = widget.note.title;

    return GestureDetector(
      onTap: ()=> _noteTapped(context),
      child: Container(
        decoration: BoxDecoration(
            border: tileColor == Colors.white ?   Border.all(color: CentralStation.borderColor) : null,
            color: tileColor,
            borderRadius: BorderRadius.all(Radius.circular(8))),
        padding: EdgeInsets.all(8),
        child:  constructChild(),) ,
    );
  }

  void _noteTapped(BuildContext ctx) {
    CentralStation.updateNeeded = false;
    Navigator.push(ctx, MaterialPageRoute(builder: (ctx) => NotePage(widget.note)));
  }

  Widget constructChild() {

    List<Widget> contentsOfTiles = [];

    if(widget.note.title.length != 0) {
      contentsOfTiles.add(
        AutoSizeText(
          title,
          style: TextStyle(fontSize: _fontSize,fontWeight: FontWeight.bold),
          maxLines: widget.note.title.length == 0 ? 1 : 3,
          textScaleFactor: 1.5,
        ),
      );
      contentsOfTiles.add(Divider(color: Colors.transparent,height: 6,),);
    }

    contentsOfTiles.add(
        AutoSizeText(
          _content,
          style: TextStyle(fontSize: _fontSize),
          maxLines: 10,
          textScaleFactor: 1.5,
        )
    );

    return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children:     contentsOfTiles
    );
  }


  double _determineFontSizeForContent() {
    int charCount = _content.length + widget.note.title.length ;
    double fontSize = 20 ;
    if (charCount > 110 ) { fontSize = 12; }
    else if (charCount > 80) {  fontSize = 14;  }
    else if (charCount > 50) {  fontSize = 16;  }
    else if (charCount > 20) {  fontSize = 18;  }


    return fontSize;
  }

}

This class basically has four fields 

  String _content ;
  double _fontSize ;
  Color tileColor ;
  String title;

These properties are used to customize the look and feel of each note on the main app screen.


void _noteTapped(BuildContext ctx) {
    CentralStation.updateNeeded = false;
    Navigator.push(ctx, MaterialPageRoute(builder: (ctx) => NotePage(widget.note)));
  }

The function above sets the action on each note been tapped on the screen.


There are two more files that needs to be created inside the ViewControllers package. The first is the StaggeredView.dart file. 

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../Model/Note.dart';
import '../Model/SqliteHandler.dart';
import '../Model/Utility.dart';
import '../Views/StaggeredTiles.dart';
import 'HomePage.dart';

class StaggeredGridPage extends StatefulWidget {
  final notesViewType;
  const StaggeredGridPage({Key key, this.notesViewType}) : super(key: key);
  @override
  _StaggeredGridPageState createState() => _StaggeredGridPageState();
}

class _StaggeredGridPageState extends State<StaggeredGridPage> {


  //instance of our Sqlite class
  var  noteDB = NotesDBHandler();

  //a map which will be used in inflating our staggered grid view
  List<Map<String, dynamic>> _allNotesInQueryResult = [];
  viewType notesViewType ;


  @override
  void initState() {
    super.initState();
    this.notesViewType = widget.notesViewType;
  }


  @override void setState(fn) {
    super.setState(fn);
    this.notesViewType = widget.notesViewType;
  }

  @override
  Widget build(BuildContext context) {


    GlobalKey _stagKey = GlobalKey();

    print("update needed?: ${CentralStation.updateNeeded}");
    if(CentralStation.updateNeeded) {  retrieveAllNotesFromDatabase();  }
    return Container(child: Padding(padding:  _paddingForView(context) , child:
    new StaggeredGridView.count(key: _stagKey,
      crossAxisSpacing: 6,
      mainAxisSpacing: 6,
      crossAxisCount: _colForStaggeredView(context),
      children: List.generate(_allNotesInQueryResult.length, (i){ return _tileGenerator(i); }),
      staggeredTiles: _tilesForView() ,
    ),
    )
    );
  }

  int _colForStaggeredView(BuildContext context) {

    if (widget.notesViewType == viewType.List)
      return 1;
    // for width larger than 600 on grid mode, return 3 irrelevant of the orientation to accommodate more notes horizontally
    return MediaQuery.of(context).size.width > 600 ? 3:2  ;
  }

  List<StaggeredTile> _tilesForView() { // Generate staggered tiles for the view based on the current preference.
    return List.generate(_allNotesInQueryResult.length,(index){ return StaggeredTile.fit(1); }
    ) ;
  }


  EdgeInsets _paddingForView(BuildContext context){
    double width = MediaQuery.of(context).size.width;
    double padding ;
    double top_bottom = 8;
    if (width > 500) {
      padding = ( width ) * 0.05 ; // 5% padding of width on both side
    } else {
      padding = 8;
    }
    return EdgeInsets.only(left: padding, right: padding, top: top_bottom, bottom: top_bottom);
  }

//gets the values of the notes for each of the fields in the grid
  MyStaggeredTile _tileGenerator(int i){
    return MyStaggeredTile(  Note(
        _allNotesInQueryResult[i]["id"],
        _allNotesInQueryResult[i]["title"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]["title"]),
        _allNotesInQueryResult[i]["content"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]["content"]),
        DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_created"] * 1000),
        DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_last_edited"] * 1000),
        Color(_allNotesInQueryResult[i]["note_color"] ))
    );
  }


  //carries out the queries to get the notes from the database
  void retrieveAllNotesFromDatabase() {
    // queries for all the notes from the database ordered by latest edited note. excludes archived notes.
    var _testData = noteDB.selectAllNotes();
    _testData.then((value){
      setState(() {
        this._allNotesInQueryResult = value;
        CentralStation.updateNeeded = false;
      });
    });
  }


}

Check the comments for a brief explanations on what each of the fields are responsible for. 
The last file to be created inside the ViewControllers package is the HomePage.dart file. 

import 'package:flutter/material.dart';
import 'StagerredView.dart';
import '../Model/Note.dart';
import 'NotePage.dart';
import '../Model/Utility.dart';

enum viewType {
  List,
  Staggered
}


class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  var notesViewType ;
  @override void initState() {
    notesViewType = viewType.Staggered;
  }

  @override
  Widget build(BuildContext context) {

    return
      Scaffold(
        resizeToAvoidBottomPadding: false,
        appBar: AppBar(brightness: Brightness.light,
          actions: _appBarActions(),
          elevation: 1,
          backgroundColor: Colors.white,
          centerTitle: true,
          title: Text("Notes"),
        ),
        body: SafeArea(child:   _body(), right: true, left:  true, top: true, bottom: true,),
        bottomSheet: _bottomBar(),
      );

  }

  Widget _body() {
    print(notesViewType);
    return Container(child: StaggeredGridPage(notesViewType: notesViewType,));
  }

  //Contains a FlatButton widget that is responsible for calling the _newNoteTapped function to take us to
  //a new page to create a new note
  Widget _bottomBar() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        FlatButton(
          child: Text(
            "New Note\n",
            style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold),
          ),
          onPressed: () => _newNoteTapped(context),
        )
      ],
    );
  }

/* responsible for creating a new route using the Navigator.push class*/
  void _newNoteTapped(BuildContext ctx) {
    // "-1" id indicates the note is not new
    var emptyNote = new Note(-1, "", "", DateTime.now(), DateTime.now(), Colors.white);
    Navigator.push(ctx,MaterialPageRoute(builder: (ctx) => NotePage(emptyNote)));
  }

  //sets the viewType to either grid or list based on the noteViewType value
  void _toggleViewType(){
    setState(() {
      CentralStation.updateNeeded = true;
      if(notesViewType == viewType.List)
      {
        notesViewType = viewType.Staggered;

      } else {
        notesViewType = viewType.List;
      }

    });
  }

  List<Widget> _appBarActions() {

    return [
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 12),
        child: InkWell(
          child: GestureDetector(
            onTap: () => _toggleViewType() ,
            child: Icon(
              notesViewType == viewType.List ?  Icons.developer_board : Icons.view_headline,
              color: CentralStation.fontColor,
            ),
          ),
        ),
      ),
    ];
  }


}

This class basically creates a widget and carries out database query to fetch all notes from the database and generates a staggered grid list of tiles with the information from the database. 


At this point you might think you have finished creating everything but if you try to run your application, you will notice that nothing happens, this is because we are yet to apply all of our codes to our app’s entry point and this should be the last thing to do before running your app.

Similar to our main method in java and onCreate in Android Studio, flutter has a main method which is the applications entry point. This can be found in our main.dart file. 

This is what our main.dart file should look like to be able to use all the codes we have written out. 

import 'package:flutter/material.dart';
import 'ViewControllers/HomePage.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        fontFamily: "Roboto",
        iconTheme: IconThemeData(color: Colors.black),
        primaryTextTheme: TextTheme(
          title: TextStyle(color: Colors.black),
        ),
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),

    );
  }
}

First, notice the main method which uses the runApp() to call MyApp() method.The build method uses a MaterialApp which is helpful because it gives us access to a whole lot of easy customizations on our applications, like the appbar, the title and body of our application can easily be tweaked using the MaterialApp widget. 

 debugShowCheckedModeBanner: false

This line of code is responsible for removing the debug label on the top right conner of our applications screen. As seen in the image below


fontFamily: "Roboto",
        iconTheme: IconThemeData(color: Colors.black),
        primaryTextTheme: TextTheme(
          title: TextStyle(color: Colors.black),
        ),
        primarySwatch: Colors.blue,
      ),

The following codes are mainly responsible for the appearance of our application, the fontFamily property specifies the type of font used in our application. iconTheme: specifies a theme for our icons and primarySwatch is similar to the primary Color. 

home: HomePage()

This line tells android studio that the main body of the page should utilize the HomePage class we created in the ViewControllers package.


Now Run your flutter note app and launch the note application to start showing off to your friends. 


Conclusion

Building applications with flutter is a lot easier and flutter comes with lots of strings attached to make you stay on it. The UI looks a lot more better since flutter comes pre-loaded with a modern react-style framework, a 2D rendering engine, and lots of other packages which basically reduces the time it takes to build an application from scratch.

You can find the complete source code for our flutter note app here


Share on social media

//