Skip to content

11 Flutter - SQLite, Final Project

Final Project

ToDo App

  • State and SQLite
  • Sync to ASP.NET Core backend (provided by course)
  • Auth
  • Lists, dragging, swiping

  • https://medium.muz.li/designing-pocket-lists-18b6cafd1161

  • Backend source code: https://gitlab.akaver.com/akaver/com.akaver.taltech
  • https://taltech.akaver.com
  • https://taltech.akaver.com/swagger/

Project features

Features to implement

  • Create/edit todos (category, priority)
  • Delete todo by swipping
  • Mark done/not done in list
  • Filter todo list by status (all/done/not done)
  • Change tasks order via drag-drop
  • Change theme (light to dark and vice versa) at runtime
  • Login/logout/sync data
  • Register new account
  • Work offline (local db)

Auth

Flow – Register, Login, Work, Logout

  • Content-Type: application/json
  • Host: https://taltech.akaver.com/

Auth Register

Register

  • /api/v1/account/register
  • Method: post

Body

1
2
3
4
{
    "email": "test01@akaver.com",
    "password": "Kala.maja1"
}

Response

1
2
3
4
{
    "token": "eyJhbGciOiJIUzI…",
    "status": "Account created for test01@akaver.com"
}

Auth Login

Login

  • /api/v1/account/login
  • Method: post

Body

1
2
3
4
{
    "email": "test01@akaver.com",
    "password": "Kala.maja1"
}

Response

1
2
3
4
{
    "token": "eyJhbGciOiJIUzI…",
    "status": "Logged in"
}

Auth Logout

Logout

  • Just delete the token

Api endpoints

ToDo Api endpoints (get all)

  • /api/v1/todocategories
  • /api/v1/todopriorities
  • /api/v1/todotasks

Get single

  • /api/v1/todocategories/1

Get, Post, Put, Delete – classical Rest backend http verbs

Requests with auth

On every Rest API request

  • Header:
    Authorization: bearer eyJhbGciOiJIUzI…

Rest Auth

Login/Register

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Future<JWT> fetchToken(String email, String password) async {
   Map<String, String> headers = {"Content-type": "application/json"};

  final response = await http.post(
      'https://taltech.akaver.com/api/v1/account/login',
      headers: headers,
      body: '{"email": "$email","password": "$password"}');

  if (response.statusCode == 200) {
    return JWT.fromJson(json.decode(response.body));
  } else {
    return JWT(token: "", status: response.reasonPhrase);
  }
}

JWT

JWT class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import 'package:flutter/widgets.dart';

class JWT {
  final String token;
  final String status;

  JWT({@required this.token, @required this.status});

  factory JWT.fromJson(Map<String, dynamic> json){

    return JWT(token: json['token'], status: json['status']);
  }
}

App State

Wrap the whole app inside state provider (including routes)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void main() {
  runApp(
    ChangeNotifierProvider<AuthModel>(
      builder: (_) => AuthModel(),
      child: MaterialApp(
        title: 'Navigation',
        initialRoute: '/',
        routes: {
          '/': (context) => LoginScreen(),
          '/main': (context) => MainScreen(),
        },
      ),
    ),
  );
}

DataBase SQLite

1
2
3
4
Packages
    sqflite: ^2.1.0+1
    path: ^1.8.2
    path_provider: ^2.0.11

Implement database helper class

SQLite

Singleton, Lazy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DatabaseHelper {
  static final _databaseName = "MyDatabase.db";
  static final _databaseVersion = 1;

  DatabaseHelper._internal();
  static final DatabaseHelper instance = DatabaseHelper._internal();
  factory DatabaseHelper(){
    return instance;
  }

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

  _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);

    return await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

SQLite oncreate

_onCreate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  static final table = 'my_table';

  static final columnId = '_id';
  static final columnName = 'name';
  static final columnAge = 'age';

  // SQL code to create the database table
  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE $table (
            $columnId INTEGER PRIMARY KEY,
            $columnName TEXT NOT NULL,
            $columnAge INTEGER NOT NULL
          )
          ''');
  }

SQLite data access

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  // Inserts a row in the database where each key in the Map is a column name
  // and the value is the column value. The return value is the id of the
  // inserted row.
  Future<int> insert(Map<String, dynamic> row) async {
    Database db = await instance.database;
    return await db.insert(table, row);
  }

  // All of the rows are returned as a list of maps, where each map is
  // a key-value list of columns.
  Future<List<Map<String, dynamic>>> queryAllRows() async {
    Database db = await instance.database;
    return await db.query(table);
  }

  // All of the methods (insert, query, update, delete) can also be done using
  // raw SQL commands. This method uses a raw query to give the row count.
  Future<int> queryRowCount() async {
    Database db = await instance.database;
    return Sqflite.firstIntValue(
        await db.rawQuery('SELECT COUNT(*) FROM $table'));
  }

  // Assume that the id column in the map is set. The other
  // column values will be used to update the row.
  Future<int> update(Map<String, dynamic> row) async {
    Database db = await instance.database;
    int id = row[columnId];
    return await db.update(table, row, where: '$columnId = ?', whereArgs: [id]);
  }

  // Deletes the row specified by the id. The number of affected rows is
  // returned. This should be 1 as long as the row exists.
  Future<int> delete(int id) async {
    Database db = await instance.database;
    return await db.delete(table, where: '$columnId = ?', whereArgs: [id]);
  }