With many engineering design patterns emerges that help structure your Flutter projects to allows your code perform better, structure it to make it adoptable to new changes, or makes it reusable. In this article, I will display the making of the classical Todo app using Flutter which will store & interact with it’s data reactively on its local database storage using SQLite. You can find project git repository here for complete project.
The app will cover different architectural design patterns practices such as:
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
Prerequisites:
This article will be lengthy and focused for intermediate developers that are fairly familiar with Flutter framework, as this article won’t cover all the details like the basics of Flutter or Dart syntax & semantics. The Todo App
Our target is to create a single page app that can perform CRUD (Create, Read, Update, Delete) operations on the Todo items covering mentioned design patterns.
The will have the following abilities:
- 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
Start creating Flutter project and name it whatever (I named it reactive_todo_app, but make sure when you import the dart packages to use your project name not reactive_todo_app if you were copying the code).
Go to your pubspec.yaml file (found under your main project folder) and add the two packages (sqflite & path_provider) under dependencies. Note the packages version may differ, as if now we have sqflite v1.1.0 & path_provider v0.5.0.
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.
Project Structure (Packages & Files)
Create the project packages under the lib directory/folder as shown and add its siblings dart files accordingly.
###The Todo Model/POJO (todo.dart)
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,
};
}
Our Todo model will have 3 fields as shown, id(auto-generated by database), description (the text body for Todo), and isDone (to denote whether the item was completed). We add toDatabaseJson() method in order to convert our Todo instance into a JSON format that sqflite adapters will use in order to save it on the database in form of Table record, I will demonstrated it in coming database.dart class. Vise versa the factory method fromDatabaseJson(….) will convert sqflite results (of type JSON) fetched from the database into Todo model instance.
###Creating our Database (database.dart)
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 "
")");
}
}
The purpose of database.dart is to manage creating the database schema and expose the database instance Asynchronously for TodoDao to call. The whole upper boilerplate code of the class is mostly setting up the database instance for the app. The most important piece of these lines are usage of async, await keywords that makes the database creation in a asycncronous fashion. Any asycncronous operations done in darts has to return the Future type of the class Future, in this case Futurefor database getter. As for initDB(….) this is where our tables schema are created for the first time using database.execute method to create Todo table using SQL for each model member (id, description, isDone). Note that SQLITE doesn’t have bool/boolean type for our Todo member isDone, instead we have to take alternative path to store boolean values into the database; so we will use integer type where 0 will denote isDone as False & 1 will denote isDone as True
###Data Access Object (todo_dao.dart)
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;
}
}
TodoDao is dedicated to manage all local Database Create Read Update Delete (CRUD) operations for Todo model asynchronously. This will be main communicator between the TodoRepository & our DatabaseProvider (database.dart) through following methods:
1- 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. 2- getTodos({List 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 3- updateTodo(Todo todo), update existing record by querying the database using the passed Todo instance id and update Todo’s description & isDone 4- deleteTodo(int id), delete an existing record by querying the database using the passed Todo id.
###TodoRepository (todo_repository.dart)
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();
}
In many architectural design patterns like BLoC & MVVM the repository concept responsibility is to act as proxy bridge between different data sources provider that can orchestrate CRUD operations between them. In this project scope, I’m currently using one data source which is the local database through the usage of TodoDao, as shown in the above snippet it’s only calling TodoDao methods and there’s no much value for our project, but however imagine that in the future the business needs changes and now it requires our Todo app to fetch the Todos data from a backend service (TodoWebService for example) and sync it with the local database to provide offline app support for better user experience (UX), so all what you have to do is include TodoWebService class here and perform data syncing mechanism here between the local db (TaskDao) and the API source from different data sources.
###Business Logic Component (todo_bloc.dart)
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();
}
}
TodoBloc() is our main reactive class/component that is responsible to manage our Todo data in a form of series/multiple asynchronous events (called stream). An event is the transition from data state to a new data state, for instance let’s say we have 7 Todo items in our stream that is expected to show for our users on the UI, and then through UI user decided to delete 1 Todo item to be 6 Todo items instead. In summary
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
The supervisor/admin member of TodoBloc whom responsible to manage all of the previous and more is _todoController. To elaborate, _todoController can
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.
HomePage() — The User Interface (home_page.dart)
HomePage() screen is a StatelessWidget that contains list of Todo Cards that is expected to accepts users inputs/outputs to perform the CRUD operation on the UI level. I will only show the important bits of the code since most of the code are mainly for UI designs, you can refer to the full implementation here of home_page.dart . In following, our focus will be on initializing TodoBloc() that was mentioned to get our data stream of Todo. Second point to focus on the getTodoWidget() where this is the beginning of our Reactive UI components.
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
Performing the UI READ via the stream
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);
},
);
}
First getTodoWidget() will construct the a StreamBuilder widget that is responsible of building the remaining UI components based on the data stream coming from the TodoBloc we built (todo_bloc.dart). The StreamBuilder will subscribe/observe/listen to todoBloc.todos stream in order get notified by the TodoBloc whenever there’s event that changes Todos stream and update the values of the UI like number of Todos in the listview. The stream in our project is list of Todo objects (they are referred as snapshots by convention) that can be influenced by any event that change the state of data (like deleting Todo item, re-fecthing the list of Todo, sorting the list…etc).
Performing the UI DELETE & Update
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()
Performing the UI Todo Search
We can search query Todo description using the todoBloc.getTodo(query)
// 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);
},
),
),
)
App Entry Point (main.dart)
Lastly the MyApp is standard Widget nothing special about it. It will load up our only page HomePage() we went through previously.
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'),
);
}
}
This the conclusion of this article, I hope this is somehow clarified some of these BLoC design pattern concept for Flutter project, please share your feedback or questions and I will try to do my best to reply back them.