11 Flutter - SQLite, Final Project
Final Project
ToDo App
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
| {
"email": "test01@akaver.com",
"password": "Kala.maja1"
}
|
Response
| {
"token": "eyJhbGciOiJIUzI…",
"status": "Account created for test01@akaver.com"
}
|
Auth Login
Login
- /api/v1/account/login
- Method: post
Body
| {
"email": "test01@akaver.com",
"password": "Kala.maja1"
}
|
Response
| {
"token": "eyJhbGciOiJIUzI…",
"status": "Logged in"
}
|
Auth Logout
Logout
Api endpoints
ToDo Api endpoints (get all)
- /api/v1/todocategories
- /api/v1/todopriorities
- /api/v1/todotasks
Get single
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
| 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]);
}
|