Skip to main content

Realtime Database

To start using the Realtime Database package within your project, import it at the top of your project files:

import 'package:firebase_database/firebase_database.dart';

Before using Realtime Database, you must first have ensured you have initialized FlutterFire.

To create a new Database instance, call the instance getter on FirebaseDatabase:

FirebaseDatabase database = FirebaseDatabase.instance;

By default, this allows you to interact with the Realtime Database using the default Firebase App used whilst installing FlutterFire on your platform. If however you'd like to use it with a secondary Firebase App, use the instanceFor method:

FirebaseApp secondaryApp = Firebase.app('SecondaryApp');
FirebaseDatabase database = FirebaseDatabase.instanceFor(app: secondaryApp);

Creating a reference#

Realtime Database stores data as JSON, however enables you to access nodes of the data via a DatabaseReference. For example, if your data is stored as the following:

{
"users": {
"123": {
"name": "John"
}
}
}

You can create a reference to a node by providing a path:

  • users: Creates a reference to the entire "users" object
  • users/123: Creates a reference to the "123" user object
  • users/123/name: Creates a reference to a property (with the value of "John")

To create a new DatabaseReference, pass the path to the ref method:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

If you do not provide a path, the reference will point to the root of your database.

The DatabaseReference provides some useful utilities to access sub-nodes, access the parent (if one exists) and modify data.

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Access a child of the current reference
DatabaseReference child = ref.child("name");
print(ref.key); // "123"
print(ref.parent!.key); // "users"

Read data#

The Realtime Database allows you to read data either once, or be notified on any changes to the node and it's children.

To read the data once, call the once method on a DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Get the data once
DatabaseEvent event = await ref.once();
// Print the data of the snapshot
print(event.snapshot.value); // { "name": "John" }

The result of a read is a DatabaseEvent. This contains a DataSnapshot of the data at the location of the reference, along with some additional metadata such as the reference key or whether the node exists in your database.

If you'd instead like to subscribe to realtime updates, the onValue method returns a Stream:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Get the Stream
Stream<DatabaseEvent> stream = ref.onValue;
// Subscribe to the stream!
stream.listen((DatabaseEvent event) {
print('Event Type: ${event.type}'); // DatabaseEventType.value;
print('Snapshot: ${event.snapshot}'); // DataSnapshot
});

Anytime data within the users/123 node changes, the Stream will fire a new event.

Querying#

Without specifying any query constraints, reading data will return all data within the node. If you wish to query your node for a subset of data, the package provides utilities to do so. For example, we could choose to order our data by an "age" property, and limit the returns returned:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");
Query query = ref.orderByChild("age").limitToFirst(10);
DataSnapshot event = await query.get();

Alternatively we could instead return users between certain ages by using startAt and endAt:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");
Query query = ref.orderByChild("age").startAt(18).endAt(30);
DataSnapshot event = await query.get();

Modifying data#

When modifying data we can either set the data (overwrite everything which exists at a node) or update specific parts of a node.

To set data, call the set method on a DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});

To update data, provide a Map where the keys point to specific nodes of the parent DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the name, leave the age and address!
await ref.update({
"age": 19,
});

The update method accepts a sub-path to nodes, allowing you to update multiple nodes on the database at once:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});

Removing data#

To remove data from your database, call the remove method (or set with a null value):

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.remove();

Removing a node will delete all data, including nested objects.

Running transactions#

When working with data that could be corrupted by concurrent modifications, such as incremental counters, you can use a transaction. A TransactionHandler takes the current state of the data as an argument and returns the new desired state you would like to write. If another client writes to the location before your new value is successfully written, your update function is called again with the new current value, and the write is retried.

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
_post['likes'] = (_post['likes'] ?? 0) + 1;
// Return the new data.
return Transaction.success(_post);
});

By default, events are raised each time the transaction update function runs. So if it is run multiple times, you may see intermediate states. You can set applyLocally to false to suppress these intermediate states and instead wait until the transaction has completed before events are raised:

await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);

The result of a transaction is a TransactionResult, which contains information such as whether the transaction was committed, and the new snapshot:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Aborting a transaction#

If you wish to safely abort a transaction from executing, you can throw a AbortTransactionException:

TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false

Atomic server-side operations#

Alternatively we're able to execute atomic operations on the server to ensure there is no chance of conflicts or out-of-sync updates.

For example, we can increment an age and set a timestamp on the server, rather than the client by using the ServerValue class:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.update({
// Increment the age on the server
"age": ServerValue.increment(1),
// Add a server generated timestamp
"createdAt": ServerValue.timestamp,
});

Offline Capabilities#

Firebase Database clients provide simple primitives that you can use to write to the database when a client disconnects from the Firebase Database servers. These updates occur whether the client disconnects cleanly or not, so you can rely on them to clean up data even if a connection is dropped or a client crashes. All write operations, including setting, updating, and removing, can be performed upon a disconnection.

First, create a new OnDisconnect instance on a DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
OnDisconnect onDisconnect = ref.onDisconnect();

Next apply any operations which should trigger when the user disconnects. For example, we can easily create a presence system by storing a boolean value on a user if they are online, and remove it if they go offline:

// Somewhere in your application, trigger that the user is online:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123/status");
await ref.set(true);
// And after;
// Create an OnDisconnect instance for your ref and set to false,
// the Firebase backend will then only set the value on your ref to false
// when your client goes offline.
OnDisconnect onDisconnect = ref.onDisconnect();
await onDisconnect.set(false);

The OnDisconnect instance supports set, setWithPriority, remove and update operations.

You can remove any committed operations at anytime by calling the cancel method:

// Cancel any previously committed operations
OnDisconnect onDisconnect = ref.onDisconnect();
await onDisconnect.cancel();

Emulator Usage#

If you are using the local Firestore emulators, then it is possible to connect to this using the useDatabaseEmulator method. Ensure you pass the correct port on which the Firebase emulator is running on.

Ensure you have enabled network connections to the emulators in your apps following the emulator usage instructions in the general FlutterFire installation notes for each operating system.

import 'package:flutter/foundation.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
bool isAndroid = defaultTargetPlatform == TargetPlatform.android && !kIsWeb;
// Ideal time to initialize
FirebaseDatabase.instance.useDatabaseEmulator(isAndroid ? '10.0.2.2' : 'localhost', 9000);
//...
}