Flutter: Todo App using BLoC Design Pattern with SQLite

  • Google"s Business Logic Component Architecture Design Pattern (BLoC)
  • Reactive Programming using Dart streams (Similar to Redux)
  • Asynchronous events & operations (Future events)
  • CRUD operations I/O using Local Database
The Flutter Todo mobile app

Prerequisites:

The Todo App

  • Create Todo item by clicking on Floating Action Button (+) on the navigation bar
  • Update Todo by checking the checkbox to mark the Todo item as done/completed or vise versa.
  • Read Todos by fetching all created it’s records items or when searching for description by clicking the Search Icon on the navigation bar.
  • Delete Todo item by swiping the Card horizontally to right or left.

Flutter Project

  1. Sqflite is dart adapter extension for managing device SQLITE database
  2. Path_Provider is an extension that help facilitate the common device storage path, in our case will be used in conjunction with Sqflite to store database on device.
pubspec.yaml

Project Structure (Packages & Files)

The Packages & Files Structure
class Todo {
int id;
//description is the text we see on
//main screen card text
String description;
//isDone used to mark what Todo item is completed
bool isDone = false;
//When using curly braces { } we note dart that
//the parameters are optional
Todo({this.id, this.description, this.isDone = false});
factory Todo.fromDatabaseJson(Map<String, dynamic> data) => Todo(
//This will be used to convert JSON objects that
//are coming from querying the database and converting
//it into a Todo object
id: data['id'],
description: data['description'],
//Since sqlite doesn't have boolean type for true/false
//we will 0 to denote that it is false
//and 1 for true
isDone: data['is_done'] == 0 ? false : true,
);
Map<String, dynamic> toDatabaseJson() => {
//This will be used to convert Todo objects that
//are to be stored into the datbase in a form of JSON
"id": this.id,
"description": this.description,
"is_done": this.isDone == false ? 0 : 1,
};
}
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
final todoTABLE = 'Todo';
class DatabaseProvider {
static final DatabaseProvider dbProvider = DatabaseProvider();
Database _database; Future<Database> get database async {
if (_database != null) return _database;
_database = await createDatabase();
return _database;
}
createDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
//"ReactiveTodo.db is our database instance name
String path = join(documentsDirectory.path, "ReactiveTodo.db");
var database = await openDatabase(path,
version: 1, onCreate: initDB, onUpgrade: onUpgrade);
return database;
}
//This is optional, and only used for changing DB schema migrations
void
onUpgrade(Database database, int oldVersion, int newVersion) {
if (newVersion > oldVersion) {}
}
void initDB(Database database, int version) async {
await
database.execute("CREATE TABLE $todoTABLE ("
"id INTEGER PRIMARY KEY, "
"description TEXT, "
/*SQLITE doesn't have boolean type
so we store isDone as integer where 0 is false
and 1 is true*/
"is_done INTEGER "
")");
}

}
import 'dart:async';
import 'package:reactive_todo_app/database/database.dart';
import 'package:reactive_todo_app/model/todo.dart';

class TodoDao {
final dbProvider = DatabaseProvider.dbProvider;

//Adds new Todo records
Future<int> createTodo(Todo todo) async {
final db = await dbProvider.database;
var result = db.insert(todoTABLE, todo.toDatabaseJson());
return result;
}

//Get All Todo items
//Searches if query string was passed
Future<List<Todo>> getTodos({List<String> columns, String query}) async {
final db = await dbProvider.database;

List<Map<String, dynamic>> result;
if (query != null) {
if (query.isNotEmpty)
result = await db.query(todoTABLE,
columns: columns,
where: 'description LIKE ?',
whereArgs: ["%$query%"]);
} else {
result = await db.query(todoTABLE, columns: columns);
}

List<Todo> todos = result.isNotEmpty
? result.map((item) => Todo.fromDatabaseJson(item)).toList()
: [];
return todos;
}

//Update Todo record
Future<int> updateTodo(Todo todo) async {
final db = await dbProvider.database;

var result = await db.update(todoTABLE, todo.toDatabaseJson(),
where: "id = ?", whereArgs: [todo.id]);

return result;
}

//Delete Todo records
Future<int> deleteTodo(int id) async {
final db = await dbProvider.database;
var result = await db.delete(todoTABLE, where: 'id = ?', whereArgs: [id]);

return result;
}

//We are not going to use this in the demo
Future deleteAllTodos() async {
final db = await dbProvider.database;
var result = await db.delete(
todoTABLE,
);

return result;
}
}
  • createTodo(Todo todo) creates new db records in Todo table by converting Todo model into JSON format and then stored in a form of table record.
  • getTodos({List<String> columns, String query}), returns list all of Todo records or if query parameter were injected, then it filters all records using SQL WHERE to match the search
  • updateTodo(Todo todo), update existing record by querying the database using the passed Todo instance id and update Todo’s description & isDone
  • deleteTodo(int id), delete an existing record by querying the database using the passed Todo id.
import 'package:reactive_todo_app/dao/todo_dao.dart';
import 'package:reactive_todo_app/model/todo.dart';

class TodoRepository {
final todoDao = TodoDao();

Future getAllTodos({String query}) => todoDao.getTodos(query: query);

Future insertTodo(Todo todo) => todoDao.createTodo(todo);

Future updateTodo(Todo todo) => todoDao.updateTodo(todo);

Future deleteTodoById(int id) => todoDao.deleteTodo(id);

//We are not going to use this in the demo
Future deleteAllTodos() => todoDao.deleteAllTodos();
}
import 'package:reactive_todo_app/model/todo.dart';
import 'package:reactive_todo_app/repository/todo_repository.dart';


import 'dart:async';

class TodoBloc {
//Get instance of the Repository
final
_todoRepository = TodoRepository();

//Stream controller is the 'Admin' that manages
//the state of our stream of data like adding
//new data, change the state of the stream
//and broadcast it to observers/subscribers
final
_todoController = StreamController<List<Todo>>.broadcast();

get todos => _todoController.stream;

TodoBloc() {
getTodos();
}

getTodos({String query}) async {
//sink is a way of adding data reactively to the stream
//by registering a new event
_todoController.sink.add(await _todoRepository.getAllTodos(query: query));
}

addTodo(Todo todo) async {
await _todoRepository.insertTodo(todo);
getTodos();
}

updateTodo(Todo todo) async {
await _todoRepository.updateTodo(todo);
getTodos();
}

deleteTodoById(int id) async {
_todoRepository.deleteTodoById(id);
getTodos();
}

dispose() {
_todoController.close();
}
}
  • Stream is a series of asynchronous (future) events of data
  • An event is the transition of current/old data state to a new state of data
  1. Creating the stream of Todo data (by fetching Todo data asynchronously from the TodoRepository)
  2. Adding a new data event using the sink (registering new event to change the state of the data stream)
  3. Notifies/broadcasts the new state of the data stream to subscribers/observers/listeners as we will see in HomePage() class for StreamBuilder() widget.
class HomePage extends StatelessWidget {
HomePage({Key key, this.title}) : super(key: key);
//Initialize our BLoC
final
TodoBloc todoBloc = TodoBloc();

final String title;
/*Too many lines of code not included here, refer back to github repo for the complete code.*/
child: Container(
//This is where the magic starts
child: getTodosWidget()

))),
//.... rest of the HomePage class
Widget getTodosWidget() {
/*The StreamBuilder widget,
basically this widget will take stream of data (todos)
and construct the UI (with state) based on the stream
*/

return
StreamBuilder(
stream: todoBloc.todos,
builder: (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
return getTodoCardWidget(snapshot);
},
);
}
  1. To delete a Todo item, users can swipe the desired Todo card horizontally to the right or to the left to call out our TodoBloc via todoBloc.deleteTodoById(todo.id) method passing it the desired Todo item be deleted.
  2. Same thing goes for updating a Todo item however via todoBloc.updateTodo(todo) but passing it the whole Todo object, whenever the user checks the checkbox to either mark it completed (isDone) or vise versa uncheck it to mark it not completed.
Widget getTodoCardWidget(AsyncSnapshot<List<Todo>> snapshot) {
/*Since most of our operations are asynchronous
at initial state of the operation there will be no stream
so we need to handle it if this was the case
by showing users a processing/loading indicator*/
if
(snapshot.hasData) {
/*Also handles whenever there's stream
but returned returned 0 records of Todo from DB.
If that the case show user that you have empty Todos
*/
return
snapshot.data.length != 0
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, itemPosition) {
Todo todo = snapshot.data[itemPosition];
final Widget dismissibleCard = new Dismissible(
background: Container(
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Deleting",
style: TextStyle(color: Colors.white),
),
),
),
color: Colors.redAccent,
),
onDismissed: (direction) {
/*The magic
delete Todo item by ID whenever
the card is dismissed
*/
todoBloc.deleteTodoById(todo.id);

},
direction: _dismissDirection,
key: new ObjectKey(todo),
child: Card(
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey[200], width: 0.5),
borderRadius: BorderRadius.circular(5),
),
color: Colors.white,
child: ListTile(
leading: InkWell(
onTap: () {
//Reverse the value
todo.isDone = !todo.isDone;
/*
Another magic.
This will update Todo isDone with either
completed or not
*/
todoBloc.updateTodo(todo);

},
//.... the rest of getTodoCardWidget()
// void _showTodoSearchSheet(BuildContext context){
//... rest of the code
Padding(
padding: EdgeInsets.only(left: 5, top: 15),
child: CircleAvatar(
backgroundColor: Colors.indigoAccent,
radius: 18,
child: IconButton(
icon: Icon(
Icons.search,
size: 22,
color: Colors.white,
),
onPressed: () {
/*This will get all todos
that contains similar string
in the textform
*/
todoBloc.getTodos(
query:
_todoSearchDescriptionFormController
.value.text);
//dismisses the bottomsheet
Navigator.pop(context);
},
),
),
)
import 'package:flutter/material.dart';
import 'package:reactive_todo_app/ui/home_page.dart';

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Reactive Flutter',
theme: ThemeData(
primarySwatch: Colors.indigo,
canvasColor: Colors.transparent
),
//Our only screen/page we have
home: HomePage(title: 'My Todo List'),
);
}
}

--

--

--

I’m a software developer & UI/UX designer who want to share my experience with fellow developers. abdulmohsen.co https://www.patreon.com/vaygeth

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

ScandiPWA Updates: October, 7 (Issue #61)

Pass by Reference and Value in Swift

The Path to a Successful Proposal for Go

Huckleberry AMA Dec 21st 2021 — Moonriver Unofficial Telegram group

5 Things I Wish I Knew As A New Manager

Five Flutter Widgets

How to Grow your Discord Server (I’m a 1,000-member server owner)

Server analytics screenshot

Linux GUI on macOS with NoMachine, not VNC

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Vaygeth (Abdulmohsen)

Vaygeth (Abdulmohsen)

I’m a software developer & UI/UX designer who want to share my experience with fellow developers. abdulmohsen.co https://www.patreon.com/vaygeth

More from Medium

Mocking Dependencies in Flutter Unit Tests

State Management using Providers in Flutter

Applying BDD in Flutter Integration Testing with GitLab Integration

A simple Favorites app via Riverpod: Part1