Flutter ISAR Database

Murat Taksim
8 min readDec 5, 2023

--

Isar is a fast and user-friendly NoSQL database designed for Flutter. It is known for its simple API, cross-platform support, and low disk usage. Isar offers various features such as automatic JSON serialization/deserialization, optional encryption, and the ability to perform complex queries.

Benefits of Isar:

  1. Performance: Isar provides fast access to data, optimized for both read and write operations.
  2. Type Safety: Utilizing Dart’s static type system, Isar catches errors at compile time.
  3. Flexible Querying: Isar offers flexible querying capabilities, enabling easy execution of complex data operations.
  4. Ease of Use: Isar’s API is simple and comprehensible, promoting developer productivity in a short timeframe.
  5. Local Storage: Isar stores data locally on the device, allowing access to data without the need for an internet connection.

Limitations of Isar:

  1. Database Size: The amount of data Isar can store is limited based on local storage capacity.
  2. Backup and Restore: Backup and restore operations with Isar need to be manually managed.
  3. Adoption: Isar is a relatively new library, so community support and resources may not be as extensive as other popular databases.
  4. Encryption: Isar does not currently support encryption, yet.

Flutter User Guide:

  1. Setup:

In pubspec.yaml file, we need to define the packages:

dependencies:
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0
path_provider: ^2.1.1

dev_dependencies:
isar_generator: ^3.1.0
build_runner: ^2.4.7

Then, run the following command on terminal for adding dependencies to the project:

flutter pub get

2. Open Database:
Firstly, we define the User model has name (String?) and age (int?) as user.dart as follows:

import 'package:isar/isar.dart';
part 'user.g.dart';

@collection
class User {
User(this.name, this.age);
Id id = Isar.autoIncrement;
String? name;
int? age;
}

We use @collection for model definition and id = Isar.autoIncrement for our database, as the important points.

We define OpenDB() for database usage to open Isar and return an instance of it for getting ready state to use for database CRUD (Create, Read, Update and Delete) operations:

  late Future<Isar> db;
//we define db that we want to use as late

IsarService() {
db = openDB();
//open DB for database usage.
}
  Future<Isar> openDB() async {
var dir = await getApplicationDocumentsDirectory();
// to get application directory information

if (Isar.instanceNames.isEmpty) {
return await Isar.open(
//open isar
[UserSchema],
//user.g.dart includes the schemes that we need to define here - it can be single or multiple.
directory: dir.path,
);
}

return Future.value(Isar.getInstance());
// return instance of Isar - it makes the isar state Ready for Usage for adding/deleting operations.
}

Then, run the flutter pub run build_runner build to create g.dart files (ex: user.g.dart) with — delete-conflicting-outputs parameter to avoid build conflicts as following command:

flutter pub run build_runner build --delete-conflicting-outputs

3. ISAR Database Services:

//Save a new user to the Isar database.
Future<void> saveUser(User newUser) async {
final isar = await db;
//Perform a synchronous write transaction to add the user to the database.
isar.writeTxnSync(() => isar.users.putSync(newUser));
}

//Listen to changes in the user collection and yield a stream of user lists.
Stream<List<User>> listenUser() async* {
final isar = await db;
//Watch the user collection for changes and yield the updated user list.
yield* isar.users.where().watch(fireImmediately: true);
}

//Retrieve all users from the Isar database.
Future<List<User>> getAllUser() async {
final isar = await db;
//Find all users in the user collection and return the list.
return await isar.users.where().findAll();
}

// Update an existing user in the Isar database.
Future<void> UpdateUser(User user) async {
final isar = await db;
await isar.writeTxn(() async {
//Perform a write transaction to update the user in the database.
await isar.users.put(user);
});
}

//Delete a user from the Isar database based on user ID.
Future<void> deleteUser(int userid) async {
final isar = await db;
//Perform a write transaction to delete the user with the specified ID.
isar.writeTxn(() => isar.users.delete(userid));
}

//Filter users based on name containing "test" and age equal to 45. It can be (should be) modified as dynamic.
Future<User?> filterName() async {
final isar = await db;
//Use the Isar query API to filter users based on specific criteria and return the first matching result.
final favorites = await isar.users
.filter()
.nameContains("test")
.ageEqualTo(45)
.findFirst();
return favorites;
}

After we combine these then we have isar_service.dart file:

import 'package:path_provider/path_provider.dart';
import 'package:isar/isar.dart';
import '../models/user.dart';

class IsarService {
late Future<Isar> db;
//we define db that we want to use as late
IsarService() {
db = openDB();
//open DB for use.
}
//Save a new user to the Isar database.
Future<void> saveUser(User newUser) async {
final isar = await db;
//Perform a synchronous write transaction to add the user to the database.
isar.writeTxnSync(() => isar.users.putSync(newUser));
}

//Listen to changes in the user collection and yield a stream of user lists.
Stream<List<User>> listenUser() async* {
final isar = await db;
//Watch the user collection for changes and yield the updated user list.
yield* isar.users.where().watch(fireImmediately: true);
}

//Retrieve all users from the Isar database.
Future<List<User>> getAllUser() async {
final isar = await db;
//Find all users in the user collection and return the list.
return await isar.users.where().findAll();
}

// Update an existing user in the Isar database.
Future<void> UpdateUser(User user) async {
final isar = await db;
await isar.writeTxn(() async {
//Perform a write transaction to update the user in the database.
await isar.users.put(user);
});
}

//Delete a user from the Isar database based on user ID.
Future<void> deleteUser(int userid) async {
final isar = await db;
//Perform a write transaction to delete the user with the specified ID.
isar.writeTxn(() => isar.users.delete(userid));
}

//Filter users based on name containing "test" and age equal to 45. It can be (should be) modified as dynamic.
Future<User?> filterName() async {
final isar = await db;
//Use the Isar query API to filter users based on specific criteria and return the first matching result.
final favorites = await isar.users
.filter()
.nameContains("test")
.ageEqualTo(45)
.findFirst();
return favorites;
}

Future<Isar> openDB() async {
var dir = await getApplicationDocumentsDirectory();
// to get application directory information
if (Isar.instanceNames.isEmpty) {
return await Isar.open(
//open isar
[UserSchema],
directory: dir.path,
// user.g.dart includes the schemes that we need to define here - it can be multiple.
);
}
return Future.value(Isar.getInstance());
// return instance of Isar - it makes the isar state Ready for Usage for adding/deleting operations.
}
}

Lastly, our main.dart file as follows:

// Import necessary packages and files
import 'package:flutter/material.dart';
import 'local_db/isar_service.dart';
import 'models/user.dart';

// Main function to run the Flutter application
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

// Main application widget
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
// MaterialApp configuration
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'User App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}

// Home page widget
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

// Service instance for Isar database operations
IsarService isarService = IsarService();

// Text editing controllers for user input
late TextEditingController _controller;
late TextEditingController _controller2;

// State class for the home page
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
// Initialize text editing controllers
_controller = TextEditingController();
_controller2 = TextEditingController();
super.initState();
}

// Form key for form validation
final _formKey = GlobalKey<FormState>();

@override
Widget build(BuildContext context) {
// Scaffold widget for the app's structure
return Scaffold(
appBar: AppBar(
title: const Text("User App"),
centerTitle: true,
actions: [
// Add user button
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: InkWell(
onTap: () {
// Show modal for adding a new user
showModel("add", User("", 0));
},
child: const Icon(
Icons.add,
size: 35,
),
),
)
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: StreamBuilder<List<User>>(
stream: isarService.listenUser(),
builder: (context, snapshot) => snapshot.hasData
? InkWell(
onTap: () async {
// Filter and print user data
final a = await isarService.filterName();
print(a!.name);
},
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
// User icon
Icon(
Icons.person_4_outlined,
size: 50,
),
// User name
Text(snapshot.data![index].name!),
Row(
children: [
// Update user icon
InkWell(
onTap: () {
// Prepare data for updating user
_controller.text =
snapshot.data![index].name!;
_controller2.text = snapshot
.data![index].id
.toString();
showModel(
"update",
snapshot.data![index]);
},
child: Icon(
Icons.autorenew_outlined,
size: 30,
),
),
SizedBox(
width: 10,
),
// Delete user icon
InkWell(
onTap: () async {
// Delete user
await isarService.deleteUser(
snapshot.data![index].id);
},
child: Icon(
Icons.delete,
size: 30,
),
),
],
),
],
),
),
);
},
),
)
: const Text("Data Not Found."),
),
),
],
),
);
}

// Show modal for adding or updating user
showModel(deger, User user) {
showModalBottomSheet(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(25.0)),
),
context: context,
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom),
child: Container(
height: 300,
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Close modal button
InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Icon(
Icons.close,
size: 40,
),
)
],
),
SizedBox(
height: 10,
),
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User name input
TextFormField(
controller: _controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Name',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Empty!';
}
return null;
},
),
SizedBox(
height: 10,
),
// User age input
TextFormField(
controller: _controller2,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Age',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Empty!';
}
return null;
},
),
// Add or update user button
deger == "add"
? Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0),
child: ElevatedButton(
onPressed: () async {
// Save new user and close modal
await isarService.saveUser(User(
_controller.text,
int.parse(_controller2.text)));
Navigator.of(context).pop();
_controller.text = "";
_controller2.text = "";
},
child: const Text('Submit'),
),
)
: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0),
child: ElevatedButton(
onPressed: () async {
// Update user and close modal
user.name = _controller.text;
user.age = int.parse(_controller2.text);
await isarService.UpdateUser(user);
Navigator.of(context).pop();
_controller.text = "";
_controller2.text = "";
},
child: const Text('Submit'),
),
),
],
),
)
],
),
),
),
);
},
);
}
}

4. Inspecting Data in Debug Mode:

When we run the application with debug mode, we will see a URL to observe our database in the console output. We can browse the data in our database by clicking or pasting this URL into a web browser. This is a useful tool to see the collections in the database and the data in those collections.

For more information:

Github Link: https://github.com/mtengineer90/isar_nosql_flutter
ISAR Documentation: https://isar.dev/

--

--