Using SQLite in Flutter

admin

Updated on:

SQLite databases in Flutter.

Persisting data is very important for users since it would be inconvenient for them to type their information every time or wait for the network to load the same data again. In situations like this, it would be better to save their data locally.

In this article, I will demonstrate this using SQLite in Flutter.

Why SQLite?

SQLite is one of the most popular ways to store data locally. For this article, we will be using the package sqflite to connect with SQLite. Sqflite is one of the most used and up to date packages for connecting to SQLite databases in Flutter.

1. Add dependencies to your project

In your project go to pubspec.yaml and look for dependencies. Under dependencies, add the latest version of sqflite and path_provider (use the right numbers from Pub).

[php]dependencies:
flutter:
sdk: flutter
sqflite: any
path_provider: any[/php]

 

NOTE:

We use the path_provider package to get the commonly used location such as TemporaryDirectory and ApplicationDocumentsDirectory.

2. Create a DB Client

Now in your project create a new file Database.dart. In the newly created file, we need to create a singleton.

Why we need singleton: We use the singleton pattern to ensure that we have only one class instance and provide a global point access to it

1.Create a private constructor that can be used only inside the class :

[php]class DBProvider {
DBProvider._();
static final DBProvider db = DBProvider._();
}[/php]

 

2.Setup the database

Next we will create the database object and provide it with a getter where we will instantiate the database if it’s not (lazy initialization).

[php]
static Database _database;

Future<Database> get database async {
if (_database != null)
return _database;

// if _database is null we instantiate it
_database = await initDB();
return _database;
}
[/php]

 

If there is no object assigned to the database, we use the initDB function to create the database. In this function, we will get the path for storing the database and create the desired tables:

[php]
initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "TestDB.db");
return await openDatabase(path, version: 1, onOpen: (db) {
}, onCreate: (Database db, int version) async {
await db.execute("CREATE TABLE Client ("
"id INTEGER PRIMARY KEY,"
"first_name TEXT,"
"last_name TEXT,"
"blocked BIT"
")");
});
}
[/php]

 

NOTE: The database name is TestDB and the only table we have is called Client. If you don’t know what’s going on you really need to go and learn some SQL it’s more important than water.

3. Create the Model Class

The data inside your database will be converted into Dart Maps so first, we need to create the model classes with toMap and fromMap methods. I am not going to cover how to do this manually. If you don’t know how to do this, you should consider reading this article by Poojã Bhaumik.

To create our model classes, I am going to use this website. If you don’t already have it bookmarked, you really should 🙂

You can click here to see how it all works

Our Model:

[php]
/// ClientModel.dart
import ‘dart:convert’;

Client clientFromJson(String str) {
final jsonData = json.decode(str);
return Client.fromJson(jsonData);
}

String clientToJson(Client data) {
final dyn = data.toJson();
return json.encode(dyn);
}

class Client {
int id;
String firstName;
String lastName;
bool blocked;

Client({
this.id,
this.firstName,
this.lastName,
this.blocked,
});

factory Client.fromJson(Map<String, dynamic> json) => new Client(
id: json["id"],
firstName: json["first_name"],
lastName: json["last_name"],
blocked: json["blocked"],
);

Map<String, dynamic> toJson() => {
"id": id,
"first_name": firstName,
"last_name": lastName,
"blocked": blocked,
};
}
[/php]

 

4. CRUD operations

Create

The SQFlite package provides two ways to handle these operations using RawSQL queries or by using table name and a map which contains the data :

Using rawInsert :

[php]
newClient(Client newClient) async {
final db = await database;
var res = await db.rawInsert(
"INSERT Into Client (id,first_name)"
" VALUES (${newClient.id},${newClient.firstName})");
return res;
}
[/php]

 

Using insert :

[php]
newClient(Client newClient) async {
final db = await database;
var res = await db.insert("Client", newClient.toMap());
return res;
}
[/php]

 

Another example using the biggest ID as a new ID:

[php]
newClient(Client newClient) async {
final db = await database;
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM Client");
int id = table.first["id"];
//insert to the table using the new id
var raw = await db.rawInsert(
"INSERT Into Client (id,first_name,last_name,blocked)"
" VALUES (?,?,?,?)",
[id, newClient.firstName, newClient.lastName, newClient.blocked]);
return raw;
}
[/php]

 

Read

Get Client by id

[php]
getClient(int id) async {
final db = await database;
var res =await db.query("Client", where: "id = ?", whereArgs: [id]);
return res.isNotEmpty ? Client.fromMap(res.first) : Null ;
}
[/php]

 

In the above code, we provide the query with an id as the argument using whereArgs. We then return the first result if the list is not empty else we return null.

Get all Clients with a condition

In this example I used rawQuery and I mapped the result list to a list of Client objects:

[php]
getAllClients() async {
final db = await database;
var res = await db.query("Client");
List<Client> list =
res.isNotEmpty ? res.map((c) => Client.fromMap(c)).toList() : [];
return list;
}
[/php]

 

Example: Only get the Blocked Clients

[php]
getBlockedClients() async {
final db = await database;
var res = await db.rawQuery("SELECT * FROM Client WHERE blocked=1");
List<Client> list =
res.isNotEmpty ? res.toList().map((c) => Client.fromMap(c)) : null;
return list;
}
[/php]

 

Update

Update an existing Client

[php]
updateClient(Client newClient) async {
final db = await database;
var res = await db.update("Client", newClient.toMap(),
where: "id = ?", whereArgs: [newClient.id]);
return res;
}
[/php]

 

Example: Block or unblock a Client:

[php]
blockOrUnblock(Client client) async {
final db = await database;
Client blocked = Client(
id: client.id,
firstName: client.firstName,
lastName: client.lastName,
blocked: !client.blocked);
var res = await db.update("Client", blocked.toMap(),
where: "id = ?", whereArgs: [client.id]);
return res;
}
[/php]

 

Delete

Delete one Client

[php]
deleteClient(int id) async {
final db = await database;
db.delete("Client", where: "id = ?", whereArgs: [id]);
}
[/php]

 

Delete All Clients

[php]
deleteAll() async {
final db = await database;
db.rawDelete("Delete * from Client");
}
[/php]

 

Demo

 

For our demo, we will create a simple Flutter app to interact with our database.

We will first start with the app’s layout:

[php]
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Flutter SQLite")),
body: FutureBuilder<List<Client>>(
future: DBProvider.db.getAllClients(),
builder: (BuildContext context, AsyncSnapshot<List<Client>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Client item = snapshot.data[index];
return ListTile(
title: Text(item.lastName),
leading: Text(item.id.toString()),
trailing: Checkbox(
onChanged: (bool value) {
DBProvider.db.blockClient(item);
setState(() {});
},
value: item.blocked,
),
);
},
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
Client rnd = testClients[math.Random().nextInt(testClients.length)];
await DBProvider.db.newClient(rnd);
setState(() {});
},
),
);
}
[/php]

 

Notes :

1. The FutureBuilder is used to get the data from the database.

2. The FAB to adds a random client to the database when it’s clicked.

[php]
List<Client> testClients = [
Client(firstName: "Raouf", lastName: "Rahiche", blocked: false),
Client(firstName: "Zaki", lastName: "oun", blocked: true),
Client(firstName: "oussama", lastName: "ali", blocked: false),
];
[/php]

 

3. A CircularProgressIndicator is shown if there is no data.

4. When the user clicks the checkbox the client will be blocked or unblocked according to the current state.

Now it’s very easy to add new features, for example, if you want to delete a client when the item is swiped, just wrap ListTile with a DismissibleWidget like this:

[php]
return Dismissible(
key: UniqueKey(),
background: Container(color: Colors.red),
onDismissed: (direction) {
DBProvider.db.deleteClient(item.id);
},
child: ListTile(…),
);
[/php]

 

For our OnDismissed function, we are using the Database provider to call the deleteClient method. For the argument, we are passing the item’s id.


Refactoring to use BLoC Pattern

We have done a lot in this article but in real world application, making state part of the UI isn’t really a good thing. Instead, we should always keep them separated.

There are a lot of patterns for managing state in Flutter but I will use BLoC in this article because it’s very flexible.

Create the BLoC :

[php]
class ClientsBloc {
ClientsBloc() {
getClients();
}
final _clientController = StreamController<List<Client>>.broadcast();
get clients => _clientController.stream;

dispose() {
_clientController.close();
}

getClients() async {
_clientController.sink.add(await DBProvider.db.getAllClients());
}
}
[/php]

 

Notes :

  • getClients will get the data from the Database (Client table) asynchronously. We will call this method whenever we update the table hence the reason for placing it into the constructor’s body
  • We StreamController<T>.broadcast constructor so that we are able to listen to the stream more than once. In our example, it doesn’t make much of a difference since we are only listening to the stream once but it is good to consider cases where you want to listen to the stream more than once.
  • Don’t forget to close your stream. This prevents us from getting memory leaks. In our example, we will close it using the dispose method of our StatefulWidget.

Now let’s add some methods to our block to interact with the database :

[php]
blockUnblock(Client client) {
DBProvider.db.blockOrUnblock(client);
getClients();
}

delete(int id) {
DBProvider.db.deleteClient(id);
getClients();
}

add(Client client) {
DBProvider.db.newClient(client);
getClients();
}

[/php]

 

And that’s all for our BLoC!

Our next step would be finding a way to provide our bloc to our widgets. We need a way to make the bloc accessible from different parts of the tree while also being able to free itself from memory when not in use.

For this, can take a look at thislibrary by Remi Rousselet .

In our case, the bloc is only going to be used by one widget so we can declare it and dispose of it from our stateful widget.

[php]
final bloc = ClientsBloc();

@override
void dispose() {
bloc.dispose();
super.dispose();
}
[/php]

 

Next, we need to use StreamBuilder instead of FutureBuilder. This is because we are now listening to a stream (clients stream) instead of a future.

[php]
StreamBuilder<List<Client>>(
stream: bloc.clients,

)
[/php]

 

The final step would be to refactor our code so that we are calling the methods from our bloc and not the database directly:

[php]
onDismissed: (direction) {
bloc.delete(item.id);
},
[/php]

 

Here is the final result

Finally, you can find the code source for this example in this repo (check the sqlite_demo_bloc branch to see the new version after refactoring ). I hope you enjoyed this article.

Credit : https://medium.com/flutter-community/using-sqlite-in-flutter-187c1a82e8b

Next Article : https://flutterappdev.com/2018/03/14/flutter-challenge-the-medium-app/

Leave a Comment