Building a Chat App with Kotlin and Firebase

In this tutorial, we will learn how to build a chat application using Kotlin and Firebase. Kotlin is a modern programming language that runs on the Java Virtual Machine (JVM) and is fully interoperable with Java. Firebase is a mobile and web application development platform that provides various tools and services, including a real-time database and user authentication. By combining Kotlin and Firebase, we can create a powerful and efficient chat app with real-time messaging and user authentication.

building chat app kotlin firebase

Introduction

What is Kotlin?

Kotlin is a statically typed programming language developed by JetBrains. It is designed to be concise, expressive, and safe. Kotlin can be used for developing Android apps, server-side applications, and more. With its modern features and seamless interoperability with Java, Kotlin has gained popularity among developers.

What is Firebase?

Firebase is a mobile and web application development platform provided by Google. It offers a range of tools and services that simplify the development process, including real-time database, authentication, cloud storage, and more. Firebase provides a scalable and reliable backend infrastructure, allowing developers to focus on building their applications.

Why use Kotlin and Firebase for building a chat app?

Kotlin and Firebase are a perfect combination for building a chat app. Kotlin's concise syntax and modern features make it easy to write clean and maintainable code. Firebase provides real-time database and user authentication services, which are essential for a chat app. With Firebase, we can easily handle real-time messaging, user authentication, and user presence tracking.

Setting up the Project

Creating a new Firebase project

Before we can start building our chat app, we need to create a new Firebase project. To do this, follow these steps:

  1. Go to the Firebase Console and sign in with your Google account.
  2. Click on the "Add Project" button and enter a name for your project.
  3. Select your country/region and agree to the terms of service.
  4. Click on the "Create Project" button to create your Firebase project.

Adding Firebase to the Kotlin project

Once you have created your Firebase project, you need to add Firebase to your Kotlin project. To do this, follow these steps:

  1. Open your Kotlin project in Android Studio.
  2. Click on the "Tools" menu and select "Firebase".
  3. In the Firebase Assistant panel, click on the "Authentication" option.
  4. Click on the "Connect to Firebase" button and select your Firebase project from the list.
  5. Follow the instructions to add the necessary Firebase dependencies to your project.

Configuring Firebase Authentication

To use Firebase Authentication in our chat app, we need to configure it. To do this, follow these steps:

  1. In the Firebase Console, go to the "Authentication" section.
  2. Click on the "Sign-in method" tab.
  3. Enable the "Email/Password" sign-in provider.
  4. Enable any other sign-in providers you want to support, such as Google or Facebook.
  5. Configure the necessary settings for each sign-in provider.

Designing the User Interface

Creating the chat screen layout

The first step in building our chat app is to design the user interface. We will start by creating the layout for the chat screen. To do this, create a new XML layout file and add the following code:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- Chat messages -->
    <ListView
        android:id="@+id/chatListView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:divider="@null"
        android:dividerHeight="0dp"
        android:stackFromBottom="true"/>

    <!-- Message input -->
    <EditText
        android:id="@+id/messageInputEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Type a message..."/>

    <!-- Send button -->
    <Button
        android:id="@+id/sendButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send"/>
</LinearLayout>

This layout consists of a ListView to display the chat messages, an EditText for the user to input messages, and a Button to send the messages.

Implementing user authentication UI

Next, we need to implement the user authentication UI. We will create a separate activity for the authentication process. To do this, create a new Kotlin class and add the following code:

class LoginActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // Initialize Firebase Authentication
        val auth = Firebase.auth

        // Set up the sign-in button click listener
        signInButton.setOnClickListener {
            val email = emailEditText.text.toString()
            val password = passwordEditText.text.toString()

            // Sign in with email and password
            auth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(this) { task ->
                    if (task.isSuccessful) {
                        // User signed in successfully
                        // Start the chat activity
                        startActivity(Intent(this, ChatActivity::class.java))
                        finish()
                    } else {
                        // Sign in failed
                        // Display an error message
                        Toast.makeText(
                            this, "Authentication failed.",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
        }
    }
}

This code sets up the sign-in button click listener and handles the authentication process using Firebase Authentication. If the user signs in successfully, it starts the chat activity. Otherwise, it displays an error message.

Handling user input and displaying messages

Now, we need to handle user input and display the chat messages. To do this, open the ChatActivity class and add the following code:

class ChatActivity : AppCompatActivity() {

    private lateinit var messagesAdapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        // Initialize the messages adapter
        messagesAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
        chatListView.adapter = messagesAdapter

        // Set up the send button click listener
        sendButton.setOnClickListener {
            val message = messageInputEditText.text.toString()

            // Add the message to the database
            val database = Firebase.database.reference
            val newMessageRef = database.child("messages").push()
            newMessageRef.setValue(message)

            // Clear the input field
            messageInputEditText.text.clear()
        }

        // Set up the database listener
        val database = Firebase.database.reference.child("messages")
        database.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // Get the message value from the snapshot
                val message = snapshot.getValue(String::class.java)

                // Add the message to the adapter
                if (message != null) {
                    messagesAdapter.add(message)
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // Not used in this tutorial
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })
    }
}

This code initializes the messages adapter, sets up the send button click listener to add messages to the database, and sets up a database listener to receive new messages in real-time. The messages are displayed in the ListView using the messages adapter.

Implementing Real-time Messaging

Setting up Firebase Realtime Database

To enable real-time messaging in our chat app, we need to set up the Firebase Realtime Database. To do this, follow these steps:

  1. In the Firebase Console, go to the "Database" section.
  2. Click on the "Create Database" button.
  3. Select the "Start in test mode" option and click on the "Next" button.
  4. Choose a location for your database and click on the "Done" button.

Sending and receiving messages

To send and receive messages in real-time, we need to update the code in the ChatActivity class. Replace the existing code with the following:

class ChatActivity : AppCompatActivity() {

    private lateinit var messagesAdapter: ArrayAdapter<String>
    private lateinit var database: DatabaseReference

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        // Initialize the messages adapter
        messagesAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
        chatListView.adapter = messagesAdapter

        // Get a reference to the database
        database = Firebase.database.reference.child("messages")

        // Set up the send button click listener
        sendButton.setOnClickListener {
            val message = messageInputEditText.text.toString()

            // Add the message to the database
            val newMessageRef = database.push()
            newMessageRef.setValue(message)

            // Clear the input field
            messageInputEditText.text.clear()
        }

        // Set up the database listener
        database.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // Get the message value from the snapshot
                val message = snapshot.getValue(String::class.java)

                // Add the message to the adapter
                if (message != null) {
                    messagesAdapter.add(message)
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // Not used in this tutorial
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })
    }
}

This code updates the database reference to point to the "messages" node in the Firebase Realtime Database. It also updates the send button click listener to push new messages to the database using the updated reference.

Updating the UI in real-time

To update the UI in real-time when new messages are added to the database, we need to modify the code in the ChatActivity class. Replace the existing code with the following:

class ChatActivity : AppCompatActivity() {

    private lateinit var messagesAdapter: ArrayAdapter<String>
    private lateinit var database: DatabaseReference

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        // Initialize the messages adapter
        messagesAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
        chatListView.adapter = messagesAdapter

        // Get a reference to the database
        database = Firebase.database.reference.child("messages")

        // Set up the send button click listener
        sendButton.setOnClickListener {
            val message = messageInputEditText.text.toString()

            // Add the message to the database
            val newMessageRef = database.push()
            newMessageRef.setValue(message)

            // Clear the input field
            messageInputEditText.text.clear()
        }

        // Set up the database listener
        database.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // Get the message value from the snapshot
                val message = snapshot.getValue(String::class.java)

                // Add the message to the adapter
                if (message != null) {
                    messagesAdapter.add(message)
                    messagesAdapter.notifyDataSetChanged()
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // Not used in this tutorial
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })
    }
}

This code adds a call to messagesAdapter.notifyDataSetChanged() after adding a new message to the adapter. This ensures that the ListView is updated immediately when a new message is received.

Adding User Authentication

Implementing email/password authentication

To implement email/password authentication in our chat app, we need to update the LoginActivity class. Replace the existing code with the following:

class LoginActivity : AppCompatActivity() {

    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // Initialize Firebase Authentication
        auth = Firebase.auth

        // Set up the sign-in button click listener
        signInButton.setOnClickListener {
            val email = emailEditText.text.toString()
            val password = passwordEditText.text.toString()

            // Sign in with email and password
            auth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(this) { task ->
                    if (task.isSuccessful) {
                        // User signed in successfully
                        // Start the chat activity
                        startActivity(Intent(this, ChatActivity::class.java))
                        finish()
                    } else {
                        // Sign in failed
                        // Display an error message
                        Toast.makeText(
                            this, "Authentication failed.",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
        }
    }
}

This code initializes the Firebase Authentication instance and sets up the sign-in button click listener. It uses the signInWithEmailAndPassword() method to authenticate the user using their email and password.

Integrating social media login

To integrate social media login in our chat app, we need to update the LoginActivity class. Replace the existing code with the following:

class LoginActivity : AppCompatActivity() {

    private lateinit var auth: FirebaseAuth
    private lateinit var googleSignInClient: GoogleSignInClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        // Initialize Firebase Authentication
        auth = Firebase.auth

        // Configure Google Sign-In
        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.default_web_client_id))
            .requestEmail()
            .build()
        googleSignInClient = GoogleSignIn.getClient(this, gso)

        // Set up the sign-in button click listener
        signInButton.setOnClickListener {
            val signInIntent = googleSignInClient.signInIntent
            startActivityForResult(signInIntent, RC_SIGN_IN)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == RC_SIGN_IN) {
            val task = GoogleSignIn.getSignedInAccountFromIntent(data)
            try {
                // Google Sign-In was successful
                val account = task.getResult(ApiException::class.java)
                firebaseAuthWithGoogle(account.idToken)
            } catch (e: ApiException) {
                // Google Sign-In failed
                Toast.makeText(
                    this, "Authentication failed.",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    private fun firebaseAuthWithGoogle(idToken: String?) {
        val credential = GoogleAuthProvider.getCredential(idToken, null)
        auth.signInWithCredential(credential)
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) {
                    // User signed in successfully
                    // Start the chat activity
                    startActivity(Intent(this, ChatActivity::class.java))
                    finish()
                } else {
                    // Sign in failed
                    // Display an error message
                    Toast.makeText(
                        this, "Authentication failed.",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
    }

    companion object {
        private const val RC_SIGN_IN = 9001
    }
}

This code configures the Google Sign-In options and sets up the sign-in button click listener to start the Google Sign-In flow. It uses the firebaseAuthWithGoogle() method to authenticate the user using their Google account.

Securing user data with Firebase Security Rules

To secure user data in our chat app, we need to set up Firebase Security Rules. To do this, follow these steps:

  1. In the Firebase Console, go to the "Database" section.
  2. Click on the "Rules" tab.
  3. Replace the existing rules with the following code:
{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    "messages": {
      "$messageId": {
        ".validate": "newData.hasChildren(['sender', 'text']) && newData.child('sender').isString() && newData.child('text').isString() && newData.child('sender').val() == auth.uid"
      }
    }
  }
}

These rules allow authenticated users to read and write data in the database. They also ensure that each message has a "sender" and "text" field, and the "sender" field matches the authenticated user's UID.

Handling User Presence

Tracking online/offline status

To track the online/offline status of users in our chat app, we need to update the code in the ChatActivity class. Replace the existing code with the following:

class ChatActivity : AppCompatActivity() {

    private lateinit var messagesAdapter: ArrayAdapter<String>
    private lateinit var database: DatabaseReference
    private lateinit var presenceRef: DatabaseReference
    private lateinit var connectedRef: DatabaseReference
    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        // Initialize the messages adapter
        messagesAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
        chatListView.adapter = messagesAdapter

        // Get a reference to the database
        database = Firebase.database.reference.child("messages")

        // Get a reference to the user's presence status
        auth = Firebase.auth
        presenceRef = Firebase.database.reference.child("users").child(auth.currentUser?.uid ?: "")

        // Get a reference to the connection status
        connectedRef = Firebase.database.reference.child(".info/connected")

        // Set up the send button click listener
        sendButton.setOnClickListener {
            val message = messageInputEditText.text.toString()

            // Add the message to the database
            val newMessageRef = database.push()
            newMessageRef.setValue(message)

            // Clear the input field
            messageInputEditText.text.clear()
        }

        // Set up the database listener
        database.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // Get the message value from the snapshot
                val message = snapshot.getValue(String::class.java)

                // Add the message to the adapter
                if (message != null) {
                    messagesAdapter.add(message)
                    messagesAdapter.notifyDataSetChanged()
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // Not used in this tutorial
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })

        // Set up the presence listener
        connectedRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val connected = snapshot.getValue(Boolean::class.java) ?: false

                if (connected) {
                    // User is online
                    presenceRef.setValue(true)
                    presenceRef.onDisconnect().setValue(false)
                } else {
                    // User is offline
                    presenceRef.setValue(false)
                }
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })
    }
}

This code adds a reference to the user's presence status and the connection status. It then sets up a listener for the connection status and updates the user's presence status accordingly.

Displaying user presence in the chat

To display the user presence in the chat, we need to update the code in the ChatActivity class. Replace the existing code with the following:

class ChatActivity : AppCompatActivity() {

    private lateinit var messagesAdapter: ArrayAdapter<String>
    private lateinit var database: DatabaseReference
    private lateinit var presenceRef: DatabaseReference
    private lateinit var connectedRef: DatabaseReference
    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat)

        // Initialize the messages adapter
        messagesAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1)
        chatListView.adapter = messagesAdapter

        // Get a reference to the database
        database = Firebase.database.reference.child("messages")

        // Get a reference to the user's presence status
        auth = Firebase.auth
        presenceRef = Firebase.database.reference.child("users").child(auth.currentUser?.uid ?: "")

        // Get a reference to the connection status
        connectedRef = Firebase.database.reference.child(".info/connected")

        // Set up the send button click listener
        sendButton.setOnClickListener {
            val message = messageInputEditText.text.toString()

            // Add the message to the database
            val newMessageRef = database.push()
            newMessageRef.setValue(message)

            // Clear the input field
            messageInputEditText.text.clear()
        }

        // Set up the database listener
        database.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                // Get the message value from the snapshot
                val message = snapshot.getValue(String::class.java)

                // Add the message to the adapter
                if (message != null) {
                    messagesAdapter.add(message)
                    messagesAdapter.notifyDataSetChanged()
                }
            }

            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onChildRemoved(snapshot: DataSnapshot) {
                // Not used in this tutorial
            }

            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {
                // Not used in this tutorial
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })

        // Set up the presence listener
        connectedRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val connected = snapshot.getValue(Boolean::class.java) ?: false

                if (connected) {
                    // User is online
                    presenceRef.setValue(true)
                    presenceRef.onDisconnect().setValue(false)
                } else {
                    // User is offline
                    presenceRef.setValue(false)
                }
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })

        // Set up the presence indicator
        val presenceRef = Firebase.database.reference.child("users").child(auth.currentUser?.uid ?: "")
        presenceRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val online = snapshot.getValue(Boolean::class.java) ?: false

                if (online) {
                    // User is online
                    presenceImageView.setImageResource(R.drawable.ic_online)
                } else {
                    // User is offline
                    presenceImageView.setImageResource(R.drawable.ic_offline)
                }
            }

            override fun onCancelled(error: DatabaseError) {
                // Not used in this tutorial
            }
        })
    }
}

This code adds a reference to the user's presence status and sets up a listener to update the presence indicator image view based on the user's online/offline status.

Conclusion

In this tutorial, we have learned how to build a chat app using Kotlin and Firebase. We started by setting up the project and configuring Firebase Authentication. Then, we designed the user interface for the chat screen and implemented real-time messaging using Firebase Realtime Database. We also added user authentication with email/password and social media login. Lastly, we implemented user presence tracking to display the online/offline status of users in the chat.

By following this tutorial, you have learned the basics of building a chat app with Kotlin and Firebase. You can now explore more advanced features and customize the app to suit your needs. Happy coding!