© Frank Zammetti 2019
Frank ZammettiPractical Flutterhttps://doi.org/10.1007/978-1-4842-4972-7_5

5. FlutterBook, Part I

Frank Zammetti1 
(1)
Pottstown, PA, USA
 

Okay my friend, now it’s time for some fun! We’ve slogged through the preliminaries, you’ve got a good foundation of knowledge about Dart and Flutter, so now it’s time to put it to good use and start building some real apps! Over the next five chapters, we’ll create three apps together, beginning with FlutterBook.

In the process, you’ll get some real experience with Flutter, just the thing you need to reach the next level of your Flutter journey.

So, without further ado, let’s get to it, beginning with talking about something that seems like it might be kind of relevant in this endeavor: discussing what, exactly, it is that we’re going to build!

What Are We Building?

The term PIM was made popular back in the days of the original PalmPilot devices, though it existed before then. PIM stands for “Personal Information Manager” (or “Management,” depending on who you ask) and is basically a fancy way to say an application (or device, in the case of the PalmPilot) that stores some basic information that most busy, modern people need to know, and allows it to be consumed easily. Before the electronics age, you might have a little notepad with tabs for various bits of information, but it all amounts to about the same thing either way. What data constitutes a PIM can vary, but for most people, there are four primary pieces of information: appointments, contacts, notes, and tasks. There can be others, and there can even be some overlap between those four, but those are generally considered to be the basics, and they are precisely what FlutterBook will contain.

This application will present four “entities,” which is the term I’ll use to apply generically to appointments, contacts, notes, and tasks. It will provide a way for the user to enter items of each type, store them on the device, and present a way for them to be viewed, edited, and deleted. As we build the app, we’ll do so in a roughly modular way so that later, if you want, you can add other modules to deal with other types of data (hey now, that sounds like a suggested exercise to me!). For example, maybe bookmarks are something you’d like in your PIM too, or maybe recipes if you’re a chef. The point is, you’ll be able to add them without much difficulty because we’ll design the code to be reasonably modular and easy to extend.

But, it’s all well and good to talk about what it is, but seeing it is better, no? I think so, and that’s why I’ve provided Figure 5-1 for you to look at.
../images/480328_1_En_5_Chapter/480328_1_En_5_Fig1_HTML.jpg
Figure 5-1

FlutterBook, the Appointments and Contacts entity list screens

As you can see, along the top are tabs that the user can click on to move between the four entity types (ostensibly, they can swipe to perform this navigation too, but swipe is a little problematic owing to the functionality provided on the screens, but we’ll talk about that later).

Each entity will have two screens to work with: a list screen and an entry screen. Here, you can see the list screens, although for the appointments on the left the term “list” is a bit of a misnomer because what you’ll actually see is a giant calendar that the user can interact with (you can see it peeking out from behind the day details, which appears by virtue of me having clicked the first date, so you can see the appointments for that day). For contacts though, it really is a list.

For notes and tasks, there is a similar pattern at play, as Figure 5-2 show.
../images/480328_1_En_5_Chapter/480328_1_En_5_Fig2_HTML.jpg
Figure 5-2

FlutterBook, the Notes and Tasks entity list screens

Each list screen is a little different in nature, owing to each entity type being a bit different: appointments make sense to be in a calendar, while contacts should show an avatar image, notes look (roughly) like sticky notes by using Cards, and the tasks list allows the user to check off completed tasks. It all also provides for a good variety of Flutter things to look at for your learning experience!

The entry screens we’ll get into as we look at each entity type, but this begins to give you an idea of what this thing looks like.

Note

Throughout this chapter, and in fact all the remaining chapters of this book, the code has been condensed by removing comments and print() statements and some spacing here and there, so what you’ll see in the download bundle will look a little different. But, rest assured that the actual executable code is identical.

Project Kickoff

To begin building FlutterBook, I simply used the new project wizard provided by Android Studio, and in fact, that’s how all the projects in this book began. It gives us the skeleton of what we need and a fully working app, if not a particularly exciting one, right out of the gate. From there, we begin to edit and add things as needed, starting with configuring the project.

Configuration and Plugins

The pubspec.yaml file, shown in Listing 5-1, has most of what we need automatically, but because this project is going to require us to dip into Flutter plugins a little bit, we’ll need to add a few, as you can see in the dependencies section:
name: flutter_book
description: flutter_book
version: 1.0.0+1
environment:
  sdk: ">=2.1.0 <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  scoped_model: 1.0.1
  sqflite: 1.1.2
  path_provider: 0.5.0+1
  flutter_slidable: 0.4.9
  intl: 0.15.7
  image_picker: 0.4.12+1
  flutter_calendar_carousel: 1.3.15+3
  cupertino_icons: ^0.1.2
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true
Listing 5-1

The pubspec.yaml file

Caution

Remember that YAML files are indentation-sensitive! For example, if one of these dependencies isn’t properly indented (“properly” here being two spaces from its parent), then you’ll run into problems. Note here that the child of flutter is sdk, but scoped_model is a child of dependencies, not flutter, therefore scoped_model should be two spaces to the right of dependencies, not two spaces to the right of flutter lined up with sdk. It’s an easy mistake to make (just ask my awesome technical reviewer!), especially if you’re new to YAML structure.

There are quite a few plugins here, and you will, of course, learn a little about them as they are encountered in the code, but just to give you a basic overview, they are
  • scoped_model – This will provide us a very nice way to manage state throughout the app.

  • sqflite – Since data storage is a requirement of this app, we have to choose how to do so, and I decided to go with the popular SQLite database, which this plugin provides us access to (and no, the name is not a typo!).

  • path_provider – For the contacts, we’ll have to store the avatar image, if any, for the contact, and SQLite turns out to not be the best place to do that. Instead, we’ll use the file system. Each app gets its own documents directory where we can store arbitrary files, and this plugin helps us get to that.

  • flutter_slidable – For contacts, notes, and tasks, the user can slide them on the list screen to reveal a delete button. This is a widget that gives us that capability.

  • intl – We’ll need some date and time formatting functions from this since some of our entities deal with dates and times.

  • image_picker – This plugin provides the infrastructure the app will need to let the user add avatar images for contacts from either their gallery or by taking a picture with the camera of their device.

  • flutter_calendar_carousel – This widget provides the calendar and functionality for the appointments list screen.

Everything else in this file should look familiar to you by now and aside from the dependencies listed here is what the new project wizard created for us.

UI Structure

The basic structure of this app’s UI is shown in Figure 5-3.
../images/480328_1_En_5_Chapter/480328_1_En_5_Fig3_HTML.png
Figure 5-3

The basic UI structure

While this doesn’t show every last detail, it gives you a high-level picture of things. At the top, the main widget, is a MaterialApp, with a DefaultTabController under it with a Scaffold right below it. Under that is a TabBar. Under the TabBar are four main screens, one for each of the four entities. Each of those has two “sub-screens,” if you will: the list and the entry screens, and these are children of an IndexedStack. This allows the code to show either of the two screens just by changing the index of the stack. Under the list screen for appointments is a BottomSheet that shows the details for a selected date, and under the entry screen for contacts is a dialog shown for the user to select an image source (camera or gallery).

The details of the list and entry screens for each type are, of course, more complicated than this, but we’ll look at those later when the time is right. Before then though, let’s talk about the basic structure of the app from a code standpoint.

App Code Structure

As far as the directory structure of the app goes, it’s 100% standard, nothing new to see here. All the code for the app lives in the lib directory like always, although this time, given the number of files and the desire to make it at least somewhat modular, each entity type gets its own directory. So, the lib/contacts directory contains the files related to contacts, lib/notes the files related to notes, and so on.

In each of them, you’ll find the same basic set of files, and in all the following cases, xxx is the entity name, so Appointments, Contacts, Notes, or Tasks:
  • xxx.dart – These files are the main entry point to each of these screens.

  • xxxList.dart – The list screen for the entity.

  • xxxEntry – The entry screen for the entity.

  • xxxModel.dart – These files contain a class to represent each entity type, as well as a model object as required by scoped_model (we’ll get into that later).

  • xxxDBWorker.dart – These files contain the code that works with SQLite. It provides an abstraction layer over the database so you could change the data storage mechanism without changing the application code, just these files would need to change.

The Starting Line

Now it’s time to start looking at code! As per the usual, it all starts in the main.dart file in the root of the project:
import "dart:io";
import "package:flutter/material.dart";
import "package:path_provider/path_provider.dart";
import "appointments/Appointments.dart";
import "contacts/Contacts.dart";
import "notes/Notes.dart";
import "tasks/Tasks.dart";
import "utils.dart" as utils;
void main() {
  startMeUp() async {
    Directory docsDir =
      await getApplicationDocumentsDirectory();
    utils.docsDir = docsDir;
    runApp(FlutterBook());
  }
  startMeUp();
}

Let’s stop here and discuss this preceding bit (I will typically break up these listing to present them in more digestible chunks – especially for the longer ones, it’s an important thing to help you understand what’s going on).

First, we have some imports. You already know that material.dart is the code of Material Flutter classes. We need the io library and the path_provider plugin for getting the application’s documents directory (we’ll come back to this shortly). The rest are application code. The four screen files are imported, and then utils.dart is brought in. We’ll be looking at that in the next section, but in short, it contains some functions and variables that are global concerns throughout the code and so live in this file.

After that comes the usual main() function where execution begins. Here, there’s a bit of trick in that we need to retrieve the app’s documents directory. The getApplicationDocumentsDirectory() function is provided by the path_provider.dart import for this purpose. It returns a Directory object, which is provided by the Dart io library imported. In addition to this function, this plugin also supplies a getExternalStorageDirectory(), which is only available on Android (and then only some devices), so you should usually check the OS type before making this call. This provides a path to top-level storage on an external storage device (SD card usually) where the app can read and write data. Finally, there is a getTemporaryDirectory() function . This returns path the temporary directory for the app (where you typically write short-lived, transient data, in contrast to getApplicationDocumentsDirectory(), which provides durable storage).

There’s a problem here though: we have to ensure that no other code executes until this code completes because otherwise, we’ll throw exceptions due to the databases not being available. As you’ll see later, each of the four databases, one for each entity, is a separate SQLite file stored in the app’s documents directory, so if that code gets called before docDir is determined, which it would be because the screens would load when the main widget is created, we’d have issues. So, to accomplish that, I create a function inside main() (yes, you can do that in Dart!) and ensure it’s an async function because we’ll await the call to getApplicationDocumentsDirectory(). Once that returns the Directory, that gets stored in utils.docsDir (so that we only have to get a reference to this directory once) and then call the usual runApp(), passing it a new instance of the FlutterBook class.

Note

This isn’t necessarily the best way to do things because it means that the UI won’t be built until getApplicationDocumentsDirectory() resolves. That’s usually not a good thing to do in terms of user experience, but given that this isn’t going to take too long, not even noticeable, I’d expect, this was the easiest way to do things.

After that, the main widget is created, which is where that FlutterBook class comes into play, as you can see here:
class FlutterBook extends StatelessWidget {
  Widget build(BuildContext inContext) {
    return MaterialApp(
      home : DefaultTabController(
        length : 4,
        child : Scaffold(
          appBar : AppBar(
            title : Text("FlutterBook"),
            bottom : TabBar(
              tabs : [
                Tab(icon : Icon(Icons.date_range),
                  text : "Appointments"),
                Tab(icon : Icon(Icons.contacts),
                  text : "Contacts"),
                Tab(icon : Icon(Icons.note),
                  text : "Notes"),
                Tab(icon : Icon(Icons.assignment_turned_in),
                  text : "Tasks")
              ]
            )
          ),
          body : TabBarView(
            children : [
              Appointments(), Contacts(), Notes(), Tasks()
            ]
          )
        )
      )
    );
  }
}

First, we have that MaterialApp I mentioned earlier, with a DefaultTabController as its home screen. DefaultTabController is a type of TabController, which is responsible for coordinating tab selection in a TabBar, which you can see is the bottom child of the AppBar under the Scaffold. The controller takes care of switching between the children, which are defined by the tabs property of the TabBar. Each entry in tabs is a Tab object, which can have an icon and/or a text label, and I opted to show both. With this setup, you don’t have to do anything else to enable navigation; these widgets take care of it for you.

Finally, the body of the Scaffold must be a TabBarView, so that it can be appropriately displayed by the TabBar and managed properly by DefaultTabController. The children of it are the four screens, one for each entity (and obviously those are where most of the action is, and we’ll get to that action in short order, but we have some other things to look at first, starting with utils.dart).

Some Global Utilities

The utils.dart file contains those global utility-type bits I spoke of earlier, so let’s take a look at it now:
import "dart:io";
import "package:flutter/material.dart";
import "package:path_provider/path_provider.dart";
import "package:intl/intl.dart";
import "BaseModel.dart";
Directory docsDir;

As you saw when we looked at main.dart, the docDir is the app’s documents directory, which was captured in main() there.

The next thing we find in this file is the selectDate() function:
Future selectDate(
  BuildContext inContext, BaseModel inModel,
  String inDateString
) async {
  DateTime initialDate = DateTime.now();
  if (inDateString != null) {
    List dateParts = inDateString.split(",");
    initialDate = DateTime(
      int.parse(dateParts[0]),
      int.parse(dateParts[1]),
      int.parse(dateParts[2])
    );
  }
  DateTime picked = await showDatePicker(
    context : inContext, initialDate : initialDate,
    firstDate : DateTime(1900), lastDate : DateTime(2100)
  );
  if (picked != null) {
    inModel.setChosenDate(
      DateFormat.yMMMMd("en_US").format(picked.toLocal())
    );
    return "${picked.year},${picked.month},${picked.day}";
  }
}

This function will be a little hard to explain fully at this point because it depends on a few things you haven’t seen yet, but let’s go as far as we can, and you can come back to it later where you have that other information.

Firstly, this is used to select a date on the appointments, contacts, and tasks entry screens (the date of an appointment, the birthday of a contact or the due date of a task). As such, it must be generic and work with all three entity types (and perhaps others later). So, what gets passed to it is the BuildContext of the entry screen it’s called from, along with something called a BaseModel and a date in string form. The BaseModel is that thing we haven’t gotten to yet, so, for now, it’s enough to say that it’s ultimately where the selected date will go and leave it at that. The date passed in, which is optional, will be in the form yyyy,mm,dd if supplied, and this is a common form throughout the code. The reason is that when storing a date to SQLite, there is no date data type available, so saving it as a string makes sense. I chose this format because it makes it easy to construct a DateTime object since its constructor takes precisely those pieces of information in just that order, something you can see here. If a date is passed in, the split() function is used to tokenize it, then the DateTime is constructed, passing each of the parsed tokens to it, so the year, followed by the month, followed by the day, exactly as it appears in the string form.

The initialDate is what day will be selected when the popup calendar is shown, which is applicable when editing an entity only (when creating, there will be no initialDate specified so that the calendar will select the current day).

Then, a call to showDatePicker() is called, something you saw in the previous two chapters. This displays a popup calendar for the user, and it returns a DateTime instance. Note that the range of selectable years is from 1900 to 2100. Logically, it would make more sense to limit it based on entity type (vis a vis, there’s no point creating an appointment for a date in the past), and that’s where firstDate and lastDate comes into play. But, just to keep the volume of code down, I didn’t implement this logic and instead picked a range that would, nominally at least, work for all the entity types.

Once showDatePicker() comes back (it’s asynchronous after all, as we can tell by the await in the call to it), we see if they picked something. The returned value will be null if the user clicks Cancel. Otherwise, we’ll have a DateTime object for the selected date. Now, as I alluded to earlier, we have to store that date in the BaseModel instance that was passed in, so a call to the setChosenDate() function accomplishes that. The value passed needs to be in a human-readable form, and what toString() of a DateTime object provides by default arguably isn’t, so now we use some functionality from the intl.dart file imported. Specifically, the DateFormat.yMMMMd.format() function provides a string in the form “MONTH dd, yyyy” where MONTH is the full month name (January, February, March, etc.) This plugin contains a wealth of date and time formatting code, as well as other general internationalization and localization functionality. For more info, see here: https://pub.dartlang.org/packages/intl (I’ll rarely describe these modules in their entirety as there’s typically way too much to go into detail on, so we’ll discuss just what’s needed by the code here – the path_provider was an exception because it doesn’t offer all that much, even though what it offers is rather necessary!)

However, we’re not done yet! The code that called this function needs that date back, so it is returned. The form it’s returned in is the same form that may have been passed in, namely yyyy,mm,dd.

Like I said, this function will make a little more sense once you know about the models and later when you see it used, which you, in fact, won’t until the next chapter, so let’s not dwell on it and instead look at the models a bit, which gets into the topic of state management.

On State Management

The concept of state and state management – that is, where the data that your widgets produce and consume and how your code interacts with it – is a topic that, surprisingly, is mostly left to developers to figure out. Flutter, at least at the time of this writing, doesn’t say anything definitive on the topic (rumor has it that is changing and before long Flutter maybe have a canonically “correct” state management approach, but it was not yet the case when this book went to print).

Oh, of course, you’ve got the notion of stateful widgets that was explored in previous chapters (and which you’ll undoubtedly see again before this book is over). That indeed is a form of state. But really, it’s only one kind of state: local state. In other words, it’s state that is local to a given component. For that, stateful widgets tend to be quite sufficient.

But there is another kind of state, a state which you might consider “global.” To put it another way, its state that is needed outside the widget and, in many cases, beyond the lifetime of a given widget. Maybe widgets that are children of another need its parents’ state. Or, maybe vice versa, the parent of a given widget (and maybe not the direct parent) needs access to its child’s state. The former case isn’t too tough, but the latter can be surprisingly frustrating in Flutter. Or, perhaps the widget that needs to see the state of another isn’t even in the same widget tree (not directly anyway). These situations can get tricky to deal with if all you have to work with are stateful widgets and the setState() paradigm that provides.

As I said, Flutter doesn’t specify a definitive answer here. There are numerous state management solutions available to you in a Flutter app beyond setState(), just to name a few: BLoC, Redux, and scoped_model. There’s probably a dozen more out there at least, all with pros and cons. So, which state management approach you use will be dependent on many factors including, but not limited to, your goals for the project, the specific state interactions you need and, at the end of the day, your simple personal preferences on how you like to structure your code.

In this project, and in fact for the rest of this book, I’m going to focus on one specific approach from that list: scoped_model . The reason for this decision comes down to just my belief that scoped_model is perhaps the most straightforward option available because that simplicity tends to make for simpler application code, and I like that! Simple is solid, as the saying goes! Honestly, when looking at all the options, scoped_model just makes the most sense to my brain, so that’s the one I’m going with. You of course absolutely should explore the options and see what suits your mental model. If it winds up being scoped_model , then great! If not, no problem, we can still be friends, but at least after reading this book, you’ll have a good understanding of at least this one option so as to be able to do a meaningful comparison to the others.

So, what is scoped_model all about? Well, it’s just three simple classes that, when used in conjunction with three simple steps on your part, provides a model – that is, a store of data – for a widget tree.

The first class required that you create a model class, which will extend from the scoped_model Model class, and here is where you will place your data-handling logic and, naturally, your data variables. Note that you may not have any need for any real logic, and that’s perfectly okay (though a little atypical). The whole purpose of putting the code in this model class is ultimately so that you can call the notifyListeners() method of the base Model class (which can only be called from a subclass of Model). This is the secret sauce! Calling this method informs any widget that has been “hooked up” to the model class that the model has changed and they should, if necessary, re-paint themselves.

The second step is hooking scoped_model up to your widget tree. This part is super-easy: just wrap a widget in the second class you must know about: ScopedModel. For example, if your top-most widget is a Column, then you might do:
return ScopedModel<your-model-class-here>(
  child : Column(...)
)

Actually, you don’t have to wrap the top-most widget in the tree, though that is most common because it means that any widget in the tree can have access to your model. But, if only a subset of widgets needs access to the model, then you can instead choose a widget that is a parent to those, even if not the top-most widget, and wrap that in ScopedModel. In either case, you must tell ScopedModel the type via the generic declaration <your-model-class-here> (that’s the class that extends from the scoped_model base Model class).

Finally, for any widgets underneath the one wrapped in ScopedModel that you want to access the model, wrap that widget in the third class to be aware of: ScopedModelDescendent (and again specifying the type). As with ScopedModel, you don’t need to wrap every single widget separately; just wrapping one will cover all its children too. Any widgets wrapped with this class will rebuild when the model changes (assuming Flutter’s diff algorithm determines it to be necessary of course). The syntax for ScopedModelDescendent is a little different then ScopedModel though because the builder pattern is required:
return ScopedModel<your-model-class-here>(
  child :
    ScopedModelDescendent<your-model-class-here>(
      builder : (BuildContext inContext, Widget inChild,
        <your-model-class-here> inModel) {
          return Column(...);
    )
  );
)
Now, within the Column, if you have a Text that you want to display the value from the model with, you can do:
Text(inModel.myVariable)

And voila, you’ve got yourself a store of data that is the state of your app, ready to be used and that will rebuild your UI when the data changes, all without using stateful widgets (that’s right, you can do all of this with stateless widgets!) and in a more global way.

The final piece of the puzzle is changing the state, and to understand, let’s look at an actual model class, the BaseModel.dart file from FlutterBook. Before we start that exploration though, let me say that each type of entity FlutterBook deals with has its own model class. You don’t have to do it that way – you could have a single model class that holds the data for all four entity types. But I feel that keeping them separate is more logical. But, the fact is that they all have a few bits of commonality between them, so rather than duplicate code, I instead created a BaseModel class, and this is what extends from the scoped_model base Model class. Then, the model classes for the individual entity types extends from this BaseModel class , which obviously means they extend from the scoped_model Model class as well, just as we need in the end.
import "package:scoped_model/scoped_model.dart";
Obviously, scoped_model won’t be of much use to us unless we import it, so that’s imported first. Then, the BaseModel class begins:
class BaseModel extends Model {
Ah, see, it really does extend from the Model class that scoped_model provides!
int stackIndex = 0;
List entityList = [ ];
var entityBeingEdited;
String chosenDate;
These are the four pieces of information that all (or at least most, in the case of chosenDate, have in common). Remember earlier how I said that each of the four entity’s screens is in actuality two screens, list, and entry, both children of an IndexedStack? Well, which is showing depends on the setting of the stackIndex variable here. Also, since all four entity types have some sort of list of entities, the enityList will contain them. The entityBeingEdited will be a reference to the entity that the user selects when they want to edit an existing entity. This is how the data for the entity is transferred from the list screen to the entry screen. Finally, the chosenDate variable will store a date chosen by the user when editing an entry. You’ll see why this is needed shortly, but for now, let’s continue with this class.
void setChosenDate(String inDate) {
  chosenDate = inDate;
  notifyListeners();
}
When the user chooses a date, they’ll do so via a popup, but then the selected date has to get back into the model. A call to this method will do that. As you can see, the last thing it does is calls notifyListeners(). This is key because this is what updates the screen to show the date that was selected. Without this, the data would be saved in the model, but the user wouldn’t know it by looking at the screen because the widgets wrapped by ScopedModel (and ScopedModelDescendent) wouldn’t have known to re-paint themselves otherwise.
void loadData(String inEntityType, dynamic inDatabase) async {
  entityList = await inDatabase.getAll();
  notifyListeners();
}

The loadData() method will be called whenever an entity is added or removed from entityList (code which you’ll see soon). This makes use of the xxxDBWorker class, the one that knows how to talk to SQLite. Once again, we’ll be getting to this soon, but for now, just note that the result of the call to the getAll() method replaces entityList, and then notifyListeners() is again called so that the list of entities re-paints itself. Seeing the pattern yet?

Finally, we have the setStackIndex() method :
void setStackIndex(int inStackIndex) {
  stackIndex = inStackIndex;
  notifyListeners();
}

This is the method that will be called whenever we want to navigate the user between the list and entry screens for a given entity.

I realize that you don’t yet have the full context in which this code is used, but you will before long! For now, the basic concepts of scoped_model are what’s important, and hopefully, that’s starting to make some sense. It will, I expect, make complete sense once we see the code for an entity, and that’s exactly what we’re looking at right now!

Starting with an Easy One: Notes

Of the four entity types, I think the code for notes is probably the simplest, so that’s probably a good place to start. We begin that exploration with the code that defines the main, top-level screen, for this entity type.

The Starting Point: Notes.dart

As you’ll recall each of the four entities has a master screen that is the main content of its tab. The Notes.dart file contains the code for that screen, and it begins, as most Dart source files do, with some imports:
import "package:flutter/material.dart";
import "package:scoped_model/scoped_model.dart";
import "NotesDBWorker.dart";
import "NotesList.dart";
import "NotesEntry.dart";
import "NotesModel.dart" show NotesModel, notesModel;
Aside from the usual suspects like material.dart, we have scoped_model.dart coming in. As you’ll see the entire widget tree for this screen will have access to the model for notes. We also need to bring in the NotesDBWorker.dart file so that we can load the notes data, as you’ll see next. Then, we need the source files for the two sub-screens: NotesList.dart and NotesEntry.dart. Finally, we need the model for notes in NotesModel.dart. We’ll get to all of those in turn, but marching on with this source file we have:
class Notes extends StatelessWidget {

Ah, it’s the beginning of a widget! Most importantly, note that it’s a stateless widget. Remember: using scoped_model means you’re dealing with state, but that doesn’t imply you have to have stateful widgets. Stateful widgets are effectively another approach to state, an approach we’re not using in this app (in this source file or any other).

After that, we find a constructor:
Notes() {
  notesModel.loadData("notes", NotesDBWorker.db);
}
Recall that the BaseModel has a loadData() method and it was written generically so it would work with any entity type. However, the only reason it can be written generically like that is that the constructor here calls it and provides the entity-specification information it needs, namely the entity type and a reference to the database for this entity type (the database stuff is coming up!). The result of this call is that entityList in the model will have a list of notes loaded into it from the SQLite database and so when the list screen is built, they will be displayed. Technically, since this data load is asynchronous, the list screen can and usually is built before the data is available, but due to it being wrapped in scoped_model and loadData() calling notifyListeners() when the data is loaded, the screen gets notified when the data is available and re-paints to show the data, all of which happens quickly.
Widget build(BuildContext inContext) {
  return ScopedModel<NotesModel>(
    model : notesModel,
    child : ScopedModelDescendant<NotesModel>(
      builder : (BuildContext inContext, Widget inChild,
        NotesModel inModel
      ) {
        return IndexedStack(
          index : inModel.stackIndex,
          children : [ NotesList(), NotesEntry() ]
        );
      }
    )
  );
}

Finally, the widget is returned from the build() method, which you of course know must be present given that this entire source file is defining a widget. You can see the ScopedModel at the top, with a ScopedModelDescendent underneath it, as discussed earlier. An IndexedStack is used to contain the two screens, which are defined in separate source files that we’ll look at soon. Notice that the index value of the IndexedStack is a reference to the stackIndex field in the NotesModel instance. That’s how we can display one screen vs. the other: set the value of stackIndex to 0, and NotesList is shown; set it to 1 to display NotesEntry (assuming, of course, that notifyListeners() is called after that change, which it is, as you saw in BaseModel).

The Model: NotesModel.dart

The model class for this entity is found in NotesModel.dart. The model for this entity type isn’t just the model class though; it’s also a class representing a note.

But, before any of that, we start with
import "../BaseModel.dart";

As you know, this class will extend BaseModel, which itself extends Model from scoped_model, so it must be imported.

Next, we have a class definition:
class Note {
  int id;
  String title;
  String content;
  String color;
  String toString() {
    return "{ id=$id, title=$title, "
      "content=$content, color=$color }";
  }
}

Instances of this class represent notes. Each note has four pieces of information: a unique id, a title, content (which is the note text itself), and a color for the background of the Card on the list screen for a note, so each of those is represented by a member variable here. While not required, I also added a toString() method, which overrides the default implementation provided by the Object class , which is the parent of all classes in Dart. That default implementation isn’t beneficial: it just says what type the object it’s called on is. This version instead shows the details of the note, which is very handy when debugging when you want to print() a note object to the console.

Next up is the model class itself:
class NotesModel extends BaseModel {
  String color;
  void setColor(String inColor) {
    color = inColor;
    notifyListeners();
  }
}

Yep, that’s it! Most of what this class needs are provided by BaseModel, so it’s just that color that is an issue. At the risk of jumping the gun a bit: this is needed because when the user selects a color block on the entry screen, just changing the values in a Note instance wouldn’t reflect in the model and the screen wouldn’t know to change. We instead need a direct member of the model class for this to occur. Don’t worry; I don’t expect that you’ll totally understand this part just yet! Once we get to the edit screen, it should start to make sense quickly.

But, there’s one more line in this file, and it’s rather important:
NotesModel notesModel = NotesModel();

We have a class definition before this, but we don’t have an instance of NotesModel yet. That’s what we get from this line. The file is only ever parsed once, no matter how many times it’s imported, or where it’s imported, so this ensures we only ever have a single instance of NotesModel, which it happens is exactly all we need!

The Database Layer: NotesDBWorker.dart

The next file to look at is NotesDBWorker.dart, which contains all the code for working with SQLite. First up are some imports:
import "package:path/path.dart";
import "package:sqflite/sqflite.dart";
import "../utils.dart" as utils;
import "NotesModel.dart";

There’s probably not too much surprising there. The path.dart module contains functions for working with paths on a file system in an ostensibly cross-platform manner. Things like getting the platform separator character, normalizing paths, getting file extensions from a path, and so on. Most of the typical path operations you’d expect are here, but we’ll just need one, which will turn up shortly.

Before that though, the NotesDBWorker class itself begins:
class NotesDBWorker {
  NotesDBWorker._();
  static final NotesDBWorker db = NotesDBWorker._();

The first step is ensuring there is only ever a single instance of this class, so we’re going to implement a singleton pattern. That begins with creating a private constructor, as seen on the first line. On the second line, the constructor is called, and the instance of the class stored statically in db.

Next, we need to have an instance of the Database class, which is the key class when dealing with SQLite via the sqflite plugin:
Database _db;
Future get database async {
  if (_db == null) {
    _db = await init();
  }
  return _db;
}

When the database getter is called, we see if there is already an instance in _db. If so, it’s returned, but if not, then the init() method is called. Doing this ensures that the single instance of NotesDBWorker only ever has one Database object in it, which is exactly what we want to ensure no data integrity issues.

Now, speaking of that init() method:
Future<Database> init() async {
  String path = join(utils.docsDir.path, "notes.db");
  Database db = await openDatabase(
    path, version : 1, onOpen : (db) { },
    onCreate : (Database inDB, int inVersion) async {
      await inDB.execute(
        "CREATE TABLE IF NOT EXISTS notes ("
          "id INTEGER PRIMARY KEY,"
          "title TEXT,"
          "content TEXT,"
          "color TEXT"
        ")"
      );
    }
  );
  return db;
}

The key task here is to make sure the notes database exists in SQLite. The database will be stored as a file in the app’s documents directory, so we need a path to that. Here, the one function from the path module we need is used: the join() method , which concatenates the documents directory path to the name of the file, notes.db (we’re free to call it whatever we want, but I dare say that name is logical).

Once that’s done, we need to create a Database object from that path, which is where the openDatabase() function comes in. We feed it the path, plus a version (which allows you to do schema updates if need be) plus a callback function to call when the database is opened (which here is empty since there’s nothing to do in this situation). We also give it a function to call when the database is created, which is where we create the table we need for notes, assuming it doesn’t already exist. The execute() method of the created Database object is how we do that, and it simply takes the SQL to execute. Once that’s done, the Database instance is returned, which you’ll recall gets stored in _db in the database getter. After that, we’re ready to perform database operations!

But, before we get to those operations, there are two helper functions we have to create. The problem, so to speak, is that SQLite and sqflite don’t know anything about our Note class , all they know are basic Dart maps. So, we need to provide some functions that can convert from a map to a Note and vice-versa. They’re nothing fancy though, as you can see:
Note noteFromMap(Map inMap) {
  Note note = Note();
  note.id = inMap["id"];
  note.title = inMap["title"];
  note.content = inMap["content"];
  note.color = inMap["color"];
  return note;
}
Map<String, dynamic> noteToMap(Note inNote) {
  Map<String, dynamic> map = Map<String, dynamic>();
  map["id"] = inNote.id;
  map["title"] = inNote.title;
  map["content"] = inNote.content;
  map["color"] = inNote.color;
  return map;
}

Yep, quite simple, and I’d bet entirely apparent to you by now, so let’s get to more exciting stuff: creating a note in the database!

Note

This is also why, as much as I wanted to, I couldn’t have a single DBWorker for all entities. Aside from the actual SQL statements being different, which I could have dealt with by just using some switch statements, there doesn’t at present appear to be something akin to Java’s reflection capabilities in Dart. From my reading, that’s something that’s coming, but when I wrote this code, it wasn’t possible, so without winding up with something very convoluted, there didn’t seem to be a way to do this dynamically. I like Dart, but sometimes I miss the free-wheeling, reckless abandon of JavaScript!

Future create(Note inNote) async {
  Database db = await database;
  var val = await db.rawQuery(
    "SELECT MAX(id) + 1 AS id FROM notes"
  );
  int id = val.first["id"];
  if (id == null) { id = 1; }
  return await db.rawInsert(
    "INSERT INTO notes (id, title, content, color) "
    "VALUES (?, ?, ?, ?)",
    [ id, inNote.title, inNote.content, inNote.color ]
  );
}

Creating a note is a three-step process. First, we need to get a reference to the Database object, so we await that (remember: the getter function will be called to satisfy this). Second, we need to come up with a unique ID for the note. To do this, we query the existing notes and just increment whatever the highest ID we find is. If this is the first note though, we’ll get null back, so we explicitly deal with that situation (in practice, null for an ID actually does work, but it strikes me as bad form if nothing else, so this check ensures we always have a valid numeric ID).

Once that’s done, the third step is to call the rawInsert() method of the Database object referenced by db is called and a simple SQL query executed to insert the values, which are naturally taken from the Note object passed in as inNote. As you can see, we return the Future that rawInsert() returns, so the caller of create() can await this result, but that’s the only information we need this method to return, so we’re done!

Note

If you look up the API for the Database object, you’ll see that in addition to the rawInsert() method, there is also an insert() method, and a similar split for other operations. Why use one vs. the other? In truth, I have no good reason to give you in this case! The insert() method is essentially an abstraction that saves you from having to write SQL yourself, which you have to do for rawInsert(). Personally, I’m comfortable with SQL and actually prefer writing it myself most of the time, but if you prefer something a little higher level, then you may prefer insert() to rawInsert() and, at least in this app, there’s not really any good reason to prefer one vs. the other, and aside from avoiding writing SQL I’m not sure there is in general either.

Next, we need the ability to get a specified note. In case it’s not obvious by now, we’re just implementing CRUD operations, that is, Create, Read (or “get”), Update, and Delete.
Future<Note> get(int inID) async {
  Database db = await database;
  var rec = await db.query(
    "notes", where : "id = ?", whereArgs : [ inID ]
  );
  return noteFromMap(rec.first);
}

The caller passes in the ID they want to retrieve, and the query() method of the Database instance is called. This takes the name of the table to query, and a where clause (there are multiple forms this method can take, this is just one) plus the values for that where clause. Here, we just need to query the id field. The result of this call will be a map, so we need that noteFromMap() function now to return a Note object.

Going along with this is the ability to retrieve all notes in one call, specifically to populate the list screen, which the getAll() method does:
Future<List> getAll() async {
  Database db = await database;
  var recs = await db.query("notes");
  var list = recs.isNotEmpty ?
  recs.map((m) => noteFromMap(m)).toList() : [ ];
  return list;
}

Here, the query() method just needs the name of the table, and it will dutifully retrieve all the records in it. If we got no records back then an empty list is returned, but if we did get records then we map() the returned list and for each, call noteFromMap(), and finally convert the resultant map to a list to return to the caller.

Updating a note is next:
Future update(Note inNote) async {
  Database db = await database;
  return await db.update("notes", noteToMap(inNote),
    where : "id = ?", whereArgs : [ inNote.id ]
  );
}

Well, that’s not too tough, is it? The update() method takes the name of the table, the map that contains the values to update (which we get by calling noteToMap() to convert the inNote Note object to a map) and the where clause to identify the record by ID to be updated. This method knows how to take the elements of the map and convert them to column names – well, there’s no real conversion necessary as it assumes the columns are named after the items in the map, but you knew what I mean!

The final method to look at is, of course, delete():
Future delete(int inID) async {
  Database db = await database;
  return await db.delete(
    "notes", where : "id = ?", whereArgs : [ inID ]
  );
}

Yep, that’s all there is to it! By this point, I would bet (and hope!) that an explanation isn’t necessary. So, let’s get to some screen code, starting with the list screen.

The List Screen: NotesList.dart

The list screen for notes begins with a set of imports:
import "package:flutter/material.dart";
import "package:scoped_model/scoped_model.dart";
import "package:flutter_slidable/flutter_slidable.dart";
import "NotesDBWorker.dart";
import "NotesModel.dart" show Note, NotesModel, notesModel;
class NotesList extends StatelessWidget {
The only thing new here, or unexpected, is that flutter_slidable.dart import; otherwise, we’ve got the usual suspects as far as imports go and a perfectly typical widget class beginning. Let’s skip that one import for the moment until we encounter it and instead start looking at the ubiquitous build() method :
Widget build(BuildContext inContext) {
  return ScopedModel<NotesModel>(
    model : notesModel,
    child : ScopedModelDescendant<NotesModel>(
      builder : (BuildContext inContext, Widget inChild,
        NotesModel inModel
      ) {
        return Scaffold(
As you’re now very familiar with, we have a ScopedModel that references the notesModel instance. This has a ScopedModelDescendent as a child so that all the children in this widget true can access the model. The builder() function is provided, and we begin to build our widget, which starts with a Scaffold, as is most common for a screen in a Flutter app.
floatingActionButton : FloatingActionButton(
  child : Icon(Icons.add, color : Colors.white),
  onPressed : () {
    notesModel.entityBeingEdited = Note();
    notesModel.setColor(null);
    notesModel.setStackIndex(1);
  }
)

This Scaffold has a floatingActionButton, which is how the user will add a new note. This floats in the lower right, over the content of the screen. When tapped, the onPressed function fires, and we kick off entry. To do this, we begin by creating a new Note instance and storing it in the model as the entityBeingEdited. This is the object that will ultimately be saved to the database, once all the data the user enters is put into it (which you’ll see in the next section about the entry screen).

One of the things the user can do on the entry screen is select a color for the note. Recall earlier when we talked about how the screen will re-paint itself when the model changes. Well, that’s going to be necessary whenever the user selects a color. But, just having the color stored in that new Note object won’t be enough since scoped_model won’t see it change (because it’s not a top-level property of the model – scoped_model can’t see down into the properties of objects), so as you saw earlier, the NoteModel has a color property. Initially, we want there to be no color selected, hence the call to setColor(), passing it null, which sets the color property of the model and calls notifyListeners() so the screen updates (which doesn’t really matter just yet since the entry screen isn’t shown at this point, but it’s still what happens).

Finally, we move the user to the entry screen by calling setStackIndex() and passing it a value of one, because the entry screen is the second thing on the IndexedStack (IndexedStack is zero-based obviously, and the list screen is at index zero).

After that, the body of the Scaffold is defined, and this is where we start drawing the list of notes:
body : ListView.builder(
  itemCount : notesModel.entityList.length,
  itemBuilder : (BuildContext inBuildContext, int inIndex) {
    Note note = notesModel.entityList[inIndex];
    Color color = Colors.white;
    switch (note.color) {
      case "red" : color = Colors.red; break;
      case "green" : color = Colors.green; break;
      case "blue" : color = Colors.blue; break;
      case "yellow" : color = Colors.yellow; ;
      case "grey" : color = Colors.grey; break;
      case "purple" : color = Colors.purple; break;
    }

We’re using a ListView widget here because we want a scrolling list of items. This requires us to use the builder() constructor, which takes the number of items in the list via itemCount, and that’s just the length of the entityList in the model, and then a function to actually build the widget for each item in the list. For each, we get the Note object from the list, and the first thing we need to do is deal with the color. By default, we’ll assume no color has been specified, which means the note will be white. For all the others, we set the correct color from the Colors collection (note that the value of these constants are objects, not simple strings or numbers, which is why I didn’t store those values directly, which necessitates this branching).

With the color figured out, the widget can be returned:
    return Container(
      padding : EdgeInsets.fromLTRB(20, 20, 20, 0),
      child : Slidable(
        delegate : SlidableDrawerDelegate(),
        actionExtentRatio : .25,
        secondaryActions : [
          IconSlideAction(
            caption : "Delete",
            color : Colors.red,
            icon : Icons.delete,
            onTap : () => _deleteNote(inContext, note)
          )
        ]

It all starts with a Container, and we give it a little bit of padding around on the left, top, and right. This keeps the notes away from the edges of the screen, which is just aesthetically a little more pleasing, and ensures we have some space between notes.

Next, we come to that Slidable that we saw imported earlier. This widget is just a type of container that introduces some slide functionality. In many mobile apps, when there is a list of items, you can slide them left and/or right to reveal buttons for various functions. That’s what this widget does for us. In simplest terms, you have to provide it a delegate that controls how the slide is animated (which here is just an instance of the SlidableDrawerDelegate() , also provided by this plugin). You also have to tell it how far the item can be slid, and here .25 means 25% of the way across the screen. Then, you have to specify the actions and/or secondaryActions properties. The actions property specifies what functions will be exposed with then item is slid to the right while secondaryActions are what functions will be exposed when the item is slid to the left. Here, we only have a delete action to implement, and most typically you see delete actions on the right (though there’s no rule that says it has to be that way), so secondaryActions is all I used for sliding to the left.

Each of the objects in the secondaryActions list , of which you can have as many as you want and that fit, are IconSlideAction objects, also supplied by this plugin. These objects allow you to define what caption, icon, and color you want the actions to be, as well as what to do onTap of the items. We’ll look at that _deleteNote() method soon, but there’s still a bit more widget configuration to look at first:
child : Card(
  elevation : 8, color : color,
  child : ListTile(
    title : Text("${note.title}"),
    subtitle : Text("${note.content}"),
    onTap : () async {
      notesModel.entityBeingEdited =
        await NotesDBWorker.db.get(note.id);
      notesModel.setColor(notesModel.entityBeingEdited.color);
      notesModel.setStackIndex(1);
    }
  )
)

Inside the Container and Slidable, each note is represented by a Card, which you will recall provides a box with a drop shadow on it as per Google’s Material design guidelines. These look a little bit like sticky notes to my eyes, so I felt this was a good choice here. I bump up the elevation a little bit to give them more pronounced drop shadows, and the color, of course, uses the color we determined earlier. Then, the child of the card is just a ListTile. This widget gives us a common way to lay out content with a title, which is the note title, and subtitle, which here I use to display the note’s content. The note will expand vertically as much as necessary to show all the content. The ListTile is a very common widget that is typically used as the child of a ListView, but as you can see, it doesn’t have to be a direct child of one (it doesn’t even have to be an indirect child of one technically). You’ll see more of this widget in the next chapter as well and see some other capabilities it has.

Now, when a note is tapped, we want the user to be able to edit it. This looks almost the same as creating a new note with one critical exception: the note is retrieved from the database. This is actually unnecessary since we already effectively have it in the entityList property in the model. However, for demonstration purposes, I thought it was better to show it coming from the database (there’s also something to be said for having the database be the Single Source Of Truth™ for the app, which wouldn’t be the case if we took it from entityList).

Finally, we have that _deleteNote() method that we skipped earlier:
Future _deleteNote(BuildContext inContext, Note inNote) {
  return showDialog(
    context : inContext,
    barrierDismissible : false,
    builder : (BuildContext inAlertContext) {
      return AlertDialog(
        title : Text("Delete Note"),
        content : Text(
          "Are you sure you want to delete ${inNote.title}?"
        ),
        actions : [
          FlatButton(child : Text("Cancel"),
            onPressed: () {
              Navigator.of(inAlertContext).pop();
            }
          ),
          FlatButton(child : Text("Delete"),
            onPressed : () async {
              await NotesDBWorker.db.delete(inNote.id);
              Navigator.of(inAlertContext).pop();
              Scaffold.of(inContext).showSnackBar(
                SnackBar(
                  backgroundColor : Colors.red,
                  duration : Duration(seconds : 2),
                  content : Text("Note deleted")
                )
              );
              notesModel.loadData("notes", NotesDBWorker.db);
            }
          )
        ]
      );
    }
  );
}

As with most delete operations, confirming the user’s intent is a nice thing to do, so we’ll launch a dialog to do that with showDialog(). In order to do that, we need the BuildContext in effect where the dialog is shown from, which is passed in, along with the Note instance so that we can use some of its data (the title) in the dialog. Then, inside the builder() function that showDialog() requires, we construct an AlertDialog, the content of which asks for confirmation and shows the note’s title. Then, for the actions, we build two: a cancel FlatButton, which simply pop()’s the dialog away, and the delete FlatButton. When the latter is tapped, we call the delete() method of the NotesDBWorker (it’s db property technically, which actually is the NotesDBWorker singleton instance), passing it the id of the note. Then, we pop() the dialog away, and use the showSnackBar() method of the Scaffold to show a message indicating the note was deleted. This will show for two seconds as per the Duration. Finally, the loadData() method of notesModel needs to be called so that the list will be refreshed. Recall that loadData() will re-load all the notes from the database and then call notifyListeners(), which triggers re-painting of the screen. This has to happen after removing a note; otherwise, it would be deleted from the database but not reflect that on the screen.

The Entry Screen: NotesEntry.dart

Now we come to the final part of the notes puzzle, the entry screen. It’s a simple screen, as you can see in Figure 5-4.
../images/480328_1_En_5_Chapter/480328_1_En_5_Fig4_HTML.jpg
Figure 5-4

The Notes edit screen

The title (which I’ve entered here) and the content (which I haven’t entered) are required (and you can see the error message for content where I’ve tried to save without entering anything). The color boxes are optional, but here I’ve selected red (which you can’t see on a black-and-white printed page, so just trust me, m’kay?!), indicated by it being a bit bigger. There’s a Cancel and a Save button, the former returns the user to the list screen, and the latter, of course, saves the new note (and, as I’m hoping you’ve realized by now, triggers re-painting of the list screen to show the new note).

As always, imports kick things off:
import "package:flutter/material.dart";
import "package:scoped_model/scoped_model.dart";
import "NotesDBWorker.dart";
import "NotesModel.dart" show NotesModel, notesModel;
class NotesEntry extends StatelessWidget {

Nothing new here as far as the imports go, and the widget class start is also what you’ve seen before. Keep in mind that this is still a stateless widget, despite having to deal with some state.

Now, we have two new things:
final TextEditingController _titleEditingController =
  TextEditingController();
final TextEditingController _contentEditingController =
  TextEditingController();

A TextFormField, which is what the title and content will be entered with, needs to have a TextEditingController associated with it to deal with things like its default value and the various events that can occur as the user is typing. But, we’re going to need access to these from our code too, so we create two, and they will be hooked up to the TextFormFields when we define them later but as properties of our class, are available to our application code too (as opposed to defining them inline with the TextFormFields, in which case we wouldn’t have any way to reference them, not without hackery anyway!)

But first, since we have the notion of required fields to deal with, we’re going to have a form (which isn’t required, since we could implement that logic ourselves, but as you saw in the previous two chapters, a form makes things easier), and a form requires a key:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

We don’t so much care what the key is, only that we have one, so a simple GlobalKey is created.

Next, we have some work to do when the class is created, so we have a constructor:
NotesEntry() {
  _titleEditingController.addListener(() {
    notesModel.entityBeingEdited.title =
      _titleEditingController.text;
  });
  _contentEditingController.addListener(() {
    notesModel.entityBeingEdited.content =
      _contentEditingController.text;
  });
}

See! We really did need access to those two controllers! The trick here is that any time the value of the TextFormField the controller is attached to changes, the corresponding value in entityBeingEdited needs to be updated. Calling addListener() and giving it a function to be called that does that accomplishes that goal. Without doing this, whatever the user enters on the screen wouldn’t be reflected in the model, so we’d have nothing to save later.

Now, the build() method rears its head once more:
Widget build(BuildContext inContext) {
  _titleEditingController.text =
    notesModel.entityBeingEdited.title;
  _contentEditingController.text =
    notesModel.entityBeingEdited.content;

Since this screen can effectively be used in two modes, adding and maintaining a note, we’ll want to make sure the previous values for title and content are on the screen when editing one. That’s what these statements do. When the screen is in add mode, it will just be setting null values, since that’s the default for a String, which is what the title and content properties of the Note class are. The TextFormField handles that nicely and just makes them blank as we want; otherwise, whatever the current value is when editing a Note will be shown.

Now we start to build up the top-level widget that build() returns:
return ScopedModel(
  model : notesModel,
  child : ScopedModelDescendant<NotesModel>(
    builder : (BuildContext inContext, Widget inChild,
      NotesModel inModel
    ) {
      return Scaffold(
So far, nothing new: it’s just like the start of the widget on the list screen. But after that though, we have something new:
bottomNavigationBar : Padding(
  padding :
    EdgeInsets.symmetric(vertical : 0, horizontal : 10),
  child : Row(
    children : [
      FlatButton(
        child : Text("Cancel"),
        onPressed : () {
          FocusScope.of(inContext).requestFocus(FocusNode());
          inModel.setStackIndex(0);
        }
      ),
      Spacer(),
      FlatButton(
        child : Text("Save"),
        onPressed : () { _save(inContext, notesModel); }
      )
    ]
  )
)

The bottomNavigationBar of the Scaffold widget lets us put some static content at the bottom, content that won’t scroll away even if what’s above it requires scrolling. That’s perfect for buttons, which is exactly what we have here. The first one is Cancel, which navigates the user back the list screen via a call to setStackIndex(). Just before that though, we need to hide the soft keyboard if it’s open. Otherwise, it’ll still be there obscuring the ListView on the notes list screen. The FocusScope class establishes a scope in which widgets can receive focus. Flutter keeps track via a focus tree of which widget is the user’s current focus. When you get the FocusScope of a given context via the static of() method, you can then call the requestFocus() method to send focus to a specific place, but passing a new FocusNode() instance effectively results in focus not going anywhere, which causes the OS to collapse the keyboard and mission accomplished!

The second button is Save, and that’s just a call to the _save() method, something we’ll get to when we’re done looking at the widget code. Speaking of which:
body : Form(
  key : _formKey,
  child : ListView(
    children : [
      ListTile(
        leading : Icon(Icons.title),
        title : TextFormField(
          decoration : InputDecoration(hintText : "Title"),
          controller : _titleEditingController,
          validator : (String inValue) {
            if (inValue.length == 0) {
              return "Please enter a title";
            }
            return null;
          }
        )
      )

In the previous two chapters, you saw how you can optionally have a Form widget so that you can have, among other things, validation events for your entry fields. That’s precisely what we want here, and of course, that _formKey that was created earlier is used here. The children are ListTile widgets, and here you can see one of the other things that widget provides: leading. This can be some content on the left side of the main content, and an Icon as shown here is typical. This widget also supports a trailing property to do the same on the right side, but that’s not needed here.

The title of the ListTile is the first TextFormField. It might seem weird that a property named title isn’t just a text string, but that’s the beauty of everything being a widget in Flutter: it doesn’t (usually) matter! You can put anything you want there, so long as it’s a widget (whether it’ll look good or work as you expect is another matter of course, but it will ostensibly work virtually all the time, that’s the point). This TextFormField has a decoration whose value is an InputDecoration object that. This object has many properties including labelText (text that describes the field), enabled (for visually enabling or disabling the field), suffixIcon (an icon that that appears after the editable part of the text field and after the suffix or suffixText, within the decoration’s container), just to name a few. It also has a hintText property. Setting this as shown has the effect of showing the word “Title” as slightly dimmed text whenever the field is empty of user input. It serves the same function as a label in other words. As you can see, the controller property references the TextEditingController created earlier for this field, and there is a validator defined that checks to ensure something has been entered and which returns an error string if not that will be displayed in red below the field once the form is validated, which happens in that _save() method that we’ll get into soon.

Before that though, we have another TextFormField for the content:
ListTile(
  leading : Icon(Icons.content_paste),
  title : TextFormField(
    keyboardType : TextInputType.multiline,
    maxLines : 8,
    decoration : InputDecoration(hintText : "Content"),
    controller : _contentEditingController,
    validator : (String inValue) {
      if (inValue.length == 0) {
        return "Please enter content";
      }
      return null;
    }
  )
)

It’s almost the same as that of the title field, save for one thing: the maxLines. This determines how tall the field will be. Here, there will be enough space for eight lines of text. If you know HTML, this in effect makes this TextFormField work like a <textarea>.

Now we come to the part responsible for those color blocks that the user can use to select the note’s color:
ListTile(
  leading : Icon(Icons.color_lens),
  title : Row(
    children : [

We start with another ListTile, with a leading that shows a color palette icon (Flutter calls it a color “lens,” but whatever, looks like a palette to me!). The title this time is a Row so that all the blocks can be laid out next to each other.

Because of the repetitive nature of what follows, I’m going to show the code for just one block. The other blocks are identical to this code, save for the color references of course.
GestureDetector(
  child : Container(
    decoration : ShapeDecoration(
      shape : Border.all(width : 18, color : Colors.red) +
      Border.all(width : 6,
        color : notesModel.color == "red" ?
        Colors.red : Theme.of(inContext).canvasColor
      )
    )
  ),
  onTap : () {
    notesModel.entityBeingEdited.
    color = "red";
    notesModel.setColor("red");
  }
),
Spacer(),
...repeated for each color...

Each block beings with a GestureDetector , which is a widget that gives us an element that responds to various touch events. We only care about tap events here though, hence the onTap() function provided. That’s jumping ahead though! Inside the GestureDetector is a Container, and this widget has a decoration that defines a box with a Border around all sides. The box is given a border eighteen pixels wide, which effectively results in a filled box since there is no content, so the borders in a sense “collapse” into a solid box. Then, another Border is added to that, again using the all() constructor, to put a six-pixel wide border around that box. If the color property in the model has a value of red, then the border’s color is made red. Otherwise, it’s made the same color as the background, which we can get by interrogating the Theme associated with this BuildContext. The canvasColor is the background that everything is drawn on, so that’s the element of Theme that we want. The idea here is that the box will be made thicker by virtue of that outer border only when it’s selected.

When the block is tapped, then the color is set in entityBeingEdited, and also it’s set as the color attribute of the model via the call to setColor(). That call also results in notifyListeners() being called, which causes this screen to be re-painted, which finally results in the border now being shown in the box’s color, and that’s how the box appearing bigger effect is achieved.

The final bit of code to look at in this chapter is that _save() method that you saw called earlier:
void _save(BuildContext inContext, NotesModel inModel) async {
  if (!_formKey.currentState.validate()) { return; }
  if (inModel.entityBeingEdited.id == null) {
    await NotesDBWorker.db.create(
      notesModel.entityBeingEdited
    );
  } else {
    await NotesDBWorker.db.update(
      notesModel.entityBeingEdited
    );
  }
  notesModel.loadData("notes", NotesDBWorker.db);
  inModel.setStackIndex(0);
  Scaffold.of(inContext).showSnackBar(
    SnackBar(
      backgroundColor : Colors.green,
      duration : Duration(seconds : 2),
      content : Text("Note saved")
    )
  );
}

This, obviously, is what persists the note to the database. First, the form is validated, and if it’s not valid, then the event is terminated via the early return. If it’s valid, then the first thing to determine is whether we’re creating a new note or updating one. Since there’s no flag specifically for this purpose, we must interrogate the data to tell, and that’s easy: a new note won’t yet have an id, but one being updated will. So, we branch on inModel.entityBeingUpdated.id being null or not. If it is, then a call to the create() method of the NotesDBWorker is the right thing to do. Otherwise, we’re updating, so it’s the update() method that needs to be called. In either case, the entityBeingEdited is what gets passed to it. As you saw earlier, that will be converted to a map and saved to the database.

With the note saved, we just have some final tasks to accomplish to complete the process. First, a call to loadData() needs to be made so that the list screen will be updated to reflect the new note or the changes to an existing one. Then, we navigate the user back to the entry screen with the call to setStackIndex(). Finally, we snow a SnackBar message for two seconds to indicate that the note was saved.

And that, as they say, is a wrap on notes!

Caution

Something that has burned me time and again when working with Flutter that I want to bring to your attention is the persistence, or lack thereof, of hot-reloaded changes. While hot reloading is undoubtedly a tremendous productive gain, it can sometimes cause you problems if you don’t remember that when you hot reload, the changes do not persist in your app. Meaning that if you have your app running in the emulator, you make a change and hot-reload it, you will see that change in the emulator as expected, but if you then close the app and re-start it in the emulator, your change will not be there. The change will only be present for that run of the app, or until you do a complete rebuild to effectively re-deploy the app, including the change. There have been times I’ve forgotten this, and I’m left banging my head against the desk because something that literally was just working suddenly seems not to for no apparent reason. I urge you to drill this fact into your head so you can avoid a trip to the doctor to address a frustration-induced concussion, like I probably have had a few times because I didn’t remember this!

Summary

Hooray, we did it! We’ve got FlutterBook working, if not entirely completed yet! In your first experience building a real Flutter app, you saw quite a lot including overall application architecture, project configuration including adding plugins, navigating between parts of the app, state management, data storage with SQLite, and a whole lot of widgets! It’s not a complete app yet of course, but it’s an excellent start.

In the next chapter, we’ll complete FlutterBook by adding the code for the other three entities: appointments, contacts, and tasks. In the end, you’ll have a complete, usable app and a whole lot of excellent Flutter knowledge in your head!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.68.14