This is the second part of Expenses Manager app in Flutter tutorial series. We will learn to use serializable models and offline database in Flutter apps.
Introduction
In the previous post, we started out building the Expense Manager app. In this post, we will continue the development where we left off. We will add features to manage expense categories in this part of the Flutter tutorial.
At the end of this Flutter tutorial, we will have the following in our app:
- Create built value model for category
- View list of category items
- Setup offline database for storing data
- Create a service layer to access database
Let’s get started!
Creating Serializable Model For Expense Category
Every app needs data models. For our app we start with a category model which will be a built value type model. A built value type model has many benefits over reference type such as immutability and easy json serialization.
Read more about Built Value: Introduction To Built Value Library
Add dependency for built_value and built_collection packages.
dependencies:
flutter:
sdk: flutter
path_provider: ^1.1.0
sqflite: ^1.1.5
built_value: ^6.1.6
built_collection: ^4.1.0
Also add build_runner and built_value_generator as dev dependencies.
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.0.0
built_value_generator: ^6.1.6
Next, we will create a category model and a built value serializer.
Create a folder called “models” inside the “lib” folder and add a file “category_model.dart“.
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
part 'category_model.g.dart';
abstract class CategoryModel
implements Built<CategoryModel, CategoryModelBuilder> {
CategoryModel._();
factory CategoryModel([updates(CategoryModelBuilder b)]) = _$CategoryModel;
static Serializer<CategoryModel> get serializer => _$categoryModelSerializer;
@nullable
int get id;
@nullable
String get title;
@nullable
String get desc;
@nullable
int get iconCodePoint;
}
We are marking everything nullable
for now. Next add “serializers.dart“.
Read More: JSON Serialization In Flutter.
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:expense_manager/models/category_model.dart';
part 'serializers.g.dart';
@SerializersFor(const [
CategoryModel
])
final Serializers serializers = (_$serializers.toBuilder()
..addPlugin(StandardJsonPlugin()))
.build();
Both the category_model
and serilizers
are partial files. We will auto generate the remaining part of these files using the build runner command:
flutter packages pub run build_runner build
This command generates the remaining part of the built value classes in the models folder.
Our category model is ready.
Initialize And View Category List
Now that we have a working model, let’s put it into action. Inside the _HomePageState
class, let’s first initialize a list of categories.
List<CategoryModel> _lsCateogies = List<CategoryModel>();
@override
void initState() {
super.initState();
_tabController = new TabController(vsync: this, length: _tabs.length);
_initCategories();
}
_initCategories() {
var cat1 = CategoryModel().rebuild((b) => b
..id = 0
..title = "Home Utils"
..desc = "Home utility related expenses"
..iconCodePoint = Icons.home.codePoint);
_lsCateogies.add(cat1);
var cat2 = CategoryModel().rebuild((b) => b
..id = 0
..title = "Grocery"
..desc = "Grocery related expenses"
..iconCodePoint = Icons.local_grocery_store.codePoint);
_lsCateogies.add(cat2);
var cat3 = CategoryModel().rebuild((b) => b
..id = 0
..title = "Food"
..desc = "Food related expenses"
..iconCodePoint = Icons.fastfood.codePoint);
_lsCateogies.add(cat3);
var cat4 = CategoryModel().rebuild((b) => b
..id = 0
..title = "Auto"
..desc = "Car/Bike related expenses"
..iconCodePoint = Icons.directions_bike.codePoint);
_lsCateogies.add(cat4);
}
We have added 4 categories in a list so far. Next, we will create a ListView
to show these items.
Widget _getCategoryTab() {
return ListView.builder(
itemCount: _lsCateogies.length,
itemBuilder: (BuildContext ctxt, int index) {
var category = _lsCateogies[index];
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
border: new Border.all(
width: 1.0, style: BorderStyle.solid, color: Colors.white)),
margin: EdgeInsets.all(12.0),
child: ListTile(
onTap: () {},
leading: Icon(
IconData(category.iconCodePoint, fontFamily: 'MaterialIcons'),
color: Theme.of(context).accentColor,
),
title: Text(category.title, style: Theme.of(context).textTheme.body2.copyWith(
color: Theme.of(context).accentColor
),),
subtitle: Text(category.desc, ),
),
);
},
);
}
The _getCategoryTab
function creates a list based on the items available in the _lsCategories
. When you use this list for the category tab, you can see the view in action.
...
TabBarView(
controller: _tabController,
children: <Widget>[
Center(
child: Text(
"Home",
style: Theme.of(context).textTheme.display1,
)),
_getCategoryTab(),
...

That was quite simple. But this is just a static list of items. In a real application, we need to fetch data from a persistent database.
Setup Offline Database In Flutter
In this Flutter tutorial, we will be storing all our data in an offline database. For this purpose, we will make use of the SQLite database engine.
Get SQLite Plugin For Flutter
We will be using the “sqflite” plugin, which is a Flutter plugin for SQLite database engine. To use this plugin, we need to add dependency to it in the pubspec.yaml
file.
We will also add a dependency for another plugin “path_provider” which can help us with local file system.
...
dependencies:
flutter:
sdk: flutter
path_provider: ^1.1.0
sqflite: ^1.1.5
...
Usually, when you save the pubspec.yaml
file, the plugin automatically gets downloaded. If it doesn’t, you can install the package by calling the following command from command line:
flutter pub get
Setup Offline DB Provider For SQLite
We have already covered setting up a SQLite database in Flutter app in a previous post. So, here we will jump directly into implementation without much description.
Basically, we will have a database provider, database migration scripts and later a database access service.
Create a folder “db” inside the “lib” folder and another folder “migrations” inside the “db“.
Inside the “migrations” folder, add a file called init_db.dart.
// lib\db\migrations\init_db.dart
const String initDbScript = """
CREATE TABLE Category (
id INTEGER PRIMARY KEY,
title TEXT,
desc TEXT,
iconCodePoint INTEGER
);
""";
Here, we created a SQL script for category table. The category table has a title, a description and icon code point to create IconData
.
Next, create another file “db_script.dart” which will hold a list of all database migration scripts.
// lib\db\migrations\db_script.dart
import 'package:expense_manager/db/migrations/init_db.dart';
class DbMigrator {
static final Map<int, String> migrations = {
1: initDbScript,
};
}
Next, create another file “offline_db_provider.dart” inside the db folder.
// lib\db\offline_db_provider.dart
import 'dart:io';
import 'dart:math';
import 'package:expense_manager/db/migrations/db_script.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
class OfflineDbProvider {
OfflineDbProvider._();
//We use the singleton pattern to ensure that
//we have only one class instance and provide a global point access to it
static final OfflineDbProvider provider = OfflineDbProvider._();
static Database _database;
Future<Database> get database async =>
_database == null ? await initDB() : _database;
initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, _dbName);
var maxMigratedDbVersion = DbMigrator.migrations.keys.reduce(max);
_database = await openDatabase(
path,
version: maxMigratedDbVersion,
onOpen: (db) {},
onCreate: (Database db, int _) async {
DbMigrator.migrations.keys.toList()
..sort()
..forEach((k) async {
var script = DbMigrator.migrations[k];
await db.execute(script);
});
},
onUpgrade: (Database db, int _, int __) async {
var curdDbVersion = await getCurrentDbVersion(db);
var upgradeScripts = new Map.fromIterable(
DbMigrator.migrations.keys.where((k) => k > curdDbVersion),
key: (k) => k, value: (k) => DbMigrator.migrations[k]
);
if(upgradeScripts.length == 0) return;
upgradeScripts.keys.toList()
..sort()
..forEach((k) async {
var script = upgradeScripts[k];
await db.execute(script);
});
_upgradeDbVersion(db, maxMigratedDbVersion);
}
);
return _database;
}
_upgradeDbVersion(Database db, int version) async {
await db.rawQuery("pragma user_version = $version;");
}
Future<int> getCurrentDbVersion(Database db) async {
var res = await db.rawQuery('PRAGMA user_version;', null);
var version = res[0]["user_version"].toString();
return int.parse(version);
}
dropDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, _dbName);
return await deleteDatabase(path);
}
String _dbName = "ExpenseManager.db";
}
The OfflineDbProvider
is responsible for creating and updating the offline database. For adding migrations on the database, now all we have to do is add scripts via db_script.dart file.
Finally, to initialize the database when the application first runs, update the main.dart file’s main
method as below:
void main() {
OfflineDbProvider.provider.initDB();
runApp(MyApp());
}
Building A Service Layer For SQLite Database In Flutter
Create a new folder “services” inside the “db” folder and add a file “category_service.dart“. This file will contain an abstract class which will act as an interface and an implementation for the abstract class.
The class CategoryService
implements the abstract class CategoryServiceBase
.
import 'package:built_collection/built_collection.dart';
import 'package:expense_manager/models/category_model.dart';
abstract class CategoryServiceBase {
Future<BuiltList<CategoryModel>> getAllCategories();
Future<int> createCategory(CategoryModel category);
}
class CategoryService implements CategoryServiceBase {
@override
Future<BuiltList<CategoryModel>> getAllCategories() {
// TODO: implement getAllCategories
return null;
}
@override
Future<int> createCategory(CategoryModel category) {
// TODO: implement createCategory
return null;
}
}
Right now we only have two methods to interact with the SQLite database:
- create a new row of category item
- fetch the list of category items
Notice that we are working with BuiltList instead of a regular list.
Implementing the abstract class, the code for creating a new category will look like this:
@override
Future<int> createCategory(CategoryModel category) async {
//check if exists already
var exists = await categoryExists(category.title);
if(exists) return 0;
var db = await OfflineDbProvider.provider.database;
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id) as id FROM Category");
int id = table.first["id"] == null ? 1 : table.first["id"] + 1;
//insert to the table using the new id
var resultId = await db.rawInsert(
"INSERT Into Category (id, title, desc, iconCodePoint)"
" VALUES (?,?,?,?)",
[id, category.title, category.desc, category.iconCodePoint.toString()]);
return resultId;
}
Future<bool> categoryExists(String title) async {
var db = await OfflineDbProvider.provider.database;
var res = await db.query("Category");
if (res.isEmpty) return false;
var entity = res.firstWhere(
(b) =>
b["title"] == title,
orElse: () => null);
if (entity == null) return false;
return entity.isNotEmpty;
}
If same titled category doesn’t exist, then we create a new one. Similarly, here’s the implementation for getAllCategories
:
@override
Future<BuiltList<CategoryModel>> getAllCategories() async {
var db = await OfflineDbProvider.provider.database;
var res = await db.query("Category");
if (res.isEmpty) return BuiltList();
var list = BuiltList<CategoryModel>();
res.forEach((cat) {
var category = serializers.deserializeWith<CategoryModel>(CategoryModel.serializer, cat);
list = list.rebuild((b) => b..add(category));
});
return list.rebuild((b) => b..sort((a,b) => a.title.compareTo(b.title)));
}
Here, we are deserializing the category list from SQLite database with the help of built value serializer.
Our basic service layer is ready for now.
Wrapping Up
In this post we learnt about serializable models and offline database in Flutter.
So far, we have created a basic category model, added SQLite database provider and a service layer to access category table. In the next part of this Flutter tutorial series, we will add more features to the Expense Manager app.
We will learn introduce the BLoC pattern, Inherited widgets, and Stream Builders. Stay tuned!
Hi,
I get the following when I try to add _getCategoryTab(),
Controller’s length property (3) does not match the number of tabs (4) present in TabBar’s tabs properly.
Hi, Did you add the `_getCategoryTab` function as shown in the article?
TabBarView([
controller: _tabController,
children:
Center(
child: Text(
“Home”,
style: Theme.of(context).textTheme.display1,
)),
_getCategoryTab(),
…
I think the error you are getting is implying that you did something like this:
TabBarView(
controller: _tabController,
children: _getCategoryTab()
..
Yes, I did exactly as shown in the article.
Please see Screenshot.
https://snipboard.io/rhJIue.jpg
On a contrary, I tried:
TabBarView(
controller: _tabController,
children: _getCategoryTab()
and it works.
snipboard.io/xbrLYn.jpg
Ok, looks like you were now able to load the categories. But there is one issue there as the categories are being listed under the Home tab. They should be listed under categories tab.
thanks for pointing that out. I have learn alot from your posts although I still cannot grasp how SQlite works and I wish you the best in completing this app. Will follow.
Thanks a lot Emmanuel. I am glad that you are learning from these posts. Since there are a lot of new concepts introduced in these tutorial series I am sure it must be challenging to pick all the concepts at once.
I will try to create a different post on SQlite to make it easier soon.
Keep coding 🙂
Hi.
How to call the offlinedbprovider in another class, i tried something like:
var db = new OfflienDbprovider();
but dont result
Hi Joao,
Since the `OfflineDbProvider` is a static class you can not initialize an instance like the way you have tried.
You should use the `initDB` function like this:
`OfflineDbProvider.provider.initDB();`
and you can get the database instance like this:
`OfflineDbProvider.provider.database`
Thanks.
Hi again!
i cant create an database outside:
im trying like this:
Database db = OfflineDbProvider.provider.initDB();
but i get the error:
type ‘Future’ is not a subtype of type ‘Database’