Kotlin ve Firebase ile Real-time Chat Uygulaması

mobile , android , kotlin , firebase , real-time-database

Modern bir Android mesajlaşma uygulaması geliştirmek istiyorsanız, doğru yerdesiniz! Bu yazıda Kotlin ve Firebase kullanarak gerçek zamanlı bir mesajlaşma uygulaması geliştireceğiz. Material Design 3, MVVM mimarisi ve Jetpack Compose kullanarak modern ve ölçeklenebilir bir uygulama ortaya çıkaracağız.

Neler Öğreneceğiz?

Proje Yapısı

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
chat-app/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/mbkayihan/chatapp/
│   │   │   │   ├── MainActivity.kt
│   │   │   │   ├── ChatApplication.kt
│   │   │   │   ├── di/
│   │   │   │   ├── domain/
│   │   │   │   ├── data/
│   │   │   │   ├── presentation/
│   │   │   │   └── utils/
│   │   │   └── res/
│   │   └── test/
│   └── build.gradle
└── build.gradle

Gerekli Bağımlılıklar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/build.gradle.kts
dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.compose.ui:ui:1.5.4")
    implementation("androidx.compose.material3:material3:1.1.2")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
    implementation("androidx.navigation:navigation-compose:2.7.5")
    
    // Firebase
    implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
    implementation("com.google.firebase:firebase-auth-ktx")
    implementation("com.google.firebase:firebase-database-ktx")
    implementation("com.google.firebase:firebase-storage-ktx")
    implementation("com.google.firebase:firebase-messaging-ktx")
    
    // Dependency Injection
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-compiler:2.48")
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
}

Firebase Kurulumu

  1. Firebase Console’dan yeni bir proje oluşturun
  2. Android uygulamanızı kaydedin
  3. google-services.json dosyasını indirip app/ dizinine ekleyin
  4. Authentication, Realtime Database ve Storage servislerini aktifleştirin

Veri Modelleri

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data class User(
    val id: String = "",
    val name: String = "",
    val email: String = "",
    val photoUrl: String = "",
    val status: String = "offline"
)

data class Message(
    val id: String = "",
    val senderId: String = "",
    val receiverId: String = "",
    val content: String = "",
    val timestamp: Long = System.currentTimeMillis(),
    val type: MessageType = MessageType.TEXT,
    val mediaUrl: String = ""
)

enum class MessageType {
    TEXT, IMAGE, FILE
}

Repository Katmanı

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface ChatRepository {
    suspend fun sendMessage(message: Message): Result<Unit>
    fun getMessages(chatId: String): Flow<List<Message>>
    suspend fun uploadMedia(uri: Uri): Result<String>
    fun getUserPresence(userId: String): Flow<Boolean>
    suspend fun updateUserStatus(status: String): Result<Unit>
}

class ChatRepositoryImpl @Inject constructor(
    private val database: FirebaseDatabase,
    private val storage: FirebaseStorage,
    private val auth: FirebaseAuth
) : ChatRepository {
    
    override suspend fun sendMessage(message: Message): Result<Unit> = try {
        val chatRef = database.getReference("chats")
            .child(getChatId(message.senderId, message.receiverId))
            
        chatRef.push().setValue(message).await()
        Result.success(Unit)
    } catch (e: Exception) {
        Result.failure(e)
    }
    
    override fun getMessages(chatId: String): Flow<List<Message>> = callbackFlow {
        val messagesRef = database.getReference("chats").child(chatId)
        
        val listener = messagesRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                val messages = snapshot.children.mapNotNull { 
                    it.getValue<Message>() 
                }
                trySend(messages)
            }
            
            override fun onCancelled(error: DatabaseError) {
                close(error.toException())
            }
        })
        
        awaitClose { messagesRef.removeEventListener(listener) }
    }
    
    private fun getChatId(userId1: String, userId2: String): String {
        return if (userId1 < userId2) "$userId1-$userId2" else "$userId2-$userId1"
    }
}

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@HiltViewModel
class ChatViewModel @Inject constructor(
    private val repository: ChatRepository,
    private val auth: FirebaseAuth
) : ViewModel() {

    private val _messages = MutableStateFlow<List<Message>>(emptyList())
    val messages: StateFlow<List<Message>> = _messages.asStateFlow()

    private val _uiState = MutableStateFlow<ChatUiState>(ChatUiState.Initial)
    val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()

    fun loadMessages(receiverId: String) {
        viewModelScope.launch {
            repository.getMessages(getChatId(auth.uid!!, receiverId))
                .catch { error ->
                    _uiState.value = ChatUiState.Error(error.message)
                }
                .collect { messageList ->
                    _messages.value = messageList
                    _uiState.value = ChatUiState.Success
                }
        }
    }

    fun sendMessage(content: String, receiverId: String) {
        viewModelScope.launch {
            val message = Message(
                senderId = auth.uid!!,
                receiverId = receiverId,
                content = content
            )
            
            repository.sendMessage(message)
                .onSuccess {
                    _uiState.value = ChatUiState.MessageSent
                }
                .onFailure { error ->
                    _uiState.value = ChatUiState.Error(error.message)
                }
        }
    }
}

sealed class ChatUiState {
    object Initial : ChatUiState()
    object Success : ChatUiState()
    object MessageSent : ChatUiState()
    data class Error(val message: String?) : ChatUiState()
}

UI Katmanı (Jetpack Compose)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@Composable
fun ChatScreen(
    viewModel: ChatViewModel = hiltViewModel(),
    receiverId: String
) {
    val messages by viewModel.messages.collectAsState()
    val uiState by viewModel.uiState.collectAsState()
    
    LaunchedEffect(Unit) {
        viewModel.loadMessages(receiverId)
    }
    
    Scaffold(
        topBar = {
            ChatTopBar(receiverName = "John Doe") // Gerçek kullanıcı adını alın
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            // Mesaj Listesi
            LazyColumn(
                modifier = Modifier.weight(1f),
                reverseLayout = true
            ) {
                items(messages) { message ->
                    MessageItem(message)
                }
            }
            
            // Mesaj Gönderme Alanı
            MessageInput(
                onSendMessage = { content ->
                    viewModel.sendMessage(content, receiverId)
                }
            )
        }
    }
}

@Composable
fun MessageItem(message: Message) {
    val isOutgoing = message.senderId == FirebaseAuth.getInstance().uid
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        horizontalArrangement = if (isOutgoing) 
            Arrangement.End else Arrangement.Start
    ) {
        Surface(
            shape = MaterialTheme.shapes.medium,
            color = if (isOutgoing) 
                MaterialTheme.colorScheme.primary 
            else 
                MaterialTheme.colorScheme.surface,
            tonalElevation = 2.dp
        ) {
            Column(modifier = Modifier.padding(12.dp)) {
                Text(
                    text = message.content,
                    style = MaterialTheme.typography.bodyLarge
                )
                Text(
                    text = formatTimestamp(message.timestamp),
                    style = MaterialTheme.typography.bodySmall,
                    modifier = Modifier.align(Alignment.End)
                )
            }
        }
    }
}

@Composable
fun MessageInput(
    onSendMessage: (String) -> Unit
) {
    var text by remember { mutableStateOf("") }
    
    Surface(
        tonalElevation = 3.dp,
        modifier = Modifier.fillMaxWidth()
    ) {
        Row(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = text,
                onValueChange = { text = it },
                modifier = Modifier.weight(1f),
                placeholder = { Text("Mesajınızı yazın...") },
                colors = TextFieldDefaults.colors(
                    unfocusedContainerColor = MaterialTheme.colorScheme.surface
                )
            )
            
            Spacer(modifier = Modifier.width(8.dp))
            
            IconButton(
                onClick = {
                    if (text.isNotBlank()) {
                        onSendMessage(text)
                        text = ""
                    }
                }
            ) {
                Icon(
                    imageVector = Icons.Default.Send,
                    contentDescription = "Gönder"
                )
            }
        }
    }
}

Push Notifications

Firebase Cloud Messaging ile bildirim gönderme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        
        // Bildirim oluştur
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(message.data["title"])
            .setContentText(message.data["body"])
            .setSmallIcon(R.drawable.ic_notification)
            .setAutoCancel(true)
            .build()
            
        // Bildirimi göster
        NotificationManagerCompat.from(this)
            .notify(System.currentTimeMillis().toInt(), notification)
    }
    
    override fun onNewToken(token: String) {
        // Yeni token'ı sunucuya gönder
        sendRegistrationToServer(token)
    }
}

Medya Paylaşımı

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MediaRepository @Inject constructor(
    private val storage: FirebaseStorage
) {
    suspend fun uploadImage(uri: Uri): Result<String> = try {
        val filename = "img_${System.currentTimeMillis()}.jpg"
        val imageRef = storage.reference.child("images/$filename")
        
        val uploadTask = imageRef.putFile(uri).await()
        val downloadUrl = uploadTask.storage.downloadUrl.await()
        
        Result.success(downloadUrl.toString())
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Sonuç

Bu yazıda modern bir Android mesajlaşma uygulamasının temel bileşenlerini inceledik:

Sonraki Adımlar

Sorularınız veya önerileriniz varsa, yorum bırakabilirsiniz. Bir sonraki yazıda görüşmek üzere!