Article

Creating a note-taking app in Flutter/Dart

Last updated 
May 3, 2019
 min read

Flutter is an open source cross-platform mobile development framework developed by Google. Apps for which are written in Dart. Flutter comes pre-equipped with Material Design components which makes it easy to create apps with good looks and feels to it. In Flutter everything is a Widget of either stateless or stateful. A note-taking app is something to start with, with a usable design and functionalities.

If you haven’t installed flutter and a supported IDE, you can find the instructions here.

Setting up the Project

First, let’s set up the project:

  1. Create a flutter project from Android Studio or enter the command in terminal/cmd “flutter create notes”.
  2. In main.dart remove the homePage class and create a new file with our own HomePage class extending Stateful Widget. This class will contain our Scaffold.
  3. Create another Stateful Widget class. This will Build body containing Staggered View for HomePage. We’ll call it “StaggeredGridPage”.

Let’s be creative and try to present the notes in a cool staggered view.

We’ll use this dart package to create a staggered grid view and SQLite to store the notes data on the device.

Following is a code snippet from pubspec.yaml with the required dependencies listed. Add them, save the file and use flutter command “flutter packages get” to resolve the newly added dependencies.

1dependencies:
2  flutter:
3    sdk: flutter
4
5  cupertino_icons: ^0.1.2
6  flutter_staggered_grid_view: ^0.2.7
7  auto_size_text: ^1.1.2
8  sqflite:
9  path:
10  intl: ^0.15.7
11  share: ^0.6.1

Creating the Note Class

Create a class for the notes. We’ll need the toMap function for database queries.

1class Note {
2  int id;
3  String title;
4  String content;
5  DateTime date_created;
6  DateTime date_last_edited;
7  Color note_color;
8  int is_archived = 0;
9
10  Note(this.id, this.title, this.content, this.date_created, this.date_last_edited,this.note_color);
11
12  Map<String, dynamic> toMap(bool forUpdate) {
13    var data = {
14//      'id': id,  since id is auto incremented in the database we don't need to send it to the insert query.
15      'title': utf8.encode(title),
16      'content': utf8.encode( content ),
17      'date_created': epochFromDate( date_created ),
18      'date_last_edited': epochFromDate( date_last_edited ),
19      'note_color': note_color.value,
20      'is_archived': is_archived  //  for later use for integrating archiving
21    };
22    if(forUpdate){  data["id"] = this.id;  }
23    return data;
24  }
25
26// Converting the date time object into int representing seconds passed after midnight 1st Jan, 1970 UTC
27int epochFromDate(DateTime dt) {  return dt.millisecondsSinceEpoch ~/ 1000; }
28
29void archiveThisNote(){ is_archived = 1; }
30}

Grab the code of SQLite database queries for note class and table from here.

Now your Material App’s home should have a Scaffold from HomePage.dart which should have StaggeredGridView as the body. In the AppBar of the scaffold place an action button to enable the user to toggle between list view and staggered view. Don’t forget to wrap the body in SafeArea, we want the app to be notch friendly on latest phones.

The Staggered View library requires a cross-axis count for the view which we’ll provide dynamically based on the width of the display size. This is required to tell the view number of notes we want to show side by side. In landscape mode on a phone or a tablet screen, we’ll make it arrange 3 notes horizontally and 2 for a phone in portrait mode.

1import 'dart:convert';
2import 'package:flutter/material.dart';
3import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
4import '../Models/Note.dart';
5import '../Models/SqliteHandler.dart';
6import '../Models/Utility.dart';
7import '../Views/StaggeredTiles.dart';
8import 'HomePage.dart';
9
10class StaggeredGridPage extends StatefulWidget {
11  final notesViewType;
12  const StaggeredGridPage({Key key, this.notesViewType}) : super(key: key);
13  @override
14  _StaggeredGridPageState createState() => _StaggeredGridPageState();
15}
16
17class _StaggeredGridPageState extends State<StaggeredGridPage> {
18
19  var  noteDB = NotesDBHandler();
20  List<Map<String, dynamic>> _allNotesInQueryResult = [];
21  viewType notesViewType ;
22
23@override
24  void initState() {
25    super.initState();
26    this.notesViewType = widget.notesViewType;
27  }
28
29@override void setState(fn) {
30    super.setState(fn);
31    this.notesViewType = widget.notesViewType;
32  }
33
34  @override
35  Widget build(BuildContext context) {
36    GlobalKey _stagKey = GlobalKey();
37    if(CentralStation.updateNeeded) {  retrieveAllNotesFromDatabase();  }
38    return Container(child: Padding(padding:  _paddingForView(context) , child:
39      new StaggeredGridView.count(key: _stagKey,
40        crossAxisSpacing: 6, mainAxisSpacing: 6,
41        crossAxisCount: _colForStaggeredView(context),
42        children: List.generate(_allNotesInQueryResult.length, (i){ return _tileGenerator(i); }),
43      staggeredTiles: _tilesForView() ,
44          ),
45        )
46      );
47  }
48
49  int _colForStaggeredView(BuildContext context) {
50      if (widget.notesViewType == viewType.List) { return 1; }
51      // for width larger than 600, return 3 irrelevant of the orientation to accommodate more notes horizontally
52      return MediaQuery.of(context).size.width > 600 ? 3 : 2 ;
53  }
54
55 List<StaggeredTile> _tilesForView() { // Generate staggered tiles for the view based on the current preference.
56  return List.generate(_allNotesInQueryResult.length,(index){ return StaggeredTile.fit( 1 ); }
57  ) ;
58}
59
60EdgeInsets _paddingForView(BuildContext context){
61  double width = MediaQuery.of(context).size.width;
62  double padding ;
63  double top_bottom = 8;
64  if (width > 500) {
65    padding = ( width ) * 0.05 ; // 5% padding of width on both side
66  } else {
67    padding = 8;
68  }
69  return EdgeInsets.only(left: padding, right: padding, top: top_bottom, bottom: top_bottom);
70}
71
72
73 MyStaggeredTile _tileGenerator(int i){
74 return MyStaggeredTile(  Note(
75      _allNotesInQueryResult[i]["id"],
76      _allNotesInQueryResult[i]["title"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]["title"]),
77      _allNotesInQueryResult[i]["content"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]["content"]),
78     DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_created"] * 1000),
79     DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_last_edited"] * 1000),
80      Color(_allNotesInQueryResult[i]["note_color"] ))
81  );
82  }
83
84  void retrieveAllNotesFromDatabase() {
85  // queries for all the notes from the database ordered by latest edited note. excludes archived notes.
86    var _testData = noteDB.testSelect();
87    _testData.then((value){
88        setState(() {
89          this._allNotesInQueryResult = value;
90          CentralStation.updateNeeded = false;
91        });
92    });
93  }
94}

This view needs tiles for notes to display. The tile we design for the view must preview the title and the content of the note. To handle the text of different length in the tile we’ll use a library to create auto-expanding text view. We only have to define line limit and the widget will auto-expand to accommodate the content till that limit.

Like segue in iOS and Intent in Android, to navigate between pages in Flutter we use Navigator.

1import 'package:flutter/material.dart';
2import 'package:auto_size_text/auto_size_text.dart';
3import '../ViewControllers/NotePage.dart';
4import '../Models/Note.dart';
5import '../Models/Utility.dart';
6
7class MyStaggeredTile extends StatefulWidget {
8  final Note note;
9  MyStaggeredTile(this.note);
10  @override
11  _MyStaggeredTileState createState() => _MyStaggeredTileState();
12}
13
14class _MyStaggeredTileState extends State<MyStaggeredTile> {
15
16  String _content ;
17  double _fontSize ;
18  Color tileColor ;
19  String title;
20
21  @override
22  Widget build(BuildContext context) {
23
24    _content = widget.note.content;
25    _fontSize = _determineFontSizeForContent();
26    tileColor = widget.note.note_color;
27    title = widget.note.title;
28
29    return GestureDetector(
30      onTap: ()=> _noteTapped(context),
31      child: Container(
32      decoration: BoxDecoration(
33        border: tileColor == Colors.white ?   Border.all(color: CentralStation.borderColor) : null,
34          color: tileColor,
35          borderRadius: BorderRadius.all(Radius.circular(8))),
36      padding: EdgeInsets.all(8),
37      child:  constructChild(),) ,
38    );
39  }
40
41  void _noteTapped(BuildContext ctx) {
42    CentralStation.updateNeeded = false;
43    Navigator.push(ctx, MaterialPageRoute(builder: (ctx) => NotePage(widget.note)));
44  }
45
46  Widget constructChild() {
47    List<Widget> contentsOfTiles = [];
48
49    if(widget.note.title.length != 0) {
50      contentsOfTiles.add(
51        AutoSizeText(title,
52          style: TextStyle(fontSize: _fontSize,fontWeight: FontWeight.bold),
53          maxLines: widget.note.title.length == 0 ? 1 : 3,
54          textScaleFactor: 1.5,
55        ),
56      );
57      contentsOfTiles.add(Divider(color: Colors.transparent,height: 6,),);
58    }
59
60    contentsOfTiles.add(
61        AutoSizeText(
62          _content,
63          style: TextStyle(fontSize: _fontSize),
64          maxLines: 10,
65          textScaleFactor: 1.5,)
66    );
67    return Column(
68        crossAxisAlignment: CrossAxisAlignment.start,
69        mainAxisAlignment: MainAxisAlignment.start,
70        children:     contentsOfTiles
71    );
72  }
73
74 double _determineFontSizeForContent() {
75    int charCount = _content.length + widget.note.title.length ;
76    double fontSize = 20 ;
77    if (charCount > 110 ) { fontSize = 12; }
78    else if (charCount > 80) {  fontSize = 14;  }
79    else if (charCount > 50) {  fontSize = 16;  }
80    else if (charCount > 20) {  fontSize = 18;  }
81    return fontSize;
82  }
83}

The tile in the view will look something like this.

Example of a tile in the view

Implementing the Note Page

Now we need a view to edit/create a note. which will also possess various useful actions in AppBar to undo, archive and more. The more action will bring up a bottom sheet with options like share, duplicate, delete permanently and a horizontally scrollable colour picker with which we’ll be able to change the background colour of that particular note.

We’ll segregate NotePage, BottomSheet and ColorSlider widgets in different classes and files to keep the code clean and manageable. To change the colour on all of them when the user picks a new one from the ColorSlider we need to update the state. We can connect these three widgets via callback functions to respond to the changes so they can update themselves.

Diagram illustrating widget connections for state updates
1import 'package:flutter/material.dart';
2
3class ColorSlider extends StatefulWidget {
4  final void Function(Color)  callBackColorTapped ;
5  final Color noteColor ;
6  ColorSlider({@required this.callBackColorTapped, @required this.noteColor});
7  @override
8  _ColorSliderState createState() => _ColorSliderState();
9}
10
11class _ColorSliderState extends State<ColorSlider> {
12
13  final colors = [
14    Color(0xffffffff), // classic white
15    Color(0xfff28b81), // light pink
16    Color(0xfff7bd02), // yellow
17    Color(0xfffbf476), // light yellow
18    Color(0xffcdff90), // light green
19    Color(0xffa7feeb), // turquoise
20    Color(0xffcbf0f8), // light cyan
21    Color(0xffafcbfa), // light blue
22    Color(0xffd7aefc), // plum
23    Color(0xfffbcfe9), // misty rose
24    Color(0xffe6c9a9), // light brown
25    Color(0xffe9eaee)  // light gray
26  ];
27
28   final Color borderColor = Color(0xffd3d3d3);
29   final Color foregroundColor = Color(0xff595959);
30
31  final _check = Icon(Icons.check);
32  Color noteColor;
33  
34  int indexOfCurrentColor;
35  @override void initState() {
36    super.initState();
37    this.noteColor = widget.noteColor;
38    indexOfCurrentColor = colors.indexOf(noteColor);
39  }
40
41  @override
42  Widget build(BuildContext context) {
43    return ListView(
44      scrollDirection: Axis.horizontal,
45      children:
46      List.generate(colors.length, (index)
47      {
48        return
49          GestureDetector(
50              onTap: ()=> _colorChangeTapped(index),
51              child: Padding(
52                  padding: EdgeInsets.only(left: 6, right: 6),
53                  child:Container(
54                  child: new CircleAvatar(
55                    child: _checkOrNot(index),
56                    foregroundColor: foregroundColor,
57                    backgroundColor: colors[index],
58                  ),
59                  width: 38.0,
60                  height: 38.0,
61                  padding: const EdgeInsets.all(1.0), // border width
62                  decoration: new BoxDecoration(
63                    color: borderColor, // border color
64                    shape: BoxShape.circle,
65                  )
66              ) )
67          );
68      })
69      ,);
70  }
71
72  void _colorChangeTapped(int indexOfColor) {
73    setState(() {
74      noteColor = colors[indexOfColor];
75      indexOfCurrentColor = indexOfColor;
76      widget.callBackColorTapped(colors[indexOfColor]);
77    });
78  }
79
80  Widget _checkOrNot(int index){
81    if (indexOfCurrentColor == index) {
82      return _check;
83    }
84    return null;
85  }
86
87}
 Note-Taking App in Flutter/Dart

I’ve added some handy features as well to undo changes, archive, share, duplicate and permanently delete the note.

The entire codebase for the app can be found at my repository. Feel free to drop by and give it a go yourself.

Authors

Jay Naik

Software Engineer
I find interest in solving logical challenges. Learning new technologies and working with data. I'm also an enthusiast about mobile development and intuitive design and user experience.

Tags

No items found.

Have a project in mind?

Read