Flutter - Using Firebase Realtime Database

This tutorial shows you how to use Firebase Realtime Database service in Flutter.

Firebase Realtime Database is a NoSQL cloud database that allows you to store and synchronize data in realtime. It's integrated with Firebase Authentication, so that you can use declarative security rules for data access. In addition, it also supports offline mode that allows data to be synced later when the device goes online. If you want to use the service in a Flutter application, you can read the explanation and examples in this tutorial.

Integrate Firebase to Flutter Application

Like using other Firebase services, you need to integrate your Firebase account with your Flutter application. Basically, what you need to do is create a Firebase project and add application(s) to the Firebase project. Then, download and copy the config files to your Flutter project. You can read the details in our tutorial about integration with Firebase services in Flutter.

To use Firebase Realtime Database in a Flutter application, you can use the firebase_database package. In addition, you also need to add firebase_core as a dependency. If you want to use Firebase Authentication, which can be used to authenticate and authorize data access, you also need to add firebase_auth as a dependency as well. Add the below dependencies to your pubspec.yaml file and run flutter pub get.

  dependencies:
    firebase_auth: ^1.2.0 # Needed if you want to use Firebase Authentication to restrict data access
    firebase_core: ^1.2.0
    firebase_database: ^6.1.2

Set Up Database

First of all, you need to set up the database from the Firebase Console. Navigate to BuildRealtime Database from the sidebar. If you haven't set up the database, you should see the Create database button.

Firebase Realtime Database

Click the button and you'll see a popup for setting up the database. In the first step, choose the location where the Realtime Database data will be stored using the dropdown.

Firebase Realtime Database - Set Up Location

Next, you can choose the initial rules for reading and writing data to the database. Just choose one of the options since you can edit it later. I'm going to explain about rules in the next section.

Firebase Realtime Database - Set Up Rules

After that, the database should be successfully created.

Edit Data

Firebase Realtime Database uses JSON as the data structure, with a node as the root. From the Firebase Console, you can edit the data. It's also possible to edit data from a Flutter application which is going to be explained later in this tutorial.

On the Data tab of Firebase Realtime Database, you can edit the JSON data. A JSON node can represent a leaf node or non-leaf node. For adding a leaf node, you can hover the cursor over the parent node and click the + symbol. Then, fill in the name and value fields for the new node.

Firebase Realtime Database - Add Data - Leaf Node

If the node you want to add has an object as its child, you only need to enter the name field of the node, while the value field should be left empty. Then, click the + symbol on the new object node for adding each field of the object.

Firebase Realtime Database - Add Data - Object

If the node you want to add represents an array, you only need to enter the name field of the node, while the value field should be left empty. For each array element, you need to click the + symbol on the new node. The value field should be field with the array element, while the name field should be filled with the array index.

Firebase Realtime Database - Add Data - Array

Set Rules

In some cases, you may want to authorize that only certain users can perform read or write data on a node. If you go to Rules tab, you should see a configuration in JSON format. The configuration can be used to add authentication and authorization rules, so that you can control who can access the data. In addition, you can also add validations and indexes. There is also a form that can be used for simulating read, set, and update operations based on the current rules.

There are some rule types that you can use:

  • .read: Whether the data is allowed to be read.
  • .write: Whether the data is allowed to be written.
  • .validate: Validation rule for the value.
  • .indexOn: Specifies the children to be indexed for ordering and querying.

To define a rule type, you need to add the rule type as a field in the configuration JSON. The JSON has hierarchical structure. The root's name must be rules. A rule type defined as the direct child of the root applies to all children. But you can also specify rules only for a specific path. For example, we have a data structure as shown below.

  {
    "applicationName": "woolha",
    "users": {
      "user01": {
        "id": "user01",
        "name": "Ivan Andrianto",
        "status": "Available"
      },
      "user02": {
        "id": "user02",
        "name": "Joko",,
        "status": "Unavailable"
      }
    },
    "categories": ["Category One", "Category Two"],
  }

And the following rules.

  {
    "rules": {
      ".read": true,
      "users": {
        "$uid": {
          ".write": true
        }
      }
    }
  }

Using the rules above, it means read operations are permitted for all children. There is also a specific rule which allows writing data on nodes whose path is users/{$uid}.

Authentication

You may want to limit only authenticated users are allowed to access the database. For authentication, you can utilize the Firebase Authentication. Therefore, in order to access the database, a user must be registered or signed in first. The Firebase Authentication supports some Authentication methods including anonymous, email and password, Facebook, and Google. I'm not going to explain Firebase Authentication deeply in this tutorial. But once a user is authenticated, the auth value (which is a special variable in rules) is not empty. Let's say you want to limit that all data can only be read by authenticated users only. It can be achieved by passing .read field whose value is "auth !== null".

  {
    "rules": {
      ".read": "auth !== null"
    }
  }

You can do the similar thing to apply .write rules. Understanding how Firebase applies the .read and .write is a bit tricky. If there are multiple .read or .write rules applied to a path, Firebase will evaluate the rules one by one from the outermost to the innermost. It will stop if a rule evaluates to true. With the below rules, read operation is allowed for users/${uid} path because the .read rule on line 3 evaluates to true.

  {
    "rules": {
      ".read": true,
      "users": {
        "$uid": {
          ".read": false
        }
      }
    }
  }

Read operation for users/${uid} path is also allowed using the below rules since the .read rule on line 6 evaluates to true.

  {
    "rules": {
      ".read": false,
      "users": {
        "$uid": {
          ".read": true
        }
      }
    }
  }

Authorization

Sometimes, it's necessary to restrict that a user can only write or update his own data. For example, using the data structure above, a user can only modify the data in users/{$uid} if the data represents the user, but not allowed to modify another user's data. If the user is authenticated using Firebase Authentication, the auth object will not be null. The auth object contains a property uid which is the Firebase Authentication ID for the user. Therefore, it would be easier to use the uid as the key.

  {
    "applicationName": "woolha",
    "users": {
      "GZhPWru2Mhfb0UYzZze2TW5B7hA3": {
        "id": "GZhPWru2Mhfb0UYzZze2TW5B7hA3",
        "name": "Ivan Andrianto",
        "status": "Available"
      },
      "M0SdljwvQTSjBAFj7DEMo91DWg52": {
        "id": "GZhPWru2Mhfb0UYzZze2TW5B7hA3",
        "name": "Joko",,
        "status": "Unavailable"
      }
    },
    "categories": ["Category One", "Category Two"],
  }

By using Firebase Authentication IDs as the keys, it's possible to use "$uid === auth.uid" to determine whether the operation is allowed.

  {
    "rules": {
      ".read": "auth !== null"
      "users": {
        "$uid": {
          ".write": "$uid === auth.uid"
        }
      }
    }
  }

Validation

For data consistency, you can also add validation before the data is updated by adding .validate rule. Unlike the .read and .write rules, if there are multiple .validate rules for a path, all must evaluate to true in order for the write to be allowed. The below example adds validation for the name field of objects in users/${uid}.

  {
    "rules": {
      "users": {
        "$uid": {
          ".read": "$uid === auth.uid",
          ".write": "$uid === auth.uid",
          "name": {
            ".validate": "newData.isString() && newData.val().length < 50"
          }
       }
      }
    }
  }

Index

As Firebase Realtime Database allows you to query and order the data, it can be important to specify indexes for fields that you use for filtering or ordering, especially if the data can be very big. You can use .indexOn rule to specify the children to be indexed.

  {
    "rules": {
      "users": {
        "$uid": {
          ".indexOn": ["name"]
        }
      }
    }
  }

Using Firebase Realtime Database in Flutter

After you have integrated Firebase to your Flutter application and adding firebase_database as a dependency, you should be able to use Firebase Realtime Database by adding the import below.

  import 'package:firebase_database/firebase_database.dart';

In order to perform data operations, you need to get the DatabaseReference object, which refers to a particular location (node) in the database. The DatabaseReference itself can be obtained from an instance of FirebaseDatabase. Therefore, you need to create the FirebaseDatabase instance first, using the constructor below.

  FirebaseDatabase({FirebaseApp app, String databaseURL})

The constructor has two arguments: app and databaseURL. The databaseURL argument should be passed if the app argument is passed. You can obtain the database URL from the Data tab of Firebase Realtime Database page in the Firebase Console. In order to get the FirebaseApp, first you need to import firebase_core

  import 'package:firebase_core/firebase_core.dart';

Then, you can call await Firebase.initializeApp() to get the FirebaseApp instance.

Since this tutorial is a bit complex, the code will be separated into multiple files. In this tutorial, we are going to save user data to the database. Below is the model that represents the user data.

models/user_data.dart

  class UserData {
    UserData({
      this.id,
      this.name,
      this.status,
    });
  
    String id;
    String name;
    String status;
  
    Map<String, Object> toMap() {
      return {
        'id': id,
        'name': name,
        'status': status,
      };
    }
  
    static UserData fromMap(Map value) {
      if (value == null) {
        return null;
      }
  
      return UserData(
        id: value['id'],
        name: value['name'],
        status: value['status'],
      );
    }
  
    @override
    String toString() {
      return ('{id: $id, name: $name, status: $status}');
    }
  }

The FirebaseApp needs to be initialized when the application starts. Besides for creating the FirebaseDatabase instance, it's also necessary for FirebaseAuth. Below is the main entry-point file which initializes the FirebaseApp. It uses FutureBuilder, so that the other widgets are created after the FirebaseApp has been initialized.

screens/main.dart

  import 'package:firebase_core/firebase_core.dart';
  import 'package:flutter/material.dart';
  
  import 'auth_form.dart';
  import 'data_form.dart';
  
  void main() => runApp(MyApp());
  
  class MyApp extends StatelessWidget {
  
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Woolha.com Flutter Tutorial',
        home: Main(),
        debugShowCheckedModeBanner: false,
      );
    }
  }
  
  class Main extends StatelessWidget {
  
    Future<FirebaseApp> _initFirebaseApp() async {
      return await Firebase.initializeApp();
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Woolha.com Flutter Tutorial'),
        ),
        body: SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.all(10.0),
            child: FutureBuilder(
              future: _initFirebaseApp(),
              builder: (
                BuildContext context,
                AsyncSnapshot<FirebaseApp> firebaseAppSnapshot,
              ) {
                if (firebaseAppSnapshot.hasData) {
                  return Column(
                    children: [
                      AuthForm(),
                      DataForm(firebaseApp: firebaseAppSnapshot.data),
                    ],
                  );
                } else if (firebaseAppSnapshot.hasError) {
                  return Text('Error');
                } else {
                  return CircularProgressIndicator();
                }
              },
            ),
          ),
        ),
      );
    }
  }
  

Below is a service class that handles calls and data synchronization with Firebase Realtime Database service. The constructor takes an argument of type FirebaseApp. It creates an instance of FirebaseDatabase by passing the app and databaseURL arguments. Having created the FirebaseDatabase instance, you can call its reference method to get the DatabaseReference. Later, we are going to add some methods in this class which perform various operations such as get, set, or remove data.

services/firebase_realtime_data_service.dart

  class FirebaseRealtimeDataService {
  
    DatabaseReference _db;
  
    FirebaseRealtimeDataService(FirebaseApp firebaseApp) {
_db = FirebaseDatabase( app: firebaseApp, databaseURL: 'https://woolha-fa3d2.firebaseio.com' ).reference(); } // put the methods here }

Get DatabaseReference for a Location

The DatabaseReference object represents a particular location (node) on the database. Before performing other operations, you need to know how to get the DatabaseReference for a particular location. If you know the location path, you can use the child method and pass the path as the argument.

  DatabaseReference child(String path)

The example below returns a DatabaseReference for the node whose location is users/GZhPWru2Mhfb0UYzZze2TW5B7hA3.

  _db.child('users/GZhPWru2Mhfb0UYzZze2TW5B7hA3')

To navigate to the parent location, you can use the parent method.

  DatabaseReference parent()

The example below returns a DatabaseReference for the node whose location is /users.

  _db.child('users/GZhPWru2Mhfb0UYzZze2TW5B7hA3')
    .parent()

You can go to the root by using the root method.

  DatabaseReference root()

Example:

  _db.child('users/GZhPWru2Mhfb0UYzZze2TW5B7hA3')
    .root()

DatabaseReference has some methods that can be used to perform various operations as shown below.

Set Data

The set method can be used to write value to a location with optional priority. If the node location doesn't exist, it will be created. If the location already exists, it will overwrite the old data including all child locations. If the passed value is null, the node will be removed. The supported data types are String, boolean, int, double, Map, and List.

  Future<void> set(dynamic value, {dynamic priority})

Example:

  Future<void> setUserData(String userId, String name, String status, double priority) async {
    final UserData userData = UserData(id: userId, name: name, status: status);
    await _db.child('users/$userId')
        .set(userData.toMap(), priority: priority);
  }

When the user perform a write operation while the device is offline, the request will be retried later when the device is online. It's already handled by the firebase_database package, so that you don't need to manually handle it.

Update Data

The update method can be used to update the node with the given value. Unlike set, it doesn't overwrite the location and its children. You cannot pass null value as well.

  Future<void> update(Map<String, dynamic> value)

Example:

  Future<void> updateUserData(String userId, String name, String status) async {
    final UserData userData = UserData(id: userId, name: name, status: status);
    await _db.child('users/$userId')
        .update(userData.toMap());
  }

Push Data

The push method can be used to generate a new child location using a unique key. It returns a DatabaseReference to the new location, which can be used to set the value at the new location.

  DatabaseReference push()

Example:

  Future<void> pushUserData(String userId, String name, String status) async {
    final UserData userData = UserData(id: userId, name: name, status: status);
    await _db.child('users/$userId')
        .push()
        .set(userData.toMap());
  }

Remove Data

The remove method can be used to remove data at a location along with its children.

  Future<void> remove()

Example:

  Future<void> removeUserData(String userId) async {
    await _db.child('users/$userId')
        .remove();
  }

Set Priority

To set the priority of the data at a location, you can use the setPriority method.

  Future<void> setPriority(dynamic priority)

Example:

  Future<void> setPriority(String userId, double priority) async {
    await _db.child('users/$userId')
        .setPriority(priority);
  }

Get Data

To listen for a single value event and then stop listening, you can use the once method. The return value is DataSnapshot, from which you can get the key and value properties. The type of the value property is dynamic. It depends on the retrieved data. For example, the type can be a _InternalLinkedHashMap if the value is an object, a List<dynamic> if the value is an array, or primitive types. If you are not sure what the type is, you can get the type by accessing the value's runtimeType property.

  Future<DataSnapshot> once()

Example:

  Future<UserData> getUserData(String userId) async {
    return await _db.child('users/$userId')
        .once()
        .then((result) {
      final LinkedHashMap value = result.value;
      return UserData.fromMap(value);
    });
  }

Order by Priority

To get the data ordered by priority, you can use Query's orderByPriority method.

  Query orderByPriority()

For all kinds of ordered queries, be careful not to use DataSnapshot's value to get the ordered result. That's because the value is already converted to a Map. Instead, you can use onChildAdded, which will be called every time a new child is added, and add the result to a List. The same approach should be used for other orderBy.... methods too.

  Future<List<UserData>> getUsersOrderByPriority() async {
    final List<UserData> orderedResult = [];
    final Query query = _db.child('users')
        .orderByPriority();

    query.onChildAdded.forEach((event) {
      orderedResult.add(UserData.fromMap(event.snapshot.value));
    });

    return await query.once()
        .then((ignored) => orderedResult);
  }

Order by Key

To get the data ordered by key, you can use Query's orderByKey method.

  Query orderByKey()

Example:

  Future<List<UserData>> getUsersOrderByKey() async {
    final List<UserData> orderedResult = [];
    final Query query = _db.child('users')
        .orderByKey();

    query.onChildAdded.forEach((event) {
      orderedResult.add(UserData.fromMap(event.snapshot.value));
    });

    return await query.once()
        .then((ignored) => orderedResult);
  }

Order by Value

To get the data ordered by value, you can use Query's orderByValue method.

  Query orderByValue()

Example:

  Future<List<UserData>> getUsersOrderByValue() async {
    final List<UserData> orderedResult = [];
    final Query query = _db.child('users')
        .orderByValue();

    query.onChildAdded.forEach((event) {
      orderedResult.add(UserData.fromMap(event.snapshot.value));
    });

    return await query.once()
        .then((ignored) => orderedResult);
  }

Order by Child

To get the data ordered by a child's key, you can use Query's orderByChild method.

  Query orderByChild(String key)

Example:

  Future<List<UserData>> getUsersOrderByChildName() async {
    final List<UserData> orderedResult = [];
    final Query query = _db.child('users')
        .orderByChild('name');

    query.onChildAdded.forEach((event) {
      orderedResult.add(UserData.fromMap(event.snapshot.value));
    });

    return await query.once()
        .then((ignored) => orderedResult);
  }

Handle Pagination

If there is a lot of data, perhaps it's better to use pagination and limit the query result so that the returned data is not too big. To get the first n results, you can use Query's limitToFirst method.

  Query limitToFirst(int limit)

For example, you can change the queries above to call limitToFirst method.

 final Query query = _db.child('users')
      .limitToFirst(100)
      .orderByPriority();

The opposite, if you want to get the last n results, you can use Query's limitToFirst method.

  Query limitToLast(int limit)

Example.

 final Query query = _db.child('users')
      .limitToLast(100)
      .orderByPriority();

For fetching the data on the next page, the data already fetched before should not be fetched again. You can use Query's startAt method, which returns only child nodes whose value is greater than or equal to the given value. The passed value's must be a String, bool, double, or int. If the data to be fetched is a list of key-value pairs of a JSON object, you need to use one of the keys as the value.

  Query startAt(dynamic value, {String key})

Example:

  final Query query = _db.child('users')
      .limitToLast(100)
      .startAt('GZhPWru2Mhfb0UYzZze2TW5B7hA3')
      .orderByPriority();

The opposite, you can use Query's endAt method, which returns only child nodes whose value is less than or equal to the given value. Example:

  final Query query = _db.child('users')
      .limitToLast(100)
      .endAt('GZhPWru2Mhfb0UYzZze2TW5B7hA3')
      .orderByPriority();

Another Query's method you may need to know is equalTo, which returns only child nodes whose value is equal to the given value.

    Query equalTo(dynamic value, {String key})

Example:

  final Query query = _db.child('users')
      .limitToLast(100)
      .equalTo('GZhPWru2Mhfb0UYzZze2TW5B7hA3')
      .orderByPriority();

Handle Data Synchronization

One of the advantages of using Firebase Realtime Database is its ability to handle data synchronization. You can add a listener that will be called every time the data on a particular location changes, either changed by another user or changed from the Firebase Console. The Query class has some methods to use.

  • Stream<Event> get onChildAdded: called every time a child is added.
  • Stream<Event> get onChildRemoved: called every time a child is removed.
  • Stream<Event> get onChildChanged: called every time a child is changed.
  • Stream<Event> get onChildMoved: called every time a child is moved.
  • Stream<Event> get onValue: called every time the data at the location is updated.

Below is a usage example for onChildChanged. After you get the Query object, you can add onChildChanged in the chain, which returns Event<Stream>. After that, you can process the received Events and do anything that you want. In this example, we call a callback function passed from another class, so that the class that uses the service can determine what to do when the data changes. The usage of the other methods is similar, so I'm not going to give examples one by one for each method.

  _userDetailChangedEvent = _db.child('users/GZhPWru2Mhfb0UYzZze2TW5B7hA3')
      .onChildChanged
      .map((event) => event.snapshot)
      .toList()
      .then((dataSnapshots) {
        _userDetailChangedCallback(dataSnapshots);
      });

Handle Authentication

Firebase Authentication is quite common to use for Firebase Realtime Database. As I have written above, it can be used to control that a particular data can only be accessed by authenticated or authorized users. In your Flutter application, you need to make the user authenticated. There are some authentication methods. In this tutorial, I am not going to explain about authentication in detail. For example, we are going to use email and password authentication method. But if you want to use other authentication method, the concept should be the same.

To use Firebase Authentication in Flutter, you can use the firebase_auth plugin. You need to add it in the dependency and run flutter pub get.

  dependencies:
    firebase_auth: ^1.2.0

After that, you can import it in the file where you want to use it.

  import 'package:firebase_auth/firebase_auth.dart';

First, you need to initialize FirebaseApp. In this tutorial, it's already done in the MainState class. After that, you need to get the instance of FirebaseAuth. It can be obtained by using FirebaseAuth.instance, which returns an instance using the default FirebaseApp. After getting the instance of FirebaseAuth, for authentication with email and password, the methods you need to call are createUserWithEmailAndPassword (register) and signInWithEmailAndPassword (sign in). After the user successfully registered or logged in, the requests to Firebase Realtime Database will also be authenticated too. Therefore, you can use the auth variable to define the rules. Below is a service that uses FirebaseAuthentication's email and password authentication method.

services/firebase_auth_service.dart

  import 'package:firebase_auth/firebase_auth.dart';
  
  class FirebaseAuthService {
  
    FirebaseAuth _firebaseAuth;
  
    FirebaseAuthService() {
      _firebaseAuth = FirebaseAuth.instance;
  
      FirebaseAuth.instance
          .authStateChanges()
          .listen((User user) {
        if (user == null) {
          print('User is currently signed out!');
        } else {
          print('User ${user.uid} is signed in!');
        }
      });
    }
  
    Future<String> authenticateUser({ String email, String password, bool isNewUser }) async {
      try {
        UserCredential userCredential;
  
        if (isNewUser) {
          userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
            email: email,
            password: password,
          );
        } else {
          userCredential = await _firebaseAuth.signInWithEmailAndPassword(
            email: email,
            password: password,
          );
        }
  
        return userCredential.user.uid;
      } catch (e) {
        print(e);
  
        return null;
      }
    }
  }

And below is a simple form for sign in or register that uses the service above.

screens/auth_form.dart

  import 'package:flutter/material.dart';
  import 'package:fluttersimpleapp/firebase_realtime_database/services/firebase_auth_service.dart';
  
  class AuthForm extends StatefulWidget {
  
    @override
    _AuthFormState createState() => _AuthFormState();
  }
  
  class _AuthFormState extends State<AuthForm> {
  
    final GlobalKey _formKey = GlobalKey<FormState>();
    final TextEditingController _email = TextEditingController();
    final TextEditingController _password = TextEditingController();
    final FirebaseAuthService _firebaseAuthService = new FirebaseAuthService();
  
    @override
    Widget build(BuildContext context) {
      return Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextFormField(
              key: Key('email'),
              controller: _email,
              decoration: InputDecoration(
                  labelText: 'Email'
              ),
            ),
            TextFormField(
              key: Key('password'),
              controller: _password,
              keyboardType: TextInputType.visiblePassword,
              decoration: InputDecoration(
                  labelText: 'Password'
              ),
            ),
            Row(
              children: [
                Expanded(
                  flex: 1,
                  child: OutlinedButton(
                    child: Text('Register'),
                    onPressed: () async {
                      await _firebaseAuthService.register(
                        _email.value.text,
                        _password.value.text,
                      );
                    },
                  ),
                ),
                SizedBox(width: 10),
                Expanded(
                  flex: 1,
                  child: OutlinedButton(
                    child: Text('Sign In'),
                    onPressed: () async {
                      await _firebaseAuthService.signIn(
                        _email.value.text,
                        _password.value.text,
                      );
                    },
                  ),
                )
              ],
            ),
          ],
        ),
      );
    }
  }

Create Form

Below is a form that makes use of the FirebaseRealtimeDatabaseService class above. You can try the operations above using this form.

screens/data_form.dart

  import 'package:firebase_core/firebase_core.dart';
  import 'package:flutter/material.dart';
  import 'package:fluttersimpleapp/firebase_realtime_database/models/user_data.dart';
  import 'package:fluttersimpleapp/firebase_realtime_database/services/firebase_realtime_database_service.dart';
  
  class DataForm extends StatefulWidget {
  
    final FirebaseApp firebaseApp;
  
    DataForm({Key key, @required this.firebaseApp}) : super(key: key);
  
    @override
    _DataFormState createState() => _DataFormState(this.firebaseApp);
  }
  
  class _DataFormState extends State<DataForm> {
  
    static final List<String> _operations = [
      'setUserData',
      'updateUserData',
      'pushUserData',
      'setPriority',
      'removeUserData',
      'getUserData',
      'getUsersOrderByPriority',
      'getUsersOrderByKey',
      'getUsersOrderByValue',
      'getUsersOrderByChildName',
    ];
  
    final GlobalKey _dataFormKey = GlobalKey<FormState>();
    final TextEditingController _id = TextEditingController();
    final TextEditingController _name = TextEditingController();
    final TextEditingController _priority = TextEditingController();
    final TextEditingController _startAt = TextEditingController();
    final TextEditingController _status = TextEditingController();
  
    String _operation = 'setUserData';
    String _result = '';
    FirebaseRealtimeDatabaseService _realtimeDatabaseService;
  
    _DataFormState(FirebaseApp firebaseApp) {
      _realtimeDatabaseService = FirebaseRealtimeDatabaseService(
        firebaseApp: firebaseApp,
      );
    }
  
    Future<void> _handleSetUserData() async {
      await _realtimeDatabaseService.setUserData(
        _id.value.text,
        _name.value.text,
        _status.value.text,
        double.parse(_priority.value.text),
      );
  
      _result = 'Set data success.';
    }
  
    Future<void> _handleUpdateUserData() async {
      await _realtimeDatabaseService.updateUserData(
        _id.value.text,
        _name.value.text,
        _status.value.text,
      );
  
      _result = 'Update data success.';
    }
  
    Future<void> _handlePushUserData() async {
      await _realtimeDatabaseService.pushUserData(
        _id.value.text,
        _name.value.text,
        _status.value.text,
      );
  
      _result = 'Push data success.';
    }
  
    Future<void> _handleSetPriority() async {
      await _realtimeDatabaseService.setPriority(
        _id.value.text,
        double.parse(_priority.value.text),
      );
  
      _result = 'Update data success.';
    }
  
    Future<void> _handleRemoveUserData() async {
      await _realtimeDatabaseService.removeUserData(_id.value.text);
  
      _result = 'Remove data success.';
    }
  
    Future<void> _handleGetUserData() async {
      final UserData userData = await _realtimeDatabaseService.getUserData(_id.value.text);
  
      _result = userData.toString();
    }
  
    Future<void> _handleGetUsersOrderByPriority({ String startAt }) async {
      final List<UserData> users = await _realtimeDatabaseService
          .getUsersOrderByPriority(startAt: startAt);
  
      _result = users.toString();
    }
  
    Future<void> _handleGetUsersOrderByKey({ String startAt }) async {
      final List<UserData> users = await _realtimeDatabaseService
          .getUsersOrderByKey(startAt: startAt);
  
      _result = users.toString();
    }
  
    Future<void> _handleGetUsersOrderByValue({ String startAt }) async {
      final List<UserData> users = await _realtimeDatabaseService
          .getUsersOrderByValue(startAt: startAt);
  
      _result = users.toString();
    }
  
    Future<void> _handleGetUsersOrderByChildName({ String startAt }) async {
      final List<UserData> users = await _realtimeDatabaseService
          .getUsersOrderByChildName(startAt: startAt);
  
      _result = users.toString();
    }
  
    Future<void> _handleSubmit() async {
      try {
        switch (_operation) {
          case 'setUserData':
            await _handleSetUserData();
            break;
          case 'updateUserData':
            await _handleUpdateUserData();
            break;
          case 'pushUserData':
            await _handlePushUserData();
            break;
          case 'setPriority':
            await _handleSetPriority();
            break;
          case 'removeUserData':
            await _handleRemoveUserData();
            break;
          case 'getUserData':
            await _handleGetUserData();
            break;
          case 'getUsersOrderByPriority':
            await _handleGetUsersOrderByPriority(startAt: _startAt.text);
            break;
          case 'getUsersOrderByKey':
            await _handleGetUsersOrderByKey(startAt: _startAt.text);
            break;
          case 'getUsersOrderByValue':
            await _handleGetUsersOrderByValue(startAt: _startAt.text);
            break;
          case 'getUsersOrderByChildName':
            await _handleGetUsersOrderByChildName(startAt: _startAt.text);
            break;
        }
      } catch (e) {
        _result = 'Error: ${e.toString()}.';
      }
  
      setState(() {});
    }
  
    @override
    Widget build(BuildContext context) {
      return Form(
        key: _dataFormKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            DropdownButton<String>(
              items: _operations.map((String value) {
                return new DropdownMenuItem<String>(
                  value: value,
                  child: new Text(value),
                );
              }).toList(),
              value: _operation,
              onChanged: (operation) {
                setState(() { _operation = operation; });
              },
            ),
            Visibility(
              visible: <String>['setUserData', 'updateUserData', 'removeUserData', 'setPriority']
                  .contains(_operation),
              child: TextFormField(
                key: Key('id'),
                controller: _id,
                decoration: InputDecoration(labelText: 'ID'),
              ),
            ),
            Visibility(
              visible: <String>['setUserData', 'updateUserData'].contains(_operation),
              child: TextFormField(
                key: Key('name'),
                controller: _name,
                decoration: InputDecoration(labelText: 'Name'),
              ),
            ),
            Visibility(
              visible: <String>['setUserData', 'updateUserData'].contains(_operation),
              child: TextFormField(
                key: Key('status'),
                controller: _status,
                decoration: InputDecoration(labelText: 'Status'),
              ),
            ),
            Visibility(
              visible: <String>['setUserData', 'setPriority'].contains(_operation),
              child: TextFormField(
                key: Key('priority'),
                controller: _priority,
                decoration: InputDecoration(labelText: 'Priority'),
              ),
            ),
            Visibility(
              visible: <String>[
                'getUsersOrderByPriority',
                'getUsersOrderByKey',
                'getUsersOrderByValue',
                'getUsersOrderByChildName',
              ].contains(_operation),
              child: TextFormField(
                key: Key('startAt'),
                controller: _startAt,
                decoration: InputDecoration(labelText: 'Start At'),
              ),
            ),
            OutlinedButton(
              child: Text('Submit'),
              onPressed: _handleSubmit,
            ),
            Text(_result),
          ],
        ),
      );
    }
  }

Full Code

For this tutorial, the code can be downloaded here.

Summary

To use Firebase Realtime Database, first you need to connect your Firebase account with your Flutter application. After that, you can set up the database from the Firebase Console and modify the rules. To access Firebase Realtime Database from a Flutter application, you can use the firebase_database package. It already provides the main functionalities such as write, remove, and get data as well as data synchronization and offline mode support. If you need to authenticate and authorize data access, you can utilize the Firebase Authentication.

You can also read our tutorials about: