Building a Real-Time Chat App in Android with Kotlin and Firebase: A Family-Friendly Guide Using Clean Architecture
Hello, devs Today, we’re creating a real-time chat application using Firebase in Kotlin and organizing our code with Clean Architecture. This guide will walk you through every step, from setting up Firebase to structuring your app with Clean Architecture principles, ensuring that your chat app is clean, maintainable, and scalable. So, let’s get started and build something fantastic together!
What is Firebase Real-Time Database?
Firebase Realtime Database is a cloud-hosted NoSQL database that enables you to store and sync data between users in real-time. It’s perfect for applications that require instant data updates, such as chat apps, where messages need to be delivered and displayed immediately. With Firebase, you don’t have to worry about server-side infrastructure or complex data synchronization logic—Firebase handles it all for you.
Why Use Clean Architecture?
Clean Architecture is a software design philosophy that helps you structure your code in a way that separates concerns, making it easier to manage, test, and scale. By following Clean Architecture, you ensure that your application’s core logic remains independent of frameworks, making your codebase more flexible and adaptable.
Project Setup
Step 1: Set Up Firebase in Your Android Project
Create a Firebase Project: First Go to the Firebase Console, create a new project, and follow the setup instructions.
Add Firebase to Your Android Project:
Download the
google-services.json
file from Firebase and place it in theapp/
directory of your Android project.Add the Firebase dependencies to your
build.gradle
files.
// Project-level build.gradle
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
// App-level build.gradle
apply plugin: 'com.google.gms.google-services'
dependencies {
implementation 'com.google.firebase:firebase-database-ktx:20.1.0'
implementation 'com.google.firebase:firebase-auth-ktx:22.0.0'
}
- Set Up Firebase Authentication: To manage users, enable Firebase Authentication in the Firebase console. For a chat app, you might use email/password authentication or other methods provided by Firebase.
Step 2: Structure Your Project with Clean Architecture
We’ll divide our project into three main layers:
Data Layer: Handles data sources and interactions with Firebase.
Domain Layer: Contains business logic and use cases.
Presentation Layer: Manages UI and user interactions.
Implementing Real-Time Chat
1. Data Layer
In the data layer, we’ll create a repository for managing chat messages and user authentication.
FirebaseRepository.kt: This interface and its implementation will handle interactions with Firebase Realtime Database and Firebase Authentication.
interface FirebaseRepository { fun getMessages(chatRoomId: String): LiveData<List<Message>> fun sendMessage(chatRoomId: String, message: Message) fun getCurrentUser(): User? fun authenticateUser(email: String, password: String, callback: (Boolean) -> Unit) } class FirebaseRepositoryImpl : FirebaseRepository { private val database = FirebaseDatabase.getInstance().reference private val auth = FirebaseAuth.getInstance() override fun getMessages(chatRoomId: String): LiveData<List<Message>> { val messagesLiveData = MutableLiveData<List<Message>>() val messagesRef = database.child("chatRooms").child(chatRoomId).child("messages") messagesRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val messages = mutableListOf<Message>() for (dataSnapshot in snapshot.children) { val message = dataSnapshot.getValue(Message::class.java) message?.let { messages.add(it) } } messagesLiveData.value = messages } override fun onCancelled(error: DatabaseError) { // Handle possible errors } }) return messagesLiveData } override fun sendMessage(chatRoomId: String, message: Message) { val messagesRef = database.child("chatRooms").child(chatRoomId).child("messages") messagesRef.push().setValue(message) } override fun getCurrentUser(): User? { return auth.currentUser?.let { User(it.uid, it.email ?: "Unknown") } } override fun authenticateUser(email: String, password: String, callback: (Boolean) -> Unit) { auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { task -> callback(task.isSuccessful) } } }
Message.kt: Data class for chat messages.
data class Message( val userId: String = "", val userName: String = "", val text: String = "", val timestamp: Long = System.currentTimeMillis() )
User.kt: Data class for users.
data class User( val id: String, val email: String )
2. Domain Layer
In this layer, we define use cases that encapsulate the core business logic.
SendMessageUseCase.kt: Handles the logic for sending a message.
class SendMessageUseCase(private val repository: FirebaseRepository) { fun execute(chatRoomId: String, message: Message) { repository.sendMessage(chatRoomId, message) } }
GetMessagesUseCase.kt: Retrieves messages for a chat room.
class GetMessagesUseCase(private val repository: FirebaseRepository) { fun execute(chatRoomId: String): LiveData<List<Message>> { return repository.getMessages(chatRoomId) } }
AuthenticateUserUseCase.kt: Manages user authentication.
class AuthenticateUserUseCase(private val repository: FirebaseRepository) { fun execute(email: String, password: String, callback: (Boolean) -> Unit) { repository.authenticateUser(email, password, callback) } }
3. Presentation Layer
In the presentation layer, we manage the UI and user interactions.
ChatViewModel.kt: ViewModel that interacts with use cases to handle chat functionality.
class ChatViewModel( private val getMessagesUseCase: GetMessagesUseCase, private val sendMessageUseCase: SendMessageUseCase ) : ViewModel() { private val _messages = MutableLiveData<List<Message>>() val messages: LiveData<List<Message>> get() = _messages fun loadMessages(chatRoomId: String) { getMessagesUseCase.execute(chatRoomId).observeForever { _messages.value = it } } fun sendMessage(chatRoomId: String, message: Message) { sendMessageUseCase.execute(chatRoomId, message) } }
AuthViewModel.kt: ViewModel for handling user authentication.
class AuthViewModel(private val authenticateUserUseCase: AuthenticateUserUseCase) : ViewModel() { private val _authStatus = MutableLiveData<Boolean>() val authStatus: LiveData<Boolean> get() = _authStatus fun authenticate(email: String, password: String) { authenticateUserUseCase.execute(email, password) { isSuccess -> _authStatus.value = isSuccess } } }
ChatActivity.kt: Activity that displays chat messages and handles user interactions.
class ChatActivity : AppCompatActivity() { private lateinit var viewModel: ChatViewModel private lateinit var adapter: MessageAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_chat) viewModel = ViewModelProvider(this).get(ChatViewModel::class.java) adapter = MessageAdapter() val recyclerView = findViewById<RecyclerView>(R.id.recyclerView) recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = adapter val chatRoomId = "sampleChatRoomId" // Get chat room ID dynamically viewModel.loadMessages(chatRoomId) viewModel.messages.observe(this, Observer { messages -> adapter.submitList(messages) }) val sendButton = findViewById<Button>(R.id.sendButton) val messageEditText = findViewById<EditText>(R.id.messageEditText) sendButton.setOnClickListener { val text = messageEditText.text.toString() val currentUser = viewModel.getCurrentUser() if (text.isNotEmpty() && currentUser != null) { val message = Message( userId = currentUser.id, userName = currentUser.email, text = text ) viewModel.sendMessage(chatRoomId, message) messageEditText.text.clear() } } } }
MessageAdapter.kt: RecyclerView adapter for displaying messages.
class MessageAdapter : ListAdapter<Message, MessageAdapter.MessageViewHolder>(MessageDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false) return MessageViewHolder(view) } override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { val message = getItem(position) holder.bind(message) } class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val userNameTextView: TextView = itemView.findViewById(R.id.userNameTextView) private val messageTextView: TextView = itemView.findViewById(R.id.messageTextView) fun bind(message: Message) { userNameTextView.text = message.userName messageTextView.text = message.text } } class MessageDiffCallback : DiffUtil.ItemCallback<Message>() { override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean { return oldItem.timestamp == newItem.timestamp } override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean { return oldItem == newItem } } }
Dependency Injection with Koin
To manage dependencies, we’ll use Koin.
AppModule.kt: Define your Koin module with dependencies.
val appModule = module { single<FirebaseRepository> { FirebaseRepositoryImpl() } single { GetMessagesUseCase(get()) } single { SendMessageUseCase(get()) } single { AuthenticateUserUseCase(get()) } viewModel { ChatViewModel(get(), get()) } viewModel { AuthViewModel(get()) } }
MyApplication.kt: Start Koin in your application class.
class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } }
Testing Your Chat App
Testing is essential to ensure your app works as expected.
FirebaseRepositoryTest.kt: Unit test for the repository.
class FirebaseRepositoryTest { private val repository = FirebaseRepositoryImpl() @Test fun `getMessages should return list of messages`() { val chatRoomId = "sampleChatRoomId" val messages = repository.getMessages(chatRoomId).getOrAwaitValue() assert(messages.isNotEmpty()) } @Test fun `sendMessage should send message successfully`() { val chatRoomId = "sampleChatRoomId" val message = Message(userId = "user1", userName = "User One", text = "Hello!") repository.sendMessage(chatRoomId, message) // Verify message is sent } }
ChatViewModelTest.kt: Unit test for the ViewModel.
class ChatViewModelTest { private val getMessagesUseCase = mockk<GetMessagesUseCase>() private val sendMessageUseCase = mockk<SendMessageUseCase>() private val viewModel = ChatViewModel(getMessagesUseCase, sendMessageUseCase) @Test fun `loadMessages should update messages LiveData`() { val messages = listOf(Message(userId = "user1", userName = "User One", text = "Hello!")) every { getMessagesUseCase.execute(any()) } returns MutableLiveData(messages) viewModel.loadMessages("sampleChatRoomId") assert(viewModel.messages.value == messages) } @Test fun `sendMessage should call sendMessageUseCase`() { val message = Message(userId = "user1", userName = "User One", text = "Hello!") viewModel.sendMessage("sampleChatRoomId", message) verify { sendMessageUseCase.execute("sampleChatRoomId", message) } } }
Congratulations! You’ve now built a real-time chat app using Firebase and Kotlin, structured with Clean Architecture. By following this guide, you’ve ensured that your app is not only functional but also well-organized and maintainable.
This project is a great way to practice your Android development skills and see the power of Firebase in action.
Okay devs, I hope you found this guide helpful and enjoyable. If you have any questions or run into any issues, feel free to reach out. I’m always here to help!
Connect with Me:
Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. 📲👨💻 Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟
Subscribe to my newsletter
Read articles from Mayursinh Parmar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mayursinh Parmar
Mayursinh Parmar
📱Mobile App Developer | Android & Flutter 🌟💡 Passionate about creating intuitive and engaging apps 💭✨ Let’s shape the future of mobile technology!