Skip to content

Navigating an App and Dealing with Data

Adarsh Kumar Maurya edited this page Dec 16, 2018 · 3 revisions

Data & SQFLite in Flutter

You've probably heard that data is king, and this certainly holds true in the context of mobile developing as well. Welcome to this chapter of the tutorial, Flutter: Getting Started. In this chapter we'll wrap up most of the concepts that we've introduced in the previous chapters to create our first real world app that will be a to-do list app, at least as real world as it gets in half an hour or so. At the end of this chapter you'll be able to read and write data to your first database with Flutter. We'll cover several topics.

  • You may have realized by now that things can get rather messy with Flutter, so in this chapter we'll try to organize your code, so that you don't get lost in the UI.
  • We'll see how to use database with SQ(F)lite, a Flutter plugin that will allow you to use SQLite with Flutter. If you haven't used SQLite yet, don't worry. We'll have a look at that as well. I'll assume you have some basic experience in dealing with databases, but if you've come so far I'm sure you'll be able to follow along anyway.
  • We'll see how to use asynchronous programming in Flutter.Among other things, we'll use future, async, await, and then.
  • We'll also introduce singletons, and
  • We'll have a look at two very useful plugins, Path_provider, that will help us find common paths for both iOS and Android, and Intl that we'll use to format dates.

Let's have a look at the app you'll build during this chapter. There are two pages or screens. The first one contains a list of to-do's. They have a priority, a title, and a date. This list can be scrolled, so even if there are more items in the list than fit the screen, you can still use the app.

Depending on the priority, each circle of the to-do's gets a color. For the high priority to-do's it's red. The medium priority is orange, and the low priority is green. From that you can click on the add floating action button to insert a new to-do or you can click on one of the items on the list and get to the to-do detail, the second screen of our app. Here you can view a single item that also has a description that wasn't visible in the list in the first page, and you can add it or delete items.

There is a menu button at the top of the screen that lets you choose the action you want to perform.Everything we do within the app will be written to a SQLite database.

SQLite So let's briefly talk about SQLite. According to the official site, sqlite.org, SQLite is an in-process library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.

Let's see what that means to us as mobile developers. First of all, SQLite is an SQL database engine. We can use the SQL language to build queries, so if you are already familiar with SQL you can definitely leverage your knowledge. - Self-contained means that SQLite requires very little support from external libraries, making it the perfect choice for a light weight, platform independent app.

  • Serverless means that SQLite reads and writes directly from the database files on disk, so you don't have to set up any client server connection in order to use it.
  • Zero-configuration means that there is no installation and no set up, and I might add, no headaches.
  • Transactional is very important because all changes in a transaction occur completely or not at all. If your program crashes in the middle of a transaction no data will be returned to your database, making it secure and reliable. This also explains why SQLite is now one of the most widespread databases in the world.

Bottom line, SQLite is a very good choice for persisting data in mobile development. How can we use SQLite in our app?

Through the SQFLite plugin, which we'll have to include in our project. The database that we will create has just one table, the Todos table. It will have an id, a title, a description, a priority, and a date.

The schema is very simple, but it will allow us to experiment many of the features that are needed in order to be the Flutter app with SQLite. Okay, let's get into the action.

Creating a Model Class

In this lesson we'll create the to-do's project and the to-do model that we'll use throughout the app, so from Visual Studio Code let's create a new project and call it todo_app. We'll now create three folders. The first one will be screens. This will contain the UI files or pages of our app, one for the list of to-do's and one for the detail. The second folder is util, and the last one is model. In this last folder let's create a new file that we can call todo.dart. This file will contain our model class that will have all the properties, methods, and constructors for the to-do objects. We don't need to import anything here.It's just a plain class. We'll call it Todo. We'll create the five private properties that will make up our model, an int called _id, and remember the underscore is because we want to mark this as private or not accessible from outside the class. A String _title, another String _description, and then a String called _date, and an integer for the _priority.

Next, let's create a couple of constructors. Actually, in Flutter there's a shortcut to create a constructor that uses the this keyword. If you put this and the name of a property the value you pass as a parameter will be linked directly to the property you specified. When you have an optional parameter you enclose it in square brackets. This is the constructor for when you are creating a new Todo, and the database hasn't assigned an id yet.

Let's create another constructor for when you have the id, for example, when you're editing the Todo. As you can see, as soon as we start writing the constructor we can see that there's something wrong. This is because you can only have one unnamed constructor in a class. The solution to this issue is using named constructors.

Let's say, for example, that we want to name the second constructor withId, and here we'd only add to the arguments of the first contractor, the id property. It works exactly as the unnamed one. Now when we need to set a Todo with an id we'll call the named constructor. Okay, let's have a look at getters. They wouldn't be strictly necessary for this app, but I think they are one of those tools you really need to know, so let's begin with the id. You just need to specify the type and return the value you want to return.

In most cases, when you do not need to manipulate the value before returning it you can use the fat arrow operator, like this. Okay, let's do the same for the title. Description, priority, and date. As there are getters, there are also setters. We won't need a setter for the id, as this will never change after the creation of a Todo, but we want to be able to change the other values. Let's begin with a title. The only thing we will check is whether the length of the string is less than 255.

In a real world app you would probably check several other constraints depending on new rules. Let's do the same for the description, checking again the length. For the priority we want an integer whose value is between 0 excluded and 3 included. In the setters you could also through an error when the values are not as what you expect, but we don't need that for this particular app. Let's also write the setter for the date.

class Todo {
  int _id;
  String _title;
  String _description;
  String _date;
  int _priority;

  Todo(this._title, this._priority, this._date, [this._description]);
  Todo.withId(this._id, this._title, this._priority, this._date,
      [this._description]);
  int get id => _id;
  String get title => _title;
  String get description => _description;
  int get priority => _priority;
  String get date => _date;

  set title(String newTitle) {
    if (newTitle.length <= 255) {
      _title = newTitle;
    }
  }

  set priority(int newPriority) {
    if (newPriority > 0 && newPriority <= 3) {
      _priority = newPriority;
    }
  }

  set date(String newDate) {
    _date = newDate;
  }

  Map<String, dynamic> toMap() {
    var map = Map<String, dynamic>();
    map["title"] = _title;
    map["description"] = _description;
    map["priority"] = _priority;
    map["date"] = _date;
    if (_id != null) {
      map["id"] = _id;
    }
    return map;
  }

  Todo.fromObject(dynamic o){
    this._id = o["id"];
    this._title = o["title"];
    this._description=o["description"];
    this._priority=o["priority"];
    this._date=o["date"];
  }
}

Here we'll just set the date property without checking anything. There are two more methods that will be useful for this class. We'll call the first one toMap(). It will return a map of String and dynamic. We have already used maps in a previous chapter, but as a quick recall, maps are collections of key value pairs from which you retrieve a value using its associated key. Maps and their keys and values can be iterated, so we can create a map that has a string as a key, and a dynamic type as value. Basically what we want to do here is transforming our Todo into a map. This will come handy when we use some helper methods for SQLite, so let's create a variable called map that will be of the same type of the map we'll be returning at the end of the function, String keys dynamic values. Our map will have a title key that will take the title of the Todo. We'll do the same for the description, the priority, and the date. For the id we'll do it only if the id is not null. Then we'll return the map. The last method of this class is actually a named constructor, and it will basically do the opposite of the map method. We want to take in any object and transform it into a Todo. So we'll call it fromObject, and it will take a dynamic object, o. The idea of our Todo that we can select with a this property will be the id of the object. Again, let's repeat the process for the title, description, priority, and date. Great. We have completed our model class. Next, we'll implement the database helper class.

Using a Singleton

In this lesson we'll create the code to interact with the database. As SQFLite is a Flutter plugin, before we can use it we'll have to add the dependency in the pubspec.yaml file.

We will also add a couple of other dependencies for the Todo project. The path_provider finds commonly used locations on the file system. We'll use it to find the database location, which is different on iOS and Android devices. Using this package, we won't have to worry about the physical path of the two operating systems. In order to format the date, we will use the intl package, which is a package you can use for internationalization. In our app we'll use it to format dates.

How can you interact with SQLite in Flutter? You can actually use two different approaches. One is using the SQL language directly.

db.rawQuery("SELECT" * FROM yourTable);

db.rawInsert('INSERT INTO yourTable(name, num) VALUES("some name", 1234)');

db.rawUpdate('UPDATE yourTable SET name = ?, WHERE name =?', ["new name", "old name"]);

db.rawDelete('DELETE FROM yourTable WHERE id =1');

You can use rawQuery, rawInsert, rawUpdate, rawDelete passing an SQL comment. If you know a little bit of SQL language this has a very quick learning curve.

The second approach is using the SQFLite helpers.

db.update('yourTable',
          yourObbject.toMap(),
          where: "$colId = ?",
          whereArgs: [yourObject.id]);

On the screen you see an update command example. You pass the name of the table you are editing, the full record, in the form of a map, and you set the where property specifying the field or fields and the filter values in the whereArgs.

So which of these two approaches is better? I would say the one you prefer. In our app we'll use both methods, so you'll choose your favorite one for your next app.

Both methods are asynchronous.

We'll see how to deal with that shortly. Back into our project, let's first edit the pubspec.yaml, so that we can add the dependencies we need.

The first one will be sqflite in any version. Then we'll add the path_provider; any version will do as well. Finally, let's add the intl package. For the version we'll use the caret syntax. This means that our app requires a version that is equal or above version 0.15.7 up to a version of 1.0.0 not included.

Now let's create a new file in the util folder. We'll call this dbhelper.dart. We'll need to make a few imports. Obviously, we need the sqflite package, as this is the guest of honor in this class. The dart.async will also be very important, as we need to get the directory of our database, we'll need the dart IO class and the path provider. As all our app is about to-do's, we'll also need the todo class that we have created in the previous lesson.

Okay, once our imports are complete let's actually create the class that we'll call DbHelper. Here we'll create a few constants that will help us for our queries. The name of the table will be todo. Our columns or fields will be strings. The first one will be the id that we can call colId, and give it a value of id. We'll repeat the process for all the columns, so colTitle with a value of title, colDescription with a value of description, colPriority, priority, and colDate with a value of date. Now the thing is we do not want this class to be instantiated more than once. This class contains methods to retrieve the database and make reads and writes over it, so once you call DbHelper you only need it once for all your app, right?

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:todo_app/model/todo.dart';

class DbHelper {
  String tblTodo = "todo";
  String colId = "id";
  String colTitle = "title";
  String colDescription = "description";
  String colPriority = "priority";
  String colDate = "date";
  
}

Well, what we need is a singleton. A singleton restricts the instantiation of a class to one object only. This is how you do it in Flutter or actually, how you do it in dart.

In dart there's an interesting language feature called factory constructor that allows you to override the default behavior when you instantiate an object.

Instead of always creating a new instance the factory constructor is only required to return one instance of the class.

// Create a private instance of the class
static final DbbHelper _dbhelper = new DbHelper._internal();

// Create an empty private named constructor
DbHelper._internal();

// Use the factory to always return the same instance
factory DbHelper(){ return _dbhelper;}

Here's how it works. First, you create a private instance of the class within the class itself using a named private constructor. In the unnamed constructor, which is public, you just return the instance of the class that was created previously.

This means that when you call the constructor to your DbHelper throughout the lifecycle of the app you'll be actually calling always the same object.

Note the factory keyword in the public constructor. Use the factory keyword when implementing a constructor that doesn't always create a new instance of the class.

Okay, let's do that in our DbHelper class. Let's create a static final DbHelper that we will call _dbHelper that's marked as private. This will be a DbHelper._internal, which is a named constructor that we are going to create right now. Let's create the DbHelper._internal named constructor, which will be empty. Then in the unnamed constructor we'll return the DbHelper object we have just created.

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:todo_app/model/todo.dart';

class DbHelper {
  static final DbHelper _dbbHelper = DbHelper._internal();
  String tblTodo = "todo";
  String colId = "id";
  String colTitle = "title";
  String colDescription = "description";
  String colPriority = "priority";
  String colDate = "date";

  DbHelper._internal();

  factory DbHelper() {
    return _dbbHelper;
  }
}

Okay, let's briefly talk about asynchronous programming in Flutter. When you start an application a single thread or path of execution is automatically created. This is the main thread. The main thread is also called the UI thread because its responsibility includes, among others, drawing all the widgets in your screen and responding to user input.

So, for example, when you touch the button on a screen it's the main thread that needs to deal with the appropriate response. Raising the appropriate events, animating the button, and executing any other code that you have added to your app. If you run long operations like network access of database queries on the main thread the application may get unresponsive, and your user will have the feeling your application is slow. Even worse, if your app does not respond to a user input like a screen touch for more than 5 seconds the operating system may automatically give the user an application not responding message.

In order to run long operations without making your app unresponsive, you can delegate some tasks to another thread, so that two parts of our application can keep working in parallel without slowing each other. This is what we call multi-threading.

What happens is that from the main thread you call a secondary thread. Then the main thread and the secondary thread work in parallel, so the main thread is still responsive to user input, even when the secondary thread is performing long operations.

When the secondary thread completes it returns its result to the main thread that can react accordingly. In Flutter you apply multi-threading with futures, sync, and await.

A Future is an object that will get a value sometime in the future, so if you create a method that returns a Future, when you call it you immediately receive a Future object. Only when the code inside the Future completes its execution the then method is called with the result. Another way of dealing with asynchronous programming in Flutter is by using the async and await keywords.

When you mark a method as async this will use a secondary thread for its execution. Inside an async method you can use the await keyword to call long performing tasks. If you return something from a method marked as async this must be a future, but if an async method doesn't explicitly return a value back in the example on the screen, it returns a future wrapped around a null value. In our app we'll use futures, async, and await.

void doSomething() async{
    result = await getTodos();
}

Okay, we are now ready to write the method that will actually create or open the database. Let's call it initializeDb. This will return a Future of type Database.

A Future is used to represent a potential value or error that will be available at some time in the future. It's from the async package that we imported above. The method will be asynchronous.

Inside the method we'll create a directory, and this is from the IO package that we can call dir. This will await the getApplicationDocumentsDirectory method. This method is from the path provider package. This will simply return the directory for the documents of our app. The paths are different in iOS and Android, but this will work in any case.

As this is asynchronous, we'll use the await keyword, so that we can be sure that we'll get the path before the next instruction. That will be a String declaration of a variable called path that will contain the directory plus the name of the database. Let's call it todos.db.

Future<Database> initializeDb() async{
    Directory dir = await getApplicationDocumentsDirectory();
    String path = dir.path + "todos.db";
    var dbTodos = await openDatabase(path, version: 1, onCreate: _createDb);
    return dbTodos;
}

Finally, let's create a dbTodos variable that will await the openDatabase method, passing the path, the version, and what to do if the database does not exist. In the onCreate parameter we will call the _createDb method that we will create shortly. Finally, we'll return the future database.

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:todo_app/model/todo.dart';

class DbHelper {
  static final DbHelper _dbbHelper = DbHelper._internal();
  String tblTodo = "todo";
  String colId = "id";
  String colTitle = "title";
  String colDescription = "description";
  String colPriority = "priority";
  String colDate = "date";

  DbHelper._internal();

  factory DbHelper() {
    return _dbbHelper;
  }

  static Database _db;

  Future<Database> get db async {
    if (_db == null) {
      _db = await initializeDb();
    }
    return _db;
  }

  Future<Database> initializeDb() async {
    Directory dir = await getApplicationDocumentsDirectory();
    String path = dir.path + "todos.db";
    var dbTodos = await openDatabase(path, version: 1, onCreate: _createDb);
    return dbTodos;
  }

  void _createDb(Database db, int newVersion) async {
    await db.execute(
        "CREATE TABLE $tblTodo($colId INETEGER PRIMARY KEY, $colTitle TEXT," +
            "$colDescription TEXT, $colPriority INTEGER, $colDate TEXT)");
  }
}

Let's write the _createDb method. This will return void and take a Database db, and an integer newVersion, and will be asynchronous. This will just launch an SQL query to create the database. So let's await a db.execute method, passing the string, CREATE TABLE, the name of the table, the dollar will concatenate the variable without having to explicitly closing the string. Then we'll specify the columns. The id will be an INTEGER and PRIMARY KEY. Because it's called id, SQLite will also imply that this is an autonumber. The title will be of type TEXT, as well as the description. The priority will be an INTEGER, and the date a TEXT. Next, let's create the variable that will contain the database throughout the class. It will be a static Database called _db. We will also create a getter that will check if db is null and if it is, it will call the initializeDb method. In any case, it will return Db.

Developing a Database Helper Class

Right, finally we are ready to write the query methods. Let's begin with insert. This will return a Future of an int. We'll call it insertTodo, and it will take a todo as a parameter. We'll get the db, and put it in a local database variable called db. For the result we call the insert helper method. This requires the name of the table and the map of the record we want to insert. In the map the keys of the values must be the name of the fields and the values will be inserted as values. Okay, this was easy, right? Let's return result. By the way, if the integer is 0 something went wrong, otherwise, the result will contain the id of the record that was inserted. Let's get to the select. We want to create the method that will return all the to-do's. It will be a Future of type List called getTodos, and like before, it will be async.Let's get the database. This time the result will await a rawQuery method in which we'll just select all the elements from the Todos table and order them by priority in ascending order. Let's return result. As we are reading values from the database, let's also get the number of records in our table. This will return a Future integer. We'll call it getCount, and it will be async as well. Let's get the database. For the result we will call the Sqflite.firstIntValue method, passing the call to a rawQuery, which will be a select count from the Todos table. Let's return result. Okay, now we just need to write the update and delete methods. The update is very similar to the insert. It will return a Future int. We call it updateTodo, and it will take a todo object as a parameter. of course, it will be async. Let's get the database. The result will await a call to the db.update method, passing the name of the table and the map of the object we want to update. As this is an update, we also need to specify which record we want to update, so there's a where property, and we'll pass the id column, and for the whereArgs we will pass the todo.id. This is an array, as it may contain multiple arguments. Then we'll return the result. The delete is even easier. Let's call it deleteTodo. It will take an integer. After getting our database let's call the rawDelete method, passing a DELETE SQL statement. We'll delete from the Todos table where the id column equals id, and then we'll return the result. Well done. The DbHelper is ready.

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:todo_app/model/todo.dart';

class DbHelper {
  static final DbHelper _dbbHelper = DbHelper._internal();
  String tblTodo = "todo";
  String colId = "id";
  String colTitle = "title";
  String colDescription = "description";
  String colPriority = "priority";
  String colDate = "date";

  DbHelper._internal();

  factory DbHelper() {
    return _dbbHelper;
  }

  static Database _db;

  Future<Database> get db async {
    if (_db == null) {
      _db = await initializeDb();
    }
    return _db;
  }

  Future<Database> initializeDb() async {
    Directory dir = await getApplicationDocumentsDirectory();
    String path = dir.path + "todos.db";
    var dbTodos = await openDatabase(path, version: 1, onCreate: _createDb);
    return dbTodos;
  }

  void _createDb(Database db, int newVersion) async {
    await db.execute(
        "CREATE TABLE $tblTodo($colId INETEGER PRIMARY KEY, $colTitle TEXT," +
            "$colDescription TEXT, $colPriority INTEGER, $colDate TEXT)");
  }

  Future<int> insertTodo(Todo todo) async {
    Database db = await this.db;
    var result = await db.insert(tblTodo, todo.toMap());
    return result;
  }

  Future<List> getTodos() async {
    Database db = await this.db;
    var result =
        await db.rawQuery("SELECT * FROM $tblTodo order bt $colPriority ASC");
    return result;
  }

  Future<int> getCount() async {
    Database db = await this.db;
    var result = Sqflite.firstIntValue(
        await db.rawQuery("SELECT COUNT (*) FROM $tblTodo"));
    return result;
  }

  Future<int> updateTodo(Todo todo) async {
    var db = await this.db;
    var result = await db
        .update(tblTodo, todo.toMap(), where: "$colId=?", whereArgs: [todo.id]);
    return result;
  }

  Future<int> deleteTodo(int id) async {
    int result;
    var db = await this.db;
    result = await db.rawDelete("DELETE FROM $tblTodo WHERE $colId=$id");
    return result;
  }
}

It would be nice if you could try it out, right? Why not. In the main.dart let's import our DbHelper class and the todo class. Next, let's instantiate the DbHelper. First, we want to read if there are to-do's in the database. The first time everything will be empty, but the second time it will contain the todo we will insert shortly. Now let's call the initializeDb method, and when it returns a success this is what the then method does. It waits for the success of the call. We call the getTodos method to retrieve the todos.Then we'll read the result and pass it to a todos variable. Let's declare it above. Todos will be a new list of todo. Let's get today's date with the DateTime.now function. We'll format this better later, but it's okay for now. Let's create a new todo called Todo that will contain Buy Apples as title, 1 as priority, today.toString as date, and make sure they are good as description. Then let's call the insertTodo method from the helper passing the todo.

import 'package:flutter/material.dart';
import 'package:todo_app/util/dbhelper.dart';
import 'package:todo_app/model/todo.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    List<Todo> todos = List<Todo>();
    DbHelper helper = DbHelper();
    helper
        .initializeDb()
        .then((result) => helper.getTodos().then((result) => todos = result));
    DateTime today = DateTime.now();
    Todo todo =
        Todo("Buy Melon", 3, today.toString(), "And make sure they are good");
    var result = helper.insertTodo(todo);

    return MaterialApp(...);
...
}

Okay, let's put a breakpoint on the insertTodo. The first time this executes the todos are empty, but let's see if the insert works. Let's step into the insertTodo method, then in the DbHelper class let's step over a few times until we reach the return statement, and if you get the mouse pointer over the result you can see that the result is one, and that means that the insert statement was executed correctly. Let's get to the main.dart file. This time we will buy oranges with a priority of two. Let's run the app again, and once our breakpoint is reached let's tap into the insertTodo instruction, and this time the result is two. Remember, this number is the id that SQLite is giving to the todo we are inserting. Let's repeat this for a third and last time with melon and a priority of three. We can never have too much. After all, fruit is good for your health. This time the result is three. So now we know our table contains three records. Great. We are now ready to show something to our user, and this is what we'll do next.

Building a ListView

ListView
A scrollable list of widgets arranged linearly.
ListView is the most commonly used scrolling widget.

Now that the plumbing of our app is ready we need to create the user interface. We'll begin with the first page of our app that chose the list of to-do's. In order to-do that we'll use a ListView. This widget solves many of the headaches we've experienced in the screens we've built-in the previous chapters, mainly for one reason, the ListView is scrollable, so even if there are more items than can fit in the screen of your device, there will be no errors.

Actually, according to the official guide, the ListView is the most commonly used scrolling widget. We will use the ListView in both screens in our app.Another new control we'll be using is the FloatingActionButton or FAB.

FloatingActionButton or FAB
A floating action button is a circular icon button that hovers 
over content to promote a primary action in the application.

This button will stay visible at any time, even when we scroll the items on the ListView. We'll use it to add a new todo to the database. Okay, let's get back to the action.

We create a new file in the screens folder. Let's call it todolist.dart. We'll import material.dart, as we usually do for the user interface, then we'll import todo.dart from the model folder, and the dbhelper. Let's create the TodoList class that extends a stateful widget, and through the Visual Studio Code's quick fix let's also override the createState method that we'll call a TodoListState. Let's also create the TodoListState that extends State, and make Visual Studio Code create the build method. Here we want to use our DbHelper to retrieve the data, so let's create a private variable called helper. Then let's create a list of to-do's that we will call todos, and an integer called count that will be 0 at the beginning. This will contain the number of records in the Todo table. Now we need a method to retrieve the data from the database. Let's call it getData. It will return void, but use the setState method to update the todos and the count properties of our class, so let's declare a final variable called dbFuture that will call the helper.initializeDb() method. As you may recall, this is the method that opens the database or creates it if it doesn't exist. This returns a future of type database, not the database itself, so in order to use it we will call the then method. The then method will be executed only when the database has been successfully opened. The then will inject a result, which is the database in our case. Inside the then method we'll declare another future calling the helper.getTodos method. This is the method that retrieves all the records from the Todos table, and it's a future as well. So when data is retrieved we will create a temporary list of to-do's called todoList. We will give count the length of the result. Then we create a for loop for each item of the list. Then we'll add to the todoList a new item that will be a Todo.fromObject() of the item at position i in the result, so this will transform a genetic object into a todo. To make sure everything is correctly loading let's call the debugPrint method, so that we can see the title of the current todo in the debug window.Then we'll call the setState() method, and here we'll assign the todoList to the todos property, and count to count.

import 'package:flutter/material.dart';
import 'package:todo_app/model/todo.dart';
import 'package:todo_app/util/dbhelper.dart';

class TodoList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => TodoListState();
}

class TodoListState extends State {
  DbHelper helper = DbHelper();
  List<Todo> todos;
  int count = 0;
  @override
  Widget build(BuildContext context) {
    if (todos == null) {
      todos = List<Todo>();
      getData();
    }
    return Scaffold(
      body: todoListItems(),
      floatingActionButton: FloatingActionButton(
        onPressed: null,
        tooltip: "Add new Todo",
        child: new Icon(Icons.add),
      ),
    );
  }

  ListView todoListItems() {
    return ListView.builder(
      itemCount: count,
      itemBuilder: (BuildContext context, int position) {
        return Card(
          color: Colors.white,
          elevation: 2.0,
          child: ListTile(
              leading: CircleAvatar(
                  backgroundColor: Colors.red,
                  child: Text(this.todos[position].id.toString())),
              title: Text(this.todos[position].title),
              subtitle: Text(this.todos[position].date),
              onTap: () {
                debugPrint("Tapped on " + this.todos[position].id.toString());
              }),
        );
      },
    );
  }

  void getData() {
    final dbFuture = helper.initializeDb();
    dbFuture.then((result) {
      final todosFuture = helper.getTodos();
      todosFuture.then((result) {
        List<Todo> todoList = List<Todo>();
        count = result.length;
        for (int i = 0; i < count; i++) {
          todoList.add(Todo.fromObject(result[i]));
          debugPrint(todoList[i].title);
        }
        setState(() {
          todos = todoList;
          count = count;
        });
      });
      debugPrint("Items " + count.toString());
    });
  }
}

In order to make sure this is working, we'll also call debugPrint, writing Items and count.toString. So in the build method if the todo object is null, and this happens when the screen is loaded the first time, let's say the todos is a new instance of a List of Todo, and then call getData to retrieve our data. Now for the UI we will return a Scaffold that, for the body, will have a widget called todoListItems that we will create in a minute, but it will also have a FloatingActionButton that will be an instance of a FloatingActionButton. When we press the FloatingActionButton we will navigate to the second screen of the app to add a new todo, but for now let's just write null for the onPressed property. Let's add a tooltip, add new Todo, and as for the child property, let's choose the icon add. Let's create the todoListItems method. This will return a ListView. Let's close a few of the methods, so that we can see everything better. The ListView is constructed with a builder method. For the itemCount property we'll set it to count. The itemBuilder property takes a function that will be iterated for each item in the list. It takes the context, and then integer that we will call position. So for each item in the list will return a Card. A Card is a sheet of material with slightly rounded corners and a shadow. Let's add the color property to white. Let's also add an elevation of 2.0. The child will be a ListTile, which is a row that contains some text and deleting or trailing icon. The leading property will allow us to put a CircleAvatar. Temporarily, we'll write vad in the circle. Later on we'll put the priority. Let's give it a background color of red for now. We want to write the id, so in the child property we'llput a Text containing the id of the current todo transformed into a string. Our card will also have a title, which will be the title of the todo at the current position. For the subtitle we'll write the date of the current item. There's also a tap event that is triggered when the user taps on one of the cards. For now we will only write the id of the item in the card, so that we can make sure we get the right element.

Before we try these out we still need to fix our main.dart file. We still have the original code we got when we created the new project. First, let's import the todolist.dart from the screens folder. Then let's remove the code we used to test the app in one of the previous lessons. Let's also remove the comments in the ThemeData object. We'll change the title of the app to Todos, and the primarySwatch property of the theme to deepOrange. We'll also set the home page title to Todos. Let's remove the comments in the home page, the whole increment counter method, and everything in the build method of the state.

import 'package:flutter/material.dart';
import 'package:todo_app/util/dbhelper.dart';
import 'package:todo_app/model/todo.dart';
import 'package:todo_app/screens/todolist.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todos',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      home: MyHomePage(title: 'Todos'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: TodoList(),
    );
  }
}

In the build method let's return a Scaffold with an appBar whose title will be the widget.title, and the body will be the TodoList. Okay, let's try this out. Great isn't it. Finally, you can see that after you set up your code in the right way building the UI with Flutter is a breeze. Okay, let's also tap on a few items here and see the result in the debug window, and depending on which card we tap we get the right id. Wonderful.

Let's set the text of the CircleAvatar to priority instead of id, and this is immediately visible into our UI. Now depending on the priority of the to-do, it would be nice if the user could see a different color. To-do's with a high priority should be red, those with a medium priority, orange, and those with a low priority, green. Let's see how to-do that. We can just create a method that will return a color that we can call getColor that takes an integer that will contain the priority. Now let's set a switch checking the priority. If the priority is one, or high priority, we'll return red, if it's two, medium priority, we'll return orange, and if it's three, low priority, we'll return green. In the default let's just return green.

class TodoList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => TodoListState();
}
...
 Color getColor(int priority) {
    switch (priority) {
      case 1:
        return Colors.red;
        break;
      case 2:
        return Colors.orange;
        break;
      case 3:
        return Colors.green;
        break;
      default:
        return Colors.green;
    }
  }

Okay, now in the ListView instead of always returning red let's call the getColor method, passing the priority of the current title, and let's see how it looks. Much better.

import 'package:flutter/material.dart';
import 'package:todo_app/screens/todolist.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todos',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      home: MyHomePage(title: 'Todos'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: TodoList(),
    );
  }
}

Now in order to finish this screen we need to navigate to the detail view of our widget, but we need to create the detail view first, and this is what we'll do in the next lesson.

Building a Detail View

In this lesson we'll create the view that contains the detail of a todo. We'll get there in two different ways. The first one is when a user creates a new todo, pressing the floating action button on the Todolist screen, and the second is when the user clicks on one of the items of the list and gets there to view, edit, or delete a to-do.

In any case, what this screen needs is a todo, so we'll build it requiring a todo in the constructor, full or empty. Let's create a new file in the screens folder. We'll call it tododetail.dart. As usual, we'll begin with a few imports; the material.dart package, our todo.dart, the DbHelper, and the internationalization package that will allow us to write the date of our to-do in the format we choose. Let's create the TodoDetail class. This will extend a StatefulWidget. This will have a Todo property that we'll call todo. In the constructor it will require a todo. Then let's override the createState method, returning TodoDetailState and passing a todo. of course, we need to create the state. Let's call it TodoDetailState. It extends State. Let's create a todo property. Next, we'll create an array called _priorities that will contain the possible values of the priority; High, Medium, or Low. Let's create a private string called _priority, and let's set it to Low at the beginning. Then let's set twocontrollers, one called titleController, and the other one descriptionController. Good. Let's write the build method. It will be relatively simple. Only two text fields; one for the title, one for the description,and a button list for the priority. If the todo contains data we want to show it in the TextField. Let's set titleController.text to the title of the todo, and do the same with the descriptionController that will contain the description of the todo. We'll also use the same TextStyle that we used in the previous chapter, the textTheme.title. Then we'll return our user interface. It will be a Scaffold, but will have an app bar. Because we want to be very explicit with our navigation, we don't want to see the back button on the appBar, so we'll set the automaticallyImplyLeading to false. Let's set the title of the appBar to the title of the todo. Let's create the body. It will contain a column with three children. The first one will be a TextField that, as a controller, will have the titleController, as style with have the textStyle we have defined above, and then it will have a decoration that will be an instance ofInputDecoration with a labelText, Title, and a labelStyle, the same textStyle, and a border. It will be an OutlineInputBorder with a border radius that will be a BorderRadius.circular with a value of 5.0. Let's copy this text field and paste it below. We will change the second controller to descriptionController,as the second text field is for the description of our todo. Let's also change the label text to Description.

import 'package:flutter/material.dart';
import 'package:todo_app/model/todo.dart';
import 'package:todo_app/util/dbhelper.dart';
import 'package:intl/intl.dart';

class TodoDetail extends StatefulWidget {
  final Todo todo;

  TodoDetail(this.todo);

  @override
  State<StatefulWidget> createState() => TodoDetailState(todo);
}

class TodoDetailState extends State {
  Todo todo;
  final _priorities = ["High", "Medium", "Low"];
  String _priority = "Low";
  TextEditingController titleController = TextEditingController();
  TextEditingController descriptionController = TextEditingController();
  TodoDetailState(Todo todo) {
    this.todo = todo;
  }


  @override
  Widget build(BuildContext context) {
    titleController.text = todo.title;
    descriptionController.text = todo.description;
    TextStyle textStyle = Theme.of(context).textTheme.title;
    return Scaffold(
        appBar: AppBar(
          automaticallyImplyLeading: false,
          title: Text(todo.title),
        ),
        body: Column(
          children: <Widget>[
            TextField(
              controller: titleController,
              style: textStyle,
              decoration: InputDecoration(
                  labelText: "Title",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(5.0),
                  )),
            ),
            TextField(
              controller: descriptionController,
              style: textStyle,
              decoration: InputDecoration(
                  labelText: "Description",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(5.0),
                  )),
            ),
            DropdownButton<String>(
              items: _priorities.map((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value),
                );
              }).toList(),
              style: textStyle,
              value: "Low",
              onChanged: null,
            )
          ],
        ));
  }
}

Finally, let's create a DropdownButton for the priority. This should contain the strings for the three priority levels we want to use. We define them in the priorities value. In the items property of the dropdown button let's map the priority array. Inside the map method we'll return a dropdown menu item widget of type String. Inside, we'll set the value of the item to the value of the map, and let's say that the child of the dropdown item will be a text containing value. We'll call the toList method to complete the function. For the style we'll use the same one for this guy as well. For the value we'll just write Low for now. We'll change that in a minute. In the onChanged method we'll need to update the priority in the todo, but we'll just set it to null right now. At this moment, we only want to create the user interface. Next, let's talk a little bit about navigation.

Navigation in Flutter

Navigation in Flutter is based on a stack. A stack contains the pages or screens that an app has used from the beginning. When you want to change the page in Flutter you use an object called navigator, and there you have two methods that deal with a stack.

The push method puts a new page at the top of the stack. The pop method removes the page from the screen, so that the previous page on your stack gets visible again. When you use the push method you need to specify your root, which is the page you want to load.

Flutter has a material page root class that lets you choose the name of the page you want to push. Both push and pop require the context in order to work.

Let's see a simple example of this in our app. In order to be able to navigate to the details screen in our todolist.dart file we need to import the tododetail.dart. There are two moments we need to call the TodoDetails screen from the list, when the user taps on one of the items of the list, and when they press on the Add button. As we will actually have to repeat the same steps twice, it makes sense to create a method.Let's call it navigateToDetail. This will return void, but it will take a todo as a parameter. We'll declare a Boolean called resolved. This will await the Navigator.push method to get to the TodoDetails screen, passing the current context, and the MaterialPageRoute whose builder we'll call the ToDoDetail class, passing the todo that was passed.

import 'package:flutter/material.dart';
import 'package:todo_app/model/todo.dart';
import 'package:todo_app/util/dbhelper.dart';
import 'package:todo_app/screens/tododetail.dart';

class TodoList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => TodoListState(todo);
}

class TodoListState extends State {
  DbHelper helper = DbHelper();
  List<Todo> todos;
  int count = 0;
  TodoDetailState(Todo todo) {
    this.todo = todo;
  }


  @override
  Widget build(BuildContext context) {
    if (todos == null) {
      todos = List<Todo>();
      getData();
    }
    return Scaffold(
      body: todoListItems(),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          navigateToDetail(Todo('', 3, ''));
        },
        tooltip: "Add new Todo",
        child: new Icon(Icons.add),
      ),
    );
  }

  ListView todoListItems() {
    return ListView.builder(
      itemCount: count,
      itemBuilder: (BuildContext context, int position) {
        return Card(
          color: Colors.white,
          elevation: 2.0,
          child: ListTile(
              leading: CircleAvatar(
                  backgroundColor: getColor(this.todos[position].priority),
                  child: Text(this.todos[position].id.toString())),
              title: Text(this.todos[position].title),
              subtitle: Text(this.todos[position].date),
              onTap: () {
                debugPrint("Tapped on " + this.todos[position].id.toString());
                navigateToDetail(this.todos[position]);
              }),
        );
      },
    );
  }

  void getData() {
    final dbFuture = helper.initializeDb();
    dbFuture.then((result) {
      final todosFuture = helper.getTodos();
      todosFuture.then((result) {
        List<Todo> todoList = List<Todo>();
        count = result.length;
        for (int i = 0; i < count; i++) {
          todoList.add(Todo.fromObject(result[i]));
          debugPrint(todoList[i].title);
        }
        setState(() {
          todos = todoList;
          count = count;
        });
      });
      debugPrint("Items " + count.toString());
    });
  }

  Color getColor(int priority) {
    switch (priority) {
      case 1:
        return Colors.red;
        break;
      case 2:
        return Colors.orange;
        break;
      case 3:
        return Colors.green;
        break;
      default:
        return Colors.green;
    }
  }

  void navigateToDetail(Todo todo) async {
    bool result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => TodoDetail(todo)),
    );
  }
}

So let's call this method when the user taps on a todo first. In the onTap property we will call the navigateToDetail method passing the todo at the position that was tapped. Now let's also deal with the onPressed property of the FloatingActionButton. Here we'll call the navigateToDetail passing a new Todo with an empty title, a low priority, and an empty date.

We're done. Let's try this out. Well, the good news is that the three widgets we wanted to show are visible, and that's where the good news end. Okay, I guess it could be much better, but nothing a few lines of code won't be able to solve. The most obvious problem is that we have a spacing issue on the screen.

So let's enclose everything in a padding that will have an EdgeInsets.only top value of 35, a left of 10, and a right of 10, and this is the outside padding of our screen. Let's also add some padding to the second TextField, the Description TextField. This will create some distance between the center widget and the other widgets above and below. Let's say it will be 15.0, both for the top and for the bottom.Okay, let's have a look. Much better, right? But I want to show you something. If we turn the device, so that the orientation is horizontal, and we start typing in one of the TextField widgets, see what happens? Our screen is too small to contain all the widgets, and our app is in an error state. What could we do to solve that? I'm sure you guessed. That's right, it's enclosing everything in a ListView.Let's do that, and as we are here let's make sure the DropdownButton also takes all the horizontal space of the screen. We can do that, enclosing it in a ListTile container. Right, let's have a look now. Let's try turning the screen and type something. Nice. Everything's working. Good. This completes the user interface, but right now this is rather useless. We cannot save a to-do, we cannot delete, so let's complete it next.

Demo: Completing the App

A very important feature of a Scaffold is that it can create menus. What we'll use for this app is a pop-up menu button that, as its name implies, is a button that displays a menu when pressed. First, let's create an array of strings that will contain the menu texts we want to show to the user. Let's call it choices, and it will be a constant array of strings. The actions we want to perform through the menu are Save Todo & Back, Delete Todo, and Back to List. Then, with the same values, let's also create three distinct constants. We'll call them mnuSave, mnuDelete, and mnuBack. In order to interact with the database let's retrieve the instance of our DbHelper class.

import 'package:flutter/material.dart';
import 'package:todo_app/model/todo.dart';
import 'package:todo_app/util/dbhelper.dart';
import 'package:intl/intl.dart';

DbHelper helper = DbHelper();
final List<String> choices = const <String>[
  'Save Todo & Back',
  'Delete Todo',
  'Back to List'
];

const mnuSave = 'Save Todo & Back';
const mnuDelete = 'Delete Todo';
const mnuBack = 'Back to List';

class TodoDetail extends StatefulWidget {
  final Todo todo;
  TodoDetail(this.todo);

  @override
  State<StatefulWidget> createState() => TodoDetailState(todo);
}

class TodoDetailState extends State {
  Todo todo;
  final _priorities = ["High", "Medium", "Low"];
  String _priority = "Low";
  TextEditingController titleController = TextEditingController();
  TextEditingController descriptionController = TextEditingController();
  TodoDetailState(Todo todo) {
    this.todo = todo;
  }


  @override
  Widget build(BuildContext context) {
    titleController.text = todo.title;
    descriptionController.text = todo.description;
    TextStyle textStyle = Theme.of(context).textTheme.title;
    return Scaffold(
        appBar: AppBar(
          automaticallyImplyLeading: false,
          title: Text(todo.title),
          actions: <Widget>[
            PopupMenuButton<String>(
                onSelected: select,
                itemBuilder: (BuildContext context) {
                  return choices.map((String choice) {
                    return PopupMenuItem<String>(
                      value: choice,
                      child: Text(choice),
                    );
                  }).toList();
                }),
          ],
        ),
        body: Padding(
            padding: EdgeInsets.only(top: 35.0, left: 10.0, right: 10.0),
            child: ListView(
              children: <Widget>[
                Column(
                  children: <Widget>[
                    TextField(
                      controller: titleController,
                      style: textStyle,
                      onChanged: (value) => this.updateTitle(),
                      decoration: InputDecoration(
                          labelText: "Title",
                          labelStyle: textStyle,
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(5.0),
                          )),
                    ),
                    Padding(
                        padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
                        child: TextField(
                          controller: descriptionController,
                          style: textStyle,
                          onChanged: (value) => this.updateDescription(),
                          decoration: InputDecoration(
                              labelText: "Description",
                              labelStyle: textStyle,
                              border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(5.0),
                              )),
                        )),
                    ListTile(
                        title: DropdownButton<String>(
                      items: _priorities.map((String value) {
                        return DropdownMenuItem<String>(
                          value: value,
                          child: Text(value),
                        );
                      }).toList(),
                      style: textStyle,
                      value: retrievePriority(todo.priority),
                      onChanged: (value) => updatePriority(value),
                    ))
                  ],
                )
              ],
            )));
  }

  void select(String value) async {
    int result;
    switch (value) {
      case mnuSave:
        save();
        break;
      case mnuDelete:
        Navigator.pop(context, true);
        if (todo.id == null) {
          return;
        }
        result = await helper.deleteTodo(todo.id);
        if (result != 0) {
          AlertDialog alertDialog = AlertDialog(
            title: Text("Delete Todo"),
            content: Text("The Todo has been deleted"),
          );
          showDialog(context: context, builder: (_) => alertDialog);
        }
        break;
      case mnuBack:
        Navigator.pop(context, true);
        break;
      default:
    }
  }

  void save() {
    todo.date = new DateFormat.yMd().format(DateTime.now());
    if (todo.id != null) {
      helper.updateTodo(todo);
    } else {
      helper.insertTodo(todo);
    }
    Navigator.pop(context, true);
  }

  void updatePriority(String value) {
    switch (value) {
      case "High":
        todo.priority = 1;
        break;
      case "Medium":
        todo.priority = 2;
        break;
      case "Low":
        todo.priority = 3;
        break;
    }
    setState(() {
      _priority = value;
    });
  }

  String retrievePriority(int value) {
    return _priorities[value - 1];
  }

  void updateTitle() {
    todo.title = titleController.text;
  }

  void updateDescription() {
    todo.description = descriptionController.text;
  }
}

In the actions property of our appBar widget we'll put a PopupMenuButton of type String. In the onSelected property we will call a method called select that we will create shortly. The app will trigger onSelected and call the select method when the user selects a menu item. Then in the itemBuilder property we create a function that takes inthe current build context and will return a map of the choices. For each element of the choices array we'll return a PopupMenuItem whose value will be the choice, and the child that will be a Textcontaining the choice value, and over that we'll call the toList method. Now we need to check which menu item was selected by the user, so we'll create a method that will return void called select that will take a string as a parameter, and we'll call it value. Let's also create an integer called resolved. Here we'll write a switch statement over the value that was passed, so in case the value is mnuSave, then we call a method called save. We'll write it in a minute as well. In case mnuDelete was selected, before calling the delete method we should also check whether there is a todo to delete or not. In case the id is null, that means that the user pressed the new floating action button, creating a new todo, which has not been saved to the database yet. So if the id of the todo is null we'll just return. Otherwise, the result will be an await of the helper deleteTodo method passing the id of the todo. As we are using an await close, we also need to specify that this method is async. At this point, we could also give some feedback to our user. So if the result of the delete call is not 0, meaning that the call was successful, let's create an alert dialog that will have a title of a Text with Delete Todo, and a content with a Text of The Todo has been deleted. Then we'll call the showDialog method, passing the context, and for the builder property we'll call the alert dialog we've just created. Let's also get back to the previous screen, calling the Navigator.pop method, again, with the context. Let's also get back to the list whenthe user selects the back menu. In case value is menu back, We'll call the Navigator.pop method, passing the context and true, and then break. Okay, now let's write the save function. First, we want to format the date for the todo.date from the DateFormat class. We'll call the year month day method, and over that the format method, passing the DateTime.now method. This will write the current date inyear, month, and day format, allowing you to order dates if need be. Next, if todo.id is not null, which means we are editing an existing todo, we will call the updateTodo method. Otherwise, we'll call the insertTodo method. In any case, at the end we'll get back to the list screen. Now let's write a few helper methods to complete this part of our app. When the user selects a value from the dropdown we get a string with a value of high, medium, or low, but when we save the todo we want to actually write a number, not a string, so let's create a method that does this translation. We'll call it updatePriority, and it will take the string of the priority that we'll call value. Inside the method we'll use a switch statement to check the value. In case it's high, the priority of our todo will be one, in case it'smedium it will be two, and in case it's slow it will be three. Then we'll call the setState to update the priority, and this will also update the dropdown value. Let's also write a method to retrieve the string of the priority when we have the number. We call this retrievePriority, and it will take the integer of thepriority as a parameter. This will just return the value of the priorities array at the position of value-1 because the array index begins at 0, and our first priority is 1. Okay, let's also write two short methods, one to update the title of our todo, and one to update the description. The first one will be updateTitle, and it will set the todo.title to the titleController.text, and the other method, updateDescription, we'll set the todo.description to the descriptionController.text. Now we have to call these methods when needed. The updatePriority should be called in the onChanged method of the DropdownButton. The retrievePriority for the value and the updateTitle should be called in the onChanged property of the title TextField, and the same property of the description TextField we'll call the updateDescription. The only thing we need to do to complete our app is in the todolist.dart file in the navigateToDetail method in order to update the list when the user returns to the todolist page if the result is true, that is, when the operation succeeds, we'll call the getData method. Okay, everything's ready, and for the last time let's try this out. So now when we click on one of the items we get to the detail view. We can change something, for example, the title, and click on the Save menu. As you can see, we see the changes immediately. Let's also add a new to-do. We really need to learn Flutter, Let's save and get back to the list, and it works. Let's also try to delete an item, and this works as well. Well done. You have now completed the Todos app.

Chapter Summary & Takeaways

Building an app is always a creative and interesting process. of course, in the time we had we could only get so far, but I hope that in this chapter you could get the idea on how to use Flutter to build real world apps.

The focus of this chapter hasn't been on the user interface or widgets, but more on data, classes, navigation, and organizing an app with Flutter.

  • In particular, you have seen a way to create folders and files, so that your code is organized and modular.
  • You've seen how to use a singleton in Flutter,
  • And how you can leverage async programming. You've used futures, await, and the then method.
  • By the way, the DbHelper class is something you'll be able to use in any database app you'll create in the future, with a few tweaks.
  • We've talked about navigation in Flutter, about the stack, the push method to get to a new string, and the pop method to get back to the previous top page of the stack.
  • Obviously, this lesson was also about data. The concepts we have seen apply not only to SQLite,but also to any other way your apps will deal with data. We've seen how to use the ListView, how to pass an object that was selected to another page.

Actually, you've used and seen many other things that do not fit the screen like the use of menus, model classes and maps, and of course, you've built an app, certainly not perfect yet, but still usable.

What I like about the app we've built is that with relatively little code it's immediately useful and easy to expand. I do hope you feel the same way. Okay, in the next chapter we'll see how to use gestures in Flutter. See you there.