Implement SEMBAST DB + BLOC Pattern on Flutter Todo App
Repository
https://github.com/tekartik/sembast.dart
What Will I Learn?
- How to implement a local database with SEMBAST
- How to use the bloc library
- How to create a Todo Application
Requirements
- System Requirements: Flutter SDK, Android Studio, IntelliJ IDE or VSCode
- OS Support: Windows, Mac OS, Linux
- Required Knowledge: A fair knowledge of Dart and Flutter
Resources for this tutorial
- Flutter Docs - https://flutter.dev/docs
- Dart Docs - https://www.dartlang.org/guides/language/effective-dart/documentation
- Singleton Pattern - https://en.wikipedia.org/wiki/Singleton_pattern
- Sembast - https://pub.dartlang.org/packages/sembast
- Bloc pattern - https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
- https://medium.com/flutterpub/effective-bloc-pattern-45c36d76d5fe
- https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1
Difficulty
Intermediate
Tutorial Duration 35- 40 Minutes
Tutorial Content
In this tutorial, we are going to learn how to implement sembast in our flutter application. Sembast is a database, and it stands for Simple Embedded Application Store database
SEmBAST is a NoSQL persistent store database solution for single process io applications. The whole document based database resides in a single file and is loaded in memory when opened. Changes are appended right away to the file and the file is automatically compacted when needed. Works on Dart VM and Flutter (no plugin needed, 100% Dart). Inspired from IndexedDB, DataStore, WebSql, NeDB, Lawndart. Supports encryption using user-defined codec. source
To follow best practices in state management we are going to implement the Bloc pattern as our state management solution. To implement the bloc pattern, we will this library flutter_bloc . This library helps simplifies the implementation of the block pattern.
We are going to create a todo app to implement sembast and the bloc library. To get started, we'll start by adding the required dependencies.
STEP 1: Add all the required dependencies
In the pubspec.yaml file, add the following dependencies
//for implementing the bloc pattern
flutter_bloc: ^0.10.0
// for implementing database
sembast: ^1.15.1
// library for getting application directory path
path_provider: ^0.5.0+1
//library simplifying object equality implementation
equatable: ^0.2.0
STEP 2 : CREATE DATABASE LAYER
Let's start by creating the database part of the app. Create a new dart file and create a class with a suitable name for the database.
**Class AppDatabase **
Dart Code
class AppDatabase {
//create single instance of AppDatabase via the private constructor
static final AppDatabase _singleton = AppDatabase._();
//getter for class instance
static AppDatabase get instance => _singleton;
//database instance
Database _database;
//private constructor
AppDatabase._();
Future<Database> get database async{
//open db if db is null
if (_database == null) {
_database = await _openDatabase();
}
//return already opened db
return _database;
}
Future<Database> _openDatabase() async {
//get application directory
final directory = await getApplicationDocumentsDirectory();
//construct path
final dbPath = join(directory.path, "todo.db");
//open database
final db = await databaseFactoryIo.openDatabase(dbPath);
return db;
}
}
- The class is a singleton class i.e only one instance of it can be created throughout the app.
AppDatabase._()
denotes a private constructor. Only inside the class can the object be instantiated.static final AppDatabase _singleton = AppDatabase._();
an instance of theAppDatabase
is created internally and stored in_singleton
static AppDatabase get instance => _singleton;
to get access to the class object via a static getter method- The get database function returns an instance of the database if it is not null else it opens the database
Model Class for Todo
We are going to structure our model class to depict the data to be stored in the database.
Dart code
class Todo {
int id;
String task;
bool isDone;
String timeStamp;
Todo({@required this.task, @required this.isDone,@required this.timeStamp});
Map<String, dynamic> toMap() {
return {"task": task, "isDone": isDone,"timeStamp":timeStamp};
}
static Todo fromMap(Map<String, dynamic> map) {
return Todo(task: map["task"], isDone: map["isDone"],timeStamp: map["timeStamp"]);
}
}
- This class is a basic dart class that represents our todo object. This class has 4 attributes as member variables.
- The constructor arguments are named parameters. The
@required
annotation is used to ensure the values are passed when the object is created. toMap()
is a handy method that returns a todo item as a map. This is necessary since data are stored as maps in sembast.fromMap
is used to create a todo object from the map object returned by sembast.
Data Access Object
The Data Access object is used to manage CRUD(Create, Read, Update, Delete) operations on the database. Basically, this gives us a clean API when interacting with the database. Just to note, sembast creates something like a container where all data is stored. See it as a big container where all the data related to our database will be stored.
Dart code
class TodoDoa {
//define store name
static const String TODO_STORE_NAME = "todo_Store";
//create store, passing the store name as an argument
final _todoStore = intMapStoreFactory.store(TODO_STORE_NAME);
//get the db from the AppDatabase class. this db object will
//be used through out the app to perform CRUD operations
Future<Database> get _db async=> await AppDatabase.instance.database;
//insert _todo to store
Future insert(Todo todo) async {
await _todoStore.add( await _db, todo.toMap());
}
//update _todo item in db
Future update(Todo todo) async{
// finder is used to filter the object out for update
final finder = Finder(filter: Filter.byKey(todo.id));
await _todoStore.update( await _db, todo.toMap(),finder: finder);
}
//delete _todo item
Future delete(int id) async {
//get refence to object to be deleted using the finder method of sembast,
//specifying it's id
final finder = Finder(filter: Filter.byKey(id));
await _todoStore.delete(await _db, finder: finder);
}
//get all listem from the db
Future<List<Todo>> getAllSortedByTImeStamp() async {
//sort the _todo item in order of their timestamp
//that is entry time
final finder = Finder(sortOrders: [SortOrder("timeStamp",false)]);
//get the data
final snapshot = await _todoStore.find(
await _db,
finder: finder,
);
//call the map operator on the data
//this is so we can assign the correct value to the id from the store
//After we return it as a list
return snapshot.map((snapshot) {
final todo = Todo.fromMap(snapshot.value);
todo.id = snapshot.key;
return todo;
}).toList();
}
}
- First, we define a name for sembast store. That is the container.
- Next we create the container using
intMapStoreFactory.store(TODO_STORE_NAME)
- In the data access object, we have got methods for carrying out the CRUD operations.
- Sembast provides a finder object, which we have leverage on for sorting and filtering.
STEP 3: CREATE THE BLOC LAYER
The Bloc pattern as we have chosen to architect our app will be done using the bloc library. The Bloc pattern is a state management system for Flutter, recommended by Google developers. It helps in managing state and make access to data from a central place in your project. The Bloc pattern basically is a BLOCK that is intermediary between our UI logic and our Business Logic. The Bloc pattern depends heavily on dart streams API for reactive programming.
Setting up the bloc pattern becomes less cumbersome with the bloc library, as the stream implementation is abstracted for us, allowing the developer to focus on what is important. See image below for a block view of the pattern
To implement the bloc pattern, we'll need to create 3 dart files.
- The todo_bloc.dart file: This file is the core of the bloc implementation. It handles dispatching of events onto the stream and outputting different states unto the UI, depending on the events.
- todo_events.dart file: This file contains the events that will be handled by the bloc
- todo_states.dart file: This file contains the states that will be emitted to the listener. In our case our UI.
TodoEvents class
Create this class in the todo_events.dart file
//base class for events
abstract class TodoEvents extends Equatable {
TodoEvents([List props = const []]) : super(props);
}
class AddTodoEvent extends TodoEvents {
final String task;
AddTodoEvent(this.task):super([task]);}
class DeleteTodoEvent extends TodoEvents {
final int id;
DeleteTodoEvent(this.todo) : super([id]);}
class UpdateTodoEvent extends TodoEvents {
final Todo todo;
UpdateTodoEvent(this.todo) : super([todo]);}
class QueryTodoEvent extends TodoEvents {}
- In our todo_events.dart file, we define the events that will be dispatched unto the bloc
- First, we create a base class
TodoEvents
that extendsEquatable
which the specific event classes will also extend. The base class extends equatable to simplify the implementation of equality. Equality is determining whether two objects are equal in all regards. Doing this manually will be cumbersome, resulting in a lot of boilerplate codes. With the equatable library, the implementation is abstracted, giving us a cleaner code. AddTodoEvent
this event is dispatched to the bloc when a new todo item is to be added to the database. The event is passed with a string object specifying the title of the task.DeleteTodoEvent
this event is dispatched to the bloc, carrying the item id which is to be deleted. The bloc accepts the event and communicates with the database layer, to delete the specific item.UpdateTodoEvent
this event is fired when an item is to be updated in the DB. The updated _todo item is passed unto the bloc and then to the database layer to do the actual update int he database.QueryTodoEvent
this event is dispatched unto the bloc whenever a list of items is requested from the UI.
TodoStates Class
Create this class in todo_states.dart file
abstract class TodoStates extends Equatable{
TodoStates([List props = const []]):super(props);
}
class LoadingTodoState extends TodoStates{}
class EmptyTodoState extends TodoStates{}
class LoadedTodoState extends TodoStates{
List<Todo> list;
LoadedTodoState(this.list):super([list]);
}
- Basically, this file contains the states that will be returned to the UI, depending on the events the dispatched unto the bloc.
TodoStates
is the base class that extendsEquatable
LoadingTodoState
this state returns when the DB is loaded initially. Also when there is a possible delay in retrieving data from the database. If this state is yielded to the UI, we can display a loading indicator.EmptyTodoState
this state is returned to the UI when a query to the database returns an empty list.LoadedTodoState
this state is returned when there is a successful query of the database and also a list of data is returned to the UI.
TodoBloc Class
Create this class in todo_bloc.dart file
class TodoBloc extends Bloc<TodoEvents, TodoStates> {
final TodoDoa _todoDao;
int tdlCount = 0;
int isDoneCount=0;
TodoBloc(this._todoDao);
@override
TodoStates get initialState => LoadingTodoState();
@override
Stream<TodoStates> mapEventToState(TodoEvents event) async* {
if (event is AddTodoEvent) {
//create new _todo object
Todo todo = Todo(
task: event.task.trim(),
isDone: false,
timeStamp: DateTime.now().millisecondsSinceEpoch.toString());
//insert _todo to db
await _todoDao.insert(todo);
//query db to update ui
dispatch(QueryTodoEvent());
//
} else if (event is UpdateTodoEvent) {
//update _todo
await _todoDao.update(event.todo);
//query db to update ui
dispatch(QueryTodoEvent());
} else if (event is DeleteTodoEvent) {
//delete _todo
await _todoDao.delete(event.todo);
//query db to update ui
dispatch(QueryTodoEvent());
} else if (event is QueryTodoEvent) {
print("query");
//get all items
final tdl = await _todoDao.getAllSortedByTImeStamp();
print("query 1");
// get count of _todo list items that are checked done
isDoneCount=tdl.where((f)=>f.isDone).length;
if (tdl.isEmpty) {
//yield empty state if list is empty
yield EmptyTodoState();
} else {
//keep track of list item
tdlCount = tdl.length;
//yield loaded state unto the stream with the list
yield LoadedTodoState(tdl);
}
}
}
}
This class is the core of the bloc pattern.
TodoBloc
extendsBloc
which gives us method implementation to override.Extending
Bloc
, we specify the Events and the state like thisBloc<TodoEvents, TodoStates>
TodoStates get initialState => LoadingTodoState()
this implementation is executed first in the bloc, emitting a state ofLoadingTodoState()
The
mapEventToState()
method handles the mapping of events to states. That is, depending on what event has been dispatched unto the bloc, it will determine which state to pass unto the stream the UI is listening to.In the
mapEventToState()
we have a bunch of if else statements to determine which event has been dispatched and act accordingly.
STEP 4 : CREATE UI + TYING ALL TOGETHER
We have come a long way. We started by creating the database layer and the Bloc. We are now going to hook up the UI to the business logic.
To get started on the UI, create a stateless widget
**Override the initState and initialize objects required **
@override
void initState() {
// create instance of the class member variables
_textEditingController = TextEditingController();
_todoBloc = TodoBloc(TodoDoa());
//dispatch query event to retrieve _todo list
_todoBloc.dispatch(QueryTodoEvent());
super.initState();
}
the text editing controller is used to manage the text field that will handle user input. E.g this controller will give us the value of our text filed anytime we have need of it.
Also, we initialize our todo bloc, passing an instance of the data access object
After initializing all necessary object, we dispatch a query event, to get a list of todo items
**Create List Item Widget **
This widget will render each item properly on the list
Widget _buildListTile(Todo todo) {
return ListTile(
title: todo.isDone
? Text(
todo.task,
style: TextStyle(
decoration: TextDecoration.lineThrough,
decorationColor: Colors.blue,
color: Colors.blue),
)
: Text(todo.task),
leading: todo.isDone
? Icon(
Icons.check_circle_outline,
color: Colors.blue,
)
: Icon(Icons.radio_button_unchecked),
trailing: IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () {
//delete item
_todoBloc.dispatch(DeleteTodoEvent(todo));
}),
onTap: () {
_asyncInputDialog(context, todo);
},
onLongPress: () {
//toggle task state
if (todo.isDone) {
todo.isDone = false;
} else {
todo.isDone = true;
}
//update item
_todoBloc.dispatch(UpdateTodoEvent(todo));
},
);
}
In the list item, determine the UI appearance, depending on whether the Todo item is checked as done or not.
Long pressing on the item toggles the state of the item to either done or not
Clicking once on the item opens up a dialog for editing the task.
List Tile for Todo not Checked as done
List Tile for Todo Checked as done
**Create dialog for create/update **
Future _asyncInputDialog(BuildContext context, Todo todo) async {
...
return showDialog<String>(
context: context,
barrierDismissible: true,
// dialog is dismissible with a tap on the barrier
builder: (BuildContext context) {
return AlertDialog(
title: Text('What are you planning to perform?'),
content: Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _textEditingController,
decoration: InputDecoration(
labelText: 'New Task', hintText: "Your Task"),
onChanged: (value) {
task = value;
},
))
],
),
actions: <Widget>[
FlatButton(
//render child depending on the sate of the todo
child: todo == null ? Text('Create task') : Text("Update task"),
onPressed: () {
//handle empty state for input field
if (task.trim().length < 1) {
Toast.show("failed!", context,
duration: 1,
backgroundColor: Colors.grey,
textColor: Colors.black);
return;
}
//dispatch event depending on the state
if (todo == null) {
_todoBloc.dispatch(AddTodoEvent(task));
} else {
todo.task = task;
_todoBloc.dispatch(UpdateTodoEvent(todo));
}
//close dialog window
Navigator.of(context).pop();
},
),
],
);
},
);
}
- The dialog handles inserting new task and updating a task.
- Depending on whether the add new button or on tap of the list item was triggered, the appropriate UI for the dialog will be rendered. Also, the operation will differ, either adding a new task or updating it
The image below shows the dialog in its different states
**Return a Bloc Builder in the build method **
// bloc builder acts like a s
return BlocBuilder(bloc: _todoBloc, builder:(context, state){
print(state);
//show indicator if state is loading
if (state is LoadingTodoState) {
return Center(
child: Container(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator()));
}
//if state is empty show empty msg
if (state is EmptyTodoState) {
return Center(child: Text("Todo list is empty"));
}
if (state is LoadedTodoState) {
//if state is empty show empty msg
if (state.list.length == 0 || state.list == null) {
return Center(
child: Text("Todo list is empty"),
);
}
//get percent
var percent = ((_todoBloc.isDoneCount / _todoBloc.tdlCount) * 100);
//get current date
var format = DateFormat("yMMMMd");
var dateString = format.format(DateTime.now());
//if the state is loaded. display items in a list
...
}
- The bloc builder is like a stream builder widget that listens for state changes, to update the UI accordingly.
- A loading indicator is rendered when the state is loading; the empty message is rendered to the screen when the state is empty state while when there is data, a list of todo items are rendered unto the screen.
It's interesting how far we've come. Following all the steps above, u should be able to come up with some similar to what is shown on the image below
A simple demo of the app
Proof of Work
The complete source code can be found on gitHub
Thank you for your contribution.
After reviewing your tutorial we suggest the following points listed below:
In some sections of your code you did not post comments. As you know, comments for less experienced code users are very important.
Also in the code sections, sometimes you leave very large blanks space. Please enter the code idented.
Overall the tutorial is very well structured and complete. Thank you for your work.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Chat with us on Discord.
[utopian-moderator]
Thanks for reviewing my contribution
Thank you for your review, @portugalcoin! Keep up the good work!
Hi, @ideba!
You just got a 1.04% upvote from SteemPlus!
To get higher upvotes, earn more SteemPlus Points (SPP). On your Steemit wallet, check your SPP balance and click on "How to earn SPP?" to find out all the ways to earn.
If you're not using SteemPlus yet, please check our last posts in here to see the many ways in which SteemPlus can improve your Steem experience on Steemit and Busy.
Hi @ideba!
Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server
Hey, @ideba!
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!
Congratulations @ideba! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :
You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP
Vote for @Steemitboard as a witness to get one more award and increased upvotes!
Congratulations @ideba! You received a personal award!
You can view your badges on your Steem Board and compare to others on the Steem Ranking
Vote for @Steemitboard as a witness to get one more award and increased upvotes!