- Home ›
- Categories ›
- Flutter
Tutorial: Sembast as local data storage in Flutter
Almost every app needs to store data locally on the user’s device. One option would be using SQLite, but with Flutter, I prefer the Sembast library. Sembast is a NoSQL database which allows storing data in a JSON format.
In this tutorial, I will show you how to use Sembast as your local data storage in your Flutter app. I will not only show you how it can be used, but also how to integrate it in your app architecture.
As an example, we will create a simple app to store a list of our favorite cakes. Everyone loves cakes, right? 🎂
The complete source code of this tutorial is available on Github: https://github.com/stefangaller/flutter_sembast_local_data_storage
This tutorial shows how to use Sembast using the BLoC pattern. I have also created a tutorial for Riverpod: Tutorial: Using Sembast with Riverpod.
Prerequisite
As with my last tutorial “Simple Flutter app initialization with Splash Screen using FutureBuilder” we will start by creating a new Flutter project and cleaning up the boilerplate code. I will name the project it sembast_tutorial
.
The pubspec.yaml
and the main.dart
file should look like this:
pubspec.yaml
name: sembast_tutorial
description: A simple Sembast tutorial
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
Code language: YAML (yaml)
main.dart
import 'package:flutter/material.dart';
void main() => runApp(CakeApp());
class CakeApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Favorite Cakes',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My Favorite Cakes"),
),
);
}
}
Code language: Dart (dart)
The “Cake” Entity
First, we create a new file called cake.dart
. This file will contain our data class Cake
. This entity will have an id
, a name
, and a yummyness
factor.
cake.dart
class Cake {
final int id;
final String name;
final int yummyness;
Cake({this.id, this.name, this.yummyness});
Map<String, dynamic> toMap() {
return {
'name': this.name,
'yummyness': this.yummyness
};
}
factory Cake.fromMap(int id, Map<String, dynamic> map) {
return Cake(
id: id,
name: map['name'],
yummyness: map['yummyness'],
);
}
Cake copyWith({int id, String name, int yummyness}){
return Cake(
id: id ?? this.id,
name: name ?? this.name,
yummyness: yummyness ?? this.yummyness,
);
}
}
Code language: Dart (dart)
Let’s see what this code does in more detail:
- Lines 2-6: Here we specify all our attributes and create a default constructor for all attributes in line 6.
- Lines 8-13: The
toMap
function is used to convert our data into aMap
so we can later store it into our Sembast database. We don’t need to add theid
into the map here. - Lines 15-21: When reading the data from Sembast we will receive it as a
Map
. ThefromMap
factory constructor allows us to create a Cake instance from anid
and thisMap
. - Lines 23-30: It is common practice in Dart to use immutable data objects (notice the
final
keyword used for our attributes in lines 2-4). If we want to change an attribute we will create a copy of the object but with the desired attribute using thecopyWith
function.
For reducing complexity of this tutorial, we do not use any external libraries for generating the code used in our data model. If you want to avoid writing the toMap
, fromMap
and copyWith
functions yourself, you can use libraries like freezed and json_serializable to generate these functions.
Initialization: open the Sembast database
For this step will first add Sembast, GetIt and path_provider to our project. Sembast will be used as our database implementation and GetIt to make this database available throughout our app. The path_provider library is used to retrieve the application directory on the device. This directory will be home of our database.
To add the libraries, we specify them as dependencies
in pubspec.yaml
. The dependencies section should look like this:
...
dependencies:
flutter:
sdk: flutter
sembast: ^2.4.3
get_it: ^4.0.2
path_provider: ^1.6.7
...
Code language: YAML (yaml)
Don’t forget to run flutter pub get
to download the dependencies.
For the app initialization, we will use a similar approach we have used in the tutorial “Simple Flutter app initialization with Splash Screen using FutureBuilder”. Therefore, we will continue by creating a file called init.dart
.
init.dart
import 'package:get_it/get_it.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
class Init {
static Future initialize() async {
await _initSembast();
}
static Future _initSembast() async {
final appDir = await getApplicationDocumentsDirectory();
await appDir.create(recursive: true);
final databasePath = join(appDir.path, "sembast.db");
final database = await databaseFactoryIo.openDatabase(databasePath);
GetIt.I.registerSingleton<Database>(database);
}
}
Code language: Dart (dart)
The interesting part is the _initSembast
function.
- Lines 13 and 14 retrieve the application document directory of the app and creates it if necessary.
- Line 15 builds the path for our database file. Make sure to import
path.dart
to have access to thejoin
function. - In line 16 we use the Sembast
databaseFactoryIo
to open the database. - Line 17 registers the database to GetIt. This allows us to access our database instance as a singleton from anywhere in the app.
Cake Repository: Providing access to the database
In the next step, we will create a repository to store and receive our Cake
instances to and from the Sembast database. It is good practice to define an abstract class to define the interface of your repository. Using this approach, you can later simply exchange your database implementation for another one easily.
cake_repository.dart
In this file, we just define an abstract class CakeRepository
with two functions insertCake
and getAllCakes
that we will implement using Sembast.
import 'cake.dart';
abstract class CakeRepository {
Future<int> insertCake(Cake cake);
Future updateCake(Cake cake);
Future deleteCake(int cakeId);
Future<List<Cake>> getAllCakes();
}
Code language: Dart (dart)
sembast_cake_repository.dart
The implementation isn’t complicated either:
import 'package:get_it/get_it.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast_tutorial/cake.dart';
import 'package:sembast_tutorial/cake_repository.dart';
class SembastCakeRepository extends CakeRepository {
final Database _database = GetIt.I.get();
final StoreRef _store = intMapStoreFactory.store("cake_store");
@override
Future<int> insertCake(Cake cake) async {
return await _store.add(_database, cake.toMap());
}
@override
Future updateCake(Cake cake) async {
await _store.record(cake.id).update(_database, cake.toMap());
}
@override
Future deleteCake(int cakeId) async{
await _store.record(cakeId).delete(_database);
}
@override
Future<List<Cake>> getAllCakes() async {
final snapshots = await _store.find(_database);
return snapshots
.map((snapshot) => Cake.fromMap(snapshot.key, snapshot.value))
.toList(growable: false);
}
}
Code language: Dart (dart)
- Line 6: We create a new class
SembastCakeRepository
extending our abstractCakeRepository
. We could later also create something like aSQLiteCakeRepository
or aFirebaseCakeRepository
. This way we stay flexible. - Line 7: Here we use GetIt to retrieve the
Database
instance that we registered earlier. - Line 8: With Sembast we have multiple stores in one database. Here, we create a reference to our store with the name
cake_store
. Don’t worry, we don’t need to create the store ourselves. Sembast makes sure we can write to the store. - Lines 11 – 13: This is all it takes to add a new
Cake
to the store. Theadd
function requires a reference of the database and the data in form of a map. We can use ourtoMap
function here, we created earier. The function returns the key of the newly created entry. - Lines 16 – 18: with
_store.record
we can directly access a database record by its ID. We will use this here to explicitlyupdate
this record with the new data. - Line 21 – 23: Very similar to updating a record is deleting one. Again we fetch the record with
_store.record
and then call thedelete
function. - Lines 26 – 31: This function returns a list of all cakes in the store. Line 27 executes the query to retreive all cakes from the store. We then need to convert the
Map
retreived from the store to aCake
object again. Lukily we have created thefromMap
factory before. Don’t forget to convert the result to a List before returning. Forgetting this call will lead to error, which is hard to debug.
init.dart
Finally, all that is left is to register our SembastCakeRepository
to GetIt. Therefore, we will edit our init.dart
file. We create a new function _registerRepositories
where we do the registration, and we call this function in the initialize
function. The code should look like this:
static Future initialize() async {
await _initSembast();
_registerRepositories();
}
static _registerRepositories(){
GetIt.I.registerLazySingleton<CakeRepository>(() => SembastCakeRepository());
}
Code language: Dart (dart)
Note that in line 7 we use CakeRepository
as the generic type, but register a SembastCakeRepository
. Later you could register a different implementation of CakeRepository
here.
Putting it all together: The User Interface
Finally, we are going to connect our code and put it to use. First, we need to clean up a little. We will move the HomePage
class to a separate file and convert the CakeApp
class to a StatefulWidget
. Also, we are going to add our initialization logic to the main.dart
file.
main.dart
import 'package:flutter/material.dart';
import 'package:sembast_tutorial/home_page.dart';
import 'package:sembast_tutorial/init.dart';
void main() => runApp(CakeApp());
class CakeApp extends StatefulWidget {
@override
_CakeAppState createState() => _CakeAppState();
}
class _CakeAppState extends State<CakeApp> {
final Future _init = Init.initialize();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Favorite Cakes',
home: FutureBuilder(
future: _init,
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.done){
return HomePage();
} else {
return Material(
child: Center(
child: CircularProgressIndicator(),
),
);
}
},
),
);
}
}
Code language: Dart (dart)
As you can see, this file still looks very similar to what we created in this tutorial. The only difference here is that we used a StatefulWidget. This is necessary, since some of the Sembast code is required to run after the runApp
function (line 5) and this way it does.
home_page.dart
This is the heart of our app. On our HomePage
we are going to display a list of our Cakes
and allow them to be added, edited and deleted.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:sembast_tutorial/cake.dart';
import 'package:sembast_tutorial/cake_repository.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
CakeRepository _cakeRepository = GetIt.I.get();
List<Cake> _cakes = [];
@override
void initState() {
super.initState();
_loadCakes();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My Favorite Cakes"),
),
body: ListView.builder(
itemCount: _cakes.length,
itemBuilder: (context, index) {
final cake = _cakes[index];
return ListTile(
title: Text(cake.name),
subtitle: Text("Yummyness: ${cake.yummyness}"),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteCake(cake),
),
leading: IconButton(
icon: Icon(Icons.thumb_up),
onPressed: () => _editCake(cake),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addCake,
),
);
}
_loadCakes() async {
final cakes = await _cakeRepository.getAllCakes();
setState(() => _cakes = cakes);
}
_addCake() async {
final list = ["apple", "orange", "chocolate"]..shuffle();
final name = "My yummy ${list.first} cake";
final yummyness = Random().nextInt(10);
final newCake = Cake(name: name, yummyness: yummyness);
await _cakeRepository.insertCake(newCake);
_loadCakes();
}
_deleteCake(Cake cake) async {
await _cakeRepository.deleteCake(cake.id);
_loadCakes();
}
_editCake(Cake cake) async {
final updatedCake = cake.copyWith(yummyness: cake.yummyness + 1);
await _cakeRepository.updateCake(updatedCake);
_loadCakes();
}
}
Code language: Dart (dart)
I will assume here that you already know how to build UIs in Flutter and won’t explain what is happening in the build
function.
- Line 14: Since we have registered our
CakeRepsitory
to GetIt we are able to retrieve and use it here. - Line 15: Our state contains a list of all our Cakes. We use Flutter’s state management to update this list.
- Lines 54 – 57: Here we simply call our repository to fetch all cakes from the database. After this has completed, we use Flutter’s setState function to update the list items and trigger a redraw of the list.
- Lines 59 – 66: The first 3 lines simply generate a
name
and ayummyness
randomly. With these values, we create a newCake
instance and call theinsertCake
function on ourCakeRepository
to insert theCake
. After this, we reload our list by calling_loadCakes()
. - Lines 68 – 71: Deleting a
Cake
is even simpler. Again, we use ourCakeRepository
and reload the list afterwards. - Lines 73 – 77: Here we want to increase the
yummyness
of theCake
. We make use of thecopyWith
function to add 1 to the currentyummyness
. Again, we use theCakeRepository
to update and reload the list.
And that’s it! 🎉
Let’s start the app and see what we have accomplished. It allows you to do add, edit, and delete objects using a Sembast database. Most importantly, you also see how an architecture looks like for integrating Sembast into your app.
Summary
We have created a simple app with a local storage using Sembast. First, we have set up a data Entity to store. We needed to create some functions for converting to and from a map there.
Next, we initialized our Sembast database and made it available in the app with GetIt. Using the database, we have implemented a repository allowing us to read and write to the Sembast database.
Finally, in the last step, we added a user interface to add, edit and remove cakes and display them in a list.
I hope you enjoyed to the tutorial, and it gave you an idea on how to integrate Sembast as a local database to your app.
Write me a comment if you gave Sembast a try or whether you prefer an alternative solution.
Comments
In home_page.dart file line 56 “setState(() => _cakes = cakes);” gives an error because a value of type ‘Future<List>’ can’t be assigned to a variable of type ‘List’.
As the variable cake is a Future<List> type and _cakes is List type. I tried to cast it to a List, tried to use .tolist() inside a .then but I couldn’t figure it out
Can you please provide us/me a solution for this?? I’m stuck after I used your architecture on my project.
Hello.
Amazing tutorial. I’m having a problem. I’m trying to implement this logic on my app but I have two parameters which are nested string lists, like this “List<List>”. The terminal says “Unhandled Exception: type ‘ImmutableList’ is not a subtype of type ‘List<List>”. You know how could I implement the nested string lists into the main class??
Kind regards.
This happens when you wrong declare your interface variable which will get the instance. For example: if a make an implementation of CakeRepository, it waits an interface that declare only the sign methods, for example ICakeReposity (the I at the first position stand for Interface).
So you’ll have somenthing like ICakeRepository varName = GetIt.I.get();
Hi Szymon,
I had the same Problem. For me it worked to make registerRepositories to a void function because it was not (or maybe not right) called. Like:
static void _registerRepositories(){
this -^
I’m a bloody beginner with flutter(my first project ;)), so maybe the right and better way is a Future, but void works.
Thank you stefan Galler for this nice Tutorial.
Sorry for my bad english!
Many Greetings!
Janik-ux
Is it possible to implement this with providers like riverpod? If so, any idea to do it.
Thanks for sharing your knowledge.
All this impementation is possible to achieve using Hive?
Thanks in advanced..
I am getting this error after following the tutorial. Initially, I used the same verison for the dependencies you used and then upgraded to the latest version to attempt to fix the problem. Nothing has worked so far.
”’
No type FormulaRepository is registered inside GetIt.
Did you forget to pass an instance name?
(Did you accidentally do GetIt sl=GetIt.instance(); instead of GetIt sl=GetIt.instance;
did you forget to register it?)
‘package:get_it/get_it_impl.dart’:
Failed assertion: line 257 pos 14: ‘instanceFactory != null’
”’
Hi Szymon,
there is no class named FormulaRepository in this tutorial.
Maybe you registered a
CakeRepository
and now try to retrieve aFormulaRepository
?