Open In App

Chat Application in Android Jetpack Compose

Last Updated : 21 Oct, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Chat Application are common application across Android world leading tech companies are directly or indirectly involved in such kind of Application. In this article, we will Create a Chat Application Using Android Jetpack Compose and integrate it with Firebase to enable real-time Message exchange.

Prerequisites

Before proceeding with this tutorial, make sure you have:

  • Basic knowledge of Kotlin and Jetpack Compose.
  • Android Studio (Arctic Fox or later) Installed.
  • Firebase project setup with Firestore or Realtime Database / Email and password based Authentication.

Project Setup

Step 1: Create a New Android Project

  1. Open Android Studio.
  2. Click on New Project > Select Empty Compose Activity.
  3. Set the Application Name (e.g., FirebaseChatApp).
  4. Select Kotlin as the programming language.
  5. Click Finish to create the project.

Step 2: Add Firebase Dependencies

To connect your application with Firebase, you need to add Firebase SDK dependencies.

Open your app-level build.gradle and add the following dependencies:

Kotlin
dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)

    // Jetpack Compose
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)

    // Material Design
    implementation(libs.androidx.material3)

    // Firebase dependencies
    implementation(platform("com.google.firebase:firebase-bom:33.4.0"))
    implementation("com.google.firebase:firebase-firestore") // Firestore
    implementation("com.google.firebase:firebase-auth-ktx")
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)


    // Testing
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)

    // Debugging & UI Tooling
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}


Step 3: Configure Firebase

  1. Go to the Firebase Console.
  2. Create a new Firebase project.
  3. Add an Android app to your project, and follow the steps to register the app.
  4. Download the google-services.json file and place it in your Android project’s app/ directory.
  5. In your build.gradle (project-level), add the following:
// Top-level build file where you can add configuration
// options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}

In your app-level build.gradle, add the following at the bottom:

id("com.google.gms.google-services")


Step 4.1: Firebase Firestore Setup

In Firebase, set up Firestore as your database:

  1. Go to Cloud Firestore in the Firebase console.
  2. Click Create database.
  3. Select Start in test mode to allow read and write access.

Now, you're ready to use Firebase Firestore in your app.

Step 4.2: Firebase Authentication Setup

In Firebase, set up Email and password based authentication:

  • Go to Authentication in the Firebase console.
  • Click Email and Password based authentication.

Now, you're ready to use Firebase Authentication in your app.


Directory Structure of the Chat Application

directory_structure



Implementing the Chat Application

Step 5: Modify Code to Use Firebase

Now, we will update the chat app to store and retrieve messages from Firestore in real time and add Email and password based authentication.

Here’s the updated code for SignupActivity.kt:

SignupActivity.kt
package com.nikhil.chatapplication

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import com.google.firebase.auth.FirebaseAuth

class SignupActivity : ComponentActivity() {
    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        auth = FirebaseAuth.getInstance()

        setContent {
            SignupScreen()
        }
    }
}

@Composable
fun SignupScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var confirmPassword by remember { mutableStateOf("") }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    val auth = FirebaseAuth.getInstance()

    // Get the context for launching activities
    val context = LocalContext.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "Register", fontSize = 32.sp, color = Color.Black)

        Spacer(modifier = Modifier.height(16.dp))

        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        TextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = PasswordVisualTransformation()
        )

        Spacer(modifier = Modifier.height(16.dp))

        TextField(
            value = confirmPassword,
            onValueChange = { confirmPassword = it },
            label = { Text("Confirm Password") },
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = PasswordVisualTransformation()
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                if (password == confirmPassword) {
                    auth.createUserWithEmailAndPassword(email, password)
                        .addOnCompleteListener { task ->
                            if (task.isSuccessful) {
                                // Redirect to login after successful signup
                                val intent = Intent(context, LoginActivity::class.java)
                                context.startActivity(intent)
                            } else {
                                errorMessage = task.exception?.message
                            }
                        }
                } else {
                    errorMessage = "Passwords do not match"
                }
            }
        ) {
            Text(text = "Register")
        }

        if (errorMessage != null) {
            Text(text = errorMessage!!, color = Color.Red)
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Button to navigate to Login Screen
        TextButton(onClick = {
            val intent = Intent(context, LoginActivity::class.java)
            context.startActivity(intent)
        }) {
            Text(text = "Already have an account? Login here", color = Color.Blue)
        }
    }
}


LoginActivity File:

Login Page is created using LoginActivity.kt file. Here’s the updated code for LoginActivity.kt:

LoginActivity.kt
package com.nikhil.chatapplication


import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.firebase.auth.FirebaseAuth
import androidx.compose.ui.platform.LocalContext

class LoginActivity : ComponentActivity() {
    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        auth = FirebaseAuth.getInstance()

        setContent {
            LoginScreen()
        }
    }
}
@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    val auth = FirebaseAuth.getInstance()

    // Get the context for launching activities
    val context = LocalContext.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // "LOGIN" Heading
        Text(text = "LOGIN", fontSize = 32.sp, color = Color.Black)

        Spacer(modifier = Modifier.height(16.dp))

        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        TextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = PasswordVisualTransformation()
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = {
            auth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        val intent = Intent(context, MainActivity::class.java)
                        context.startActivity(intent)
                        // Optionally finish() if inside an activity
                    } else {
                        errorMessage = task.exception?.message
                    }
                }
        }) {
            Text(text = "Login")
        }

        if (errorMessage != null) {
            Text(text = errorMessage!!, color = Color.Red)
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Button to navigate to Signup Screen
        TextButton(onClick = {
            val intent = Intent(context, SignupActivity::class.java)
            context.startActivity(intent)
        }) {
            Text(text = "Don't have an account? Register here", color = Color.Blue)
        }
    }
}


MainActivity File:

MainActivity is the file which acts the main page where everyone can send their messages. Here’s the updated code for MainActivity.kt:

MainActivity.kt
package com.nikhil.chatapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.FirebaseFirestore
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ChatApp()
        }
    }
}

@Composable
fun ChatApp() {
    var messageText by remember { mutableStateOf(TextFieldValue("")) }
    var messages by remember { mutableStateOf(listOf<Message>()) }
    val db = FirebaseFirestore.getInstance()
    val auth = FirebaseAuth.getInstance()
    val currentUser = auth.currentUser
    val scope = rememberCoroutineScope()

    // Fetch messages from Firestore
    LaunchedEffect(Unit) {
        db.collection("messages")
            .orderBy("timestamp")
            .addSnapshotListener { snapshot, e ->
                if (e == null && snapshot != null) {
                    val fetchedMessages = snapshot.documents.map {
                        Message(
                            message = it.getString("message") ?: "",
                            senderId = it.getString("senderId") ?: "",
                            timestamp = it.getTimestamp("timestamp")?.toDate().toString()
                        )
                    }
                    messages = fetchedMessages
                }
            }
    }

    Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
        LazyColumn(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            contentPadding = PaddingValues(8.dp)
        ) {
            items(messages.size) { index ->
                MessageItem(
                    message = messages[index],
                    isCurrentUser = messages[index].senderId == currentUser?.uid
                )
            }
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            BasicTextField(
                value = messageText,
                onValueChange = { messageText = it },
                modifier = Modifier
                    .weight(1f)
                    .background(Color.LightGray, CircleShape)
                    .padding(12.dp)
            )

            Spacer(modifier = Modifier.width(8.dp))

            Button(
                onClick = {
                    if (messageText.text.isNotBlank()) {
                        scope.launch {
                            val messageData = mapOf(
                                "message" to messageText.text,
                                "senderId" to currentUser?.uid,
                                "timestamp" to com.google.firebase.Timestamp.now()
                            )
                            db.collection("messages").add(messageData)
                            messageText = TextFieldValue("")
                        }
                    }
                }
            ) {
                Text(text = "Send")
            }
        }
    }
}

@Composable
fun MessageItem(message: Message, isCurrentUser: Boolean) {
    val alignment = if (isCurrentUser) Alignment.End else Alignment.Start
    val backgroundColor = if (isCurrentUser) MaterialTheme.colorScheme.primary else Color.Gray

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        horizontalAlignment = alignment
    ) {
        Surface(
            shape = MaterialTheme.shapes.medium,
            shadowElevation = 1.dp,
            color = backgroundColor
        ) {
            Text(
                text = "${message.message}\n${message.timestamp}",
                fontSize = 16.sp,
                color = Color.White,
                modifier = Modifier.padding(12.dp)
            )
        }
    }
}

data class Message(
    val message: String,
    val senderId: String,
    val timestamp: String
)


Explanation of the above Application:

  1. Firestore: We connect to Firestore using FirebaseFirestore.getInstance() and use the collection named "messages" with senderId and timestamp.
  2. addSnapshotListener: This listens for real-time updates from Firestore. Whenever new messages are added, the UI updates automatically.
  3. messageText: When the user types a message and presses send, the message gets stored in Firestore using the add method.
  4. Authentication: Helps in recognizing the messages of each user.

Running the Application

  1. Connect an Android device or emulator.
  2. Click Run in Android Studio.
  3. Type a message, and it will be stored in Firebase Firestore.
  4. All users will see the new messages in real-time.

Data Flow Diagram (DFD)

Here's a simple Data Flow Diagram (DFD) for the chat application:

Level 0 DFD (Context Diagram)

DFD_context_diagram


Level 1 DFD (Detailed Diagram)

DFD_detailed_diagram

Output Video


Whole Application Code GitHub Link:

Click Here to Check the whole Application Code

Conclusion

In this tutorial, we built a chat application using Jetpack Compose and Firebase Firestore. With Firebase’s real-time capabilities, messages are instantly synced across all devices, making the chat app dynamic and real-time. This basic application can be extended further by adding user authentication, timestamps, and read receipts.


Next Article

Similar Reads