Categories:
Loading... Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?
Managing user presence in Cloud Firestore using Flutter

Managing user presence in Cloud Firestore using Flutter

Nov 28, 2020

Codemagic builds and tests your app after every commit, notifies selected team members and releases to the end user. Automatically. Get started

Written by Souvik Biswas

While building an application which deals with messaging, the most important part is keeping track whether they are online/offline (formally known as User Presence). But, if you are using Firebase Cloud Firestore as your database, then handling this is pretty challenging.

In this article, I will show you how to manage user presence in a Flutter app using Cloud Firestore as it’s database.

We will be using Realtime Database and Cloud Functions to achieve this.

Disclaimer: Knowledge of JavaScript is recommended, as you have to write Cloud Functions using JS.

Sample app

The app that we will be building for this demonstration will contain a simple Google Sign In for the authentication of users on Firebase. Firestore database will be used for storing some user-specific information. User presence and last seen time will be tracked using a combination of Realtime Database, Cloud Functions and Firestore.

Presence tracking in action

Firebase set up

As we are using Firebase as the backend for our app, it is required to complete the Firebase set up first.

You can follow this article in order to complete the Firebase and Google Sign setup.

Some brief about the steps you need to follow:

  • Create a new project using Firebase console.

  • Configure the Android & iOS app to use Firebase.

  • Add your support email by going to the Firebase project settings.

  • Enable Google Sign In on Firebase Authentication.

  • Complete the platform-specific configuration to use Google Sign In.

  • Enable Cloud Firestore and Realtime Database.

  • Specify Security rules for both of them.

    For now, you can just define the security rules, so that it checks if the user is authenticated:

    // Cloud Firestore security rules
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /{document=**} {
          allow read, write: if request.auth != null;
        }
      }
    }
    
    // Realtime Database security rules
    {   
    "rules": {     
    ".read": "auth != null",
     ".write": "auth != null"  
     } 
    }
    

    If you are working on a production app, don’t forget to modify these security rules for better protection.

Let’s start building the app now.

In this tutorial, I will focus on the major functionalities of the app. So, if you want to see the UI code of the app, check out my GitHub repo (present at the end of this article).

Required plugins

Before diving into code, add the plugins required for building the app to your pubspec.yaml file:

Authenticating user

We will define two methods for authenticating an user to Firebase using Google Sign In. Create a new file called authentication.dart, and add the following methods:

  • signInWithGoogle()
  • signOutGoogle()
  • getUser()

In the signInWithGoogle() method, we will first initialize the Firebase app, and then retrieve the credentials from the Google account to authenticate the user in Firebase.

The signOutGoogle() method will help the user to sign out of the currently logged in Google account.

You can define one more method, called getUser() to retrieve the user information when an already logged in user returns back to the app (also known as auto-login).

I am not going deep into these methods, you can check out this article for a more detailed explanation (with code snippet), or find the GitHub repo of this app at the end of this article.

Storing data on Firestore

Some of general user information, and most importantly user presence and last seen time should be stored in the Cloud Firestore. Create a new file called database.dart, we will be defining two basic methods there:

  • storeUserData(): To upload the user information to the Firestore when a new user logs in.
  • retrieveUsers(): To retrieve all user details from Firestore.

The database structure of Firestore will be like this:

Inside a class called Database, we will first store a reference to the main collection of Firestore.

class Database {
  /// The main Firestore user collection
  final CollectionReference userCollection = FirebaseFirestore.instance.collection('users');

  storeUserData({@required String userName}) async {}

  Stream<QuerySnapshot> retrieveUsers() {}
}

Let’s define the storeUserData() method which will store the user details under a document according to their uid.

storeUserData({@required String userName}) async {
  DocumentReference documentReferencer = userCollection.doc(uid);

  User user = User(
    uid: uid,
    name: userName,
    presence: true,
    lastSeenInEpoch: DateTime.now().millisecondsSinceEpoch,
  );

  var data = user.toJson();

  await documentReferencer.set(data).whenComplete(() {
    print("User data added");
  }).catchError((e) => print(e));
}

Here, you can use a model class to easily handle the data. In this example, I am using a model class called User:

class User {
  String uid;
  String name;
  bool presence;
  int lastSeenInEpoch;

  User({
    @required this.uid,
    @required this.name,
    @required this.presence,
    @required this.lastSeenInEpoch,
  });

  User.fromJson(Map<String, dynamic> json) {
    uid = json['uid'];
    name = json['name'];
    presence = json['presence'];
    lastSeenInEpoch = json['last_seen'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();

    data['uid'] = this.uid;
    data['name'] = this.name;
    data['presence'] = this.presence;
    data['last_seen'] = this.lastSeenInEpoch;

    return data;
  }
}

In order to retrieve the list of all users, you can define the retrieveUsers() method like this:

Stream<QuerySnapshot> retrieveUsers() {
  Stream<QuerySnapshot> queryUsers = userCollection
      .orderBy('last_seen', descending: true)
      .snapshots();

  return queryUsers;
}

Handling user presence

The problem with Cloud Firestore is, it doesn’t provide any method using which you can track when the user navigates away from this app or terminates it. But, another Firebase service called Realtime Database provide a dedicated method to track user presence, named as onDisconnect(). Whenever the client loses the connection to the database, this gets triggered automatically.

Define a new method inside the database.dart file called updateUserPresence(). This method will store the user presence and last seen time to the Firebase Realtime Database.

class Database {
  final DatabaseReference databaseReference = FirebaseDatabase.instance.reference();

  updateUserPresence() async {
    Map<String, dynamic> presenceStatusTrue = {
      'presence': true,
      'last_seen': DateTime.now().millisecondsSinceEpoch,
    };

    await databaseReference
        .child(uid)
        .update(presenceStatusTrue)
        .whenComplete(() => print('Updated your presence.'))
        .catchError((e) => print(e));

    Map<String, dynamic> presenceStatusFalse = {
      'presence': false,
      'last_seen': DateTime.now().millisecondsSinceEpoch,
    };

    databaseReference.child(uid).onDisconnect().update(presenceStatusFalse);
  }
}

You may also skip storing the last_seen property on Realtime Database, because we can directly set it using Cloud Functions in the later step.

As the onDisconnect() method gets triggered, the presence status will be updated on the Realtime Database.

Syncing user presence

Now coming to the most important part of the article, synchronizing user presence information present on the Realtime database to the Cloud Firestore. You can use an awesome service provided by Firebase called Cloud Functions, which helps to run backend code on Google’s servers in response to the events triggered by Firebase features.

In order to write and deploy Cloud Functions, you will need to initialize Firebase using Firebase CLI tools:

  • Download and install node.js.

  • Install Firebase CLI by running the following command:

    npm install -g firebase-tools
    
  • Login to your Firebase account:

    firebase login
    
  • Initialize Firebase by running the following inside the project directory:

    firebase init
    
  • Choose Functions: Configure and deploy Cloud Functions from the list.

  • In the Project Setup section, select Use an existing project and choose the project from the list.

  • In the Functions Setup section, select the language as JavaScript, and enable ESLint.

  • When prompted to install dependencies, choose yes.

You might see a message saying “high severity vulnerabilities”, but just ignore them. This is shown as Firebase CLI uses node.js 8 by default.

Also, node.js 8 is deprecated now, and they will stop supporting it on Firebase soon.

But in order to use node.js 10, you will need to upgrade your Firebase project to their Blaze Plan. So, we will be using node.js 8 for our project, but the same cloud function will also work while using node.js 10.

This completes the Firebase initialization. You will now see some new files created inside your root project directory.

Navigate to the functions > index.js present inside your root project directory. Here, you can define the cloud function.

We just need to update the presence and last_seen field of the Cloud Firestore according to the value present in the Realtime Database. You can achieve that as follows:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const firestore = admin.firestore();

exports.onUserStatusChange = functions.database
  .ref("/{uid}/presence")
  .onUpdate(async (change, context) => {
    // Get the data written to Realtime Database
    const isOnline = change.after.val();

    // Get a reference to the Firestore document
    const userStatusFirestoreRef = firestore.doc(`users/${context.params.uid}`);
    
    console.log(`status: ${isOnline}`);

    // Update the values on Firestore
    return userStatusFirestoreRef.update({
      presence: isOnline,
      last_seen: Date.now(),
    });
  });

Now, you can deploy this Cloud Function by running:

firebase deploy

Upon successful deployment, this will get triggered as soon as the user presence status changes in the Realtime Database.

Conclusion

Cloud Functions makes it a lot easier to run backend code in an effectively serverless environment. You can trigger them using other Firebase services or directly using an HTTP request. Using Cloud Functions and Realtime Database, user presence can be tracked and updated seamlessly on Cloud Firestore.

References


Souvik Biswas is a passionate Mobile App Developer (Android and Flutter). He has worked on a number of mobile apps throughout his journey. Loves open source contribution on GitHub. He is currently pursuing a B.Tech degree in Computer Science and Engineering from Indian Institute of Information Technology Kalyani. He also writes Flutter articles on Medium - Flutter Community.

How did you like this article?

Oops, your feedback wasn't sent

Latest articles

Show more posts