Implement SEMBAST DB + BLOC Pattern on Flutter Todo App

in #utopian-io5 years ago

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

  • 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 the AppDatabase 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

image source

To implement the bloc pattern, we'll need to create 3 dart files.

  1. 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.
  2. todo_events.dart file: This file contains the events that will be handled by the bloc
  3. 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 extends Equatable 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 extends Equatable
  • LoadingTodoStatethis 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 extends Bloc which gives us method implementation to override.

  • Extending Bloc , we specify the Events and the state like this Bloc<TodoEvents, TodoStates>

  • TodoStates get initialState => LoadingTodoState() this implementation is executed first in the bloc, emitting a state of LoadingTodoState()

  • ThemapEventToState() 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();
              },
            ),
          ],
        );
      },
    );
  }

full code

  • 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
        
        ...
        }

full code

  • 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

https://github.com/enyason/todo_sembast_bloc

Sort:  

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 received more than 2000 upvotes. Your next target is to reach 3000 upvotes.

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!

Happy Birthday! - You are on the Steem blockchain for 1 year!

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!

Coin Marketplace

STEEM 0.29
TRX 0.12
JST 0.033
BTC 63318.34
ETH 3108.17
USDT 1.00
SBD 3.97