diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e93dfad --- /dev/null +++ b/TODO.md @@ -0,0 +1,75 @@ +# LocalChat (lchat) - Improvement Plan + +## Phase 1: Architecture Foundation + +### 1.1 MVVM with ViewModels and Repository Pattern ✅ +- [x] Create ViewModels for each screen (LobbyViewModel, ChatViewModel) +- [x] Extract business logic from Fragments to ViewModels +- [x] Create Repository layer for data operations +- [x] Implement proper state management with LiveData/StateFlow +- [x] Add ViewModelFactory if needed + +### 1.2 Dependency Injection with Hilt +- [ ] Add Hilt dependencies +- [ ] Set up Hilt modules for WifiAwareManager +- [ ] Convert singletons to proper DI +- [ ] Inject ViewModels using Hilt + +## Phase 2: Core UX Improvements + +### 2.1 Connection Status Management +- [ ] Add connection state to ViewModels +- [ ] Create UI indicator for connection status +- [ ] Show real-time connection state changes +- [ ] Add connection error messages + +### 2.2 User List Feature +- [ ] Track connected users in Repository +- [ ] Add UI to display active users +- [ ] Handle user join/leave events +- [ ] Show user count in chat header + +## Phase 3: Data Persistence + +### 3.1 Room Database Setup +- [ ] Add Room dependencies +- [ ] Create Message and User entities +- [ ] Implement DAOs for data access +- [ ] Create database migrations + +### 3.2 Message Persistence +- [ ] Store messages in Room database +- [ ] Load message history on app restart +- [ ] Implement message sync logic +- [ ] Add message timestamps + +## Phase 4: Reliability Improvements + +### 4.1 Reconnection Handling +- [ ] Detect connection drops +- [ ] Implement exponential backoff retry +- [ ] Preserve message queue during disconnection +- [ ] Auto-reconnect when network available + +### 4.2 Network State Monitoring +- [ ] Monitor WiFi state changes +- [ ] Handle app lifecycle properly +- [ ] Save and restore connection state + +## Phase 5: Advanced Features + +### 5.1 Background Service +- [ ] Create foreground service for persistent connection +- [ ] Handle Doze mode and battery optimization +- [ ] Add notification for active chat +- [ ] Implement proper service lifecycle + +### 5.2 Additional Features +- [ ] Message delivery status +- [ ] Typing indicators +- [ ] File/image sharing support +- [ ] Message encryption improvements + +## Current Status +- 🚀 Starting with Phase 1.1 - MVVM Architecture +- Target: Create a maintainable, testable architecture \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/data/Message.kt b/app/src/main/java/com/mattintech/lchat/data/Message.kt index e429fa5..ade53f4 100644 --- a/app/src/main/java/com/mattintech/lchat/data/Message.kt +++ b/app/src/main/java/com/mattintech/lchat/data/Message.kt @@ -2,9 +2,9 @@ package com.mattintech.lchat.data data class Message( val id: String, - val senderId: String, - val senderName: String, + val userId: String, + val userName: String, val content: String, val timestamp: Long, - val isLocal: Boolean = false + val isOwnMessage: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/network/WifiAwareManagerSingleton.kt b/app/src/main/java/com/mattintech/lchat/network/WifiAwareManagerSingleton.kt deleted file mode 100644 index 7cb4b38..0000000 --- a/app/src/main/java/com/mattintech/lchat/network/WifiAwareManagerSingleton.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.mattintech.lchat.network - -import android.content.Context - -object WifiAwareManagerSingleton { - private var instance: WifiAwareManager? = null - - fun getInstance(context: Context): WifiAwareManager { - if (instance == null) { - instance = WifiAwareManager(context.applicationContext) - instance!!.initialize() - } - return instance!! - } - - fun reset() { - instance?.stop() - instance = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt new file mode 100644 index 0000000..7f6fff7 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt @@ -0,0 +1,111 @@ +package com.mattintech.lchat.repository + +import android.content.Context +import com.mattintech.lchat.data.Message +import com.mattintech.lchat.network.WifiAwareManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.UUID + +class ChatRepository private constructor(context: Context) { + + companion object { + @Volatile + private var INSTANCE: ChatRepository? = null + + fun getInstance(context: Context): ChatRepository { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: ChatRepository(context.applicationContext).also { INSTANCE = it } + } + } + } + + private val wifiAwareManager = WifiAwareManager(context) + + private val _messages = MutableStateFlow>(emptyList()) + val messages: StateFlow> = _messages.asStateFlow() + + private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) + val connectionState: StateFlow = _connectionState.asStateFlow() + + private val _connectedUsers = MutableStateFlow>(emptyList()) + val connectedUsers: StateFlow> = _connectedUsers.asStateFlow() + + private var messageCallback: ((String, String, String) -> Unit)? = null + private var connectionCallback: ((String, Boolean) -> Unit)? = null + + init { + wifiAwareManager.initialize() + setupWifiAwareCallbacks() + } + + private fun setupWifiAwareCallbacks() { + wifiAwareManager.setMessageCallback { userId, userName, content -> + val message = Message( + id = UUID.randomUUID().toString(), + userId = userId, + userName = userName, + content = content, + timestamp = System.currentTimeMillis(), + isOwnMessage = false + ) + addMessage(message) + messageCallback?.invoke(userId, userName, content) + } + } + + fun startHostMode(roomName: String) { + wifiAwareManager.startHostMode(roomName) + _connectionState.value = ConnectionState.Hosting(roomName) + } + + fun startClientMode() { + wifiAwareManager.startClientMode() + _connectionState.value = ConnectionState.Searching + } + + fun sendMessage(userId: String, userName: String, content: String) { + val message = Message( + id = UUID.randomUUID().toString(), + userId = userId, + userName = userName, + content = content, + timestamp = System.currentTimeMillis(), + isOwnMessage = true + ) + addMessage(message) + wifiAwareManager.sendMessage(userId, userName, content) + } + + private fun addMessage(message: Message) { + _messages.value = _messages.value + message + } + + fun clearMessages() { + _messages.value = emptyList() + } + + fun setMessageCallback(callback: (String, String, String) -> Unit) { + messageCallback = callback + } + + fun setConnectionCallback(callback: (String, Boolean) -> Unit) { + connectionCallback = callback + wifiAwareManager.setConnectionCallback(callback) + } + + fun stop() { + wifiAwareManager.stop() + _connectionState.value = ConnectionState.Disconnected + _connectedUsers.value = emptyList() + } + + sealed class ConnectionState { + object Disconnected : ConnectionState() + object Searching : ConnectionState() + data class Hosting(val roomName: String) : ConnectionState() + data class Connected(val roomName: String) : ConnectionState() + data class Error(val message: String) : ConnectionState() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt b/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt index 6f5e186..34cdf47 100644 --- a/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt +++ b/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt @@ -6,15 +6,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager -import com.mattintech.lchat.data.Message import com.mattintech.lchat.databinding.FragmentChatBinding -import com.mattintech.lchat.network.WifiAwareManager -import com.mattintech.lchat.network.WifiAwareManagerSingleton import com.mattintech.lchat.ui.adapters.MessageAdapter import com.mattintech.lchat.utils.LOG_PREFIX -import java.util.UUID +import com.mattintech.lchat.viewmodel.ChatViewModel +import com.mattintech.lchat.viewmodel.ViewModelFactory +import kotlinx.coroutines.launch class ChatFragment : Fragment() { @@ -26,10 +27,8 @@ class ChatFragment : Fragment() { private val binding get() = _binding!! private val args: ChatFragmentArgs by navArgs() - private lateinit var wifiAwareManager: WifiAwareManager + private lateinit var viewModel: ChatViewModel private lateinit var messageAdapter: MessageAdapter - private val messages = mutableListOf() - private val userId = UUID.randomUUID().toString() override fun onCreateView( inflater: LayoutInflater, @@ -45,8 +44,12 @@ class ChatFragment : Fragment() { super.onViewCreated(view, savedInstanceState) Log.d(TAG, "onViewCreated - room: ${args.roomName}, user: ${args.userName}, isHost: ${args.isHost}") + val factory = ViewModelFactory(requireContext()) + viewModel = ViewModelProvider(this, factory)[ChatViewModel::class.java] + viewModel.initialize(args.roomName, args.userName, args.isHost) + setupUI() - setupWifiAware() + observeViewModel() } private fun setupUI() { @@ -68,58 +71,35 @@ class ChatFragment : Fragment() { } } - private fun setupWifiAware() { - wifiAwareManager = WifiAwareManagerSingleton.getInstance(requireContext()) - - wifiAwareManager.setMessageCallback { senderId, senderName, content -> - Log.d(TAG, "Message received - from: $senderName, content: $content") - val message = Message( - id = UUID.randomUUID().toString(), - senderId = senderId, - senderName = senderName, - content = content, - timestamp = System.currentTimeMillis(), - isLocal = senderId == userId - ) - - activity?.runOnUiThread { - messages.add(message) - messageAdapter.submitList(messages.toList()) - binding.messagesRecyclerView.smoothScrollToPosition(messages.size - 1) + private fun observeViewModel() { + lifecycleScope.launch { + viewModel.messages.collect { messages -> + messageAdapter.submitList(messages) + if (messages.isNotEmpty()) { + binding.messagesRecyclerView.smoothScrollToPosition(messages.size - 1) + } } } - // No need to start host mode here - already started in LobbyFragment - Log.d(TAG, "Chat setup complete - isHost: ${args.isHost}, room: ${args.roomName}") + lifecycleScope.launch { + viewModel.connectionState.collect { state -> + Log.d(TAG, "Connection state: $state") + // Handle connection state changes if needed + } + } } private fun sendMessage() { val content = binding.messageInput.text?.toString()?.trim() if (content.isNullOrEmpty()) return - val message = Message( - id = UUID.randomUUID().toString(), - senderId = userId, - senderName = args.userName, - content = content, - timestamp = System.currentTimeMillis(), - isLocal = true - ) - - messages.add(message) - messageAdapter.submitList(messages.toList()) - binding.messagesRecyclerView.smoothScrollToPosition(messages.size - 1) - - Log.d(TAG, "Sending message: $content") - wifiAwareManager.sendMessage(userId, args.userName, content) - + viewModel.sendMessage(content) binding.messageInput.text?.clear() } override fun onDestroyView() { super.onDestroyView() Log.d(TAG, "onDestroyView") - // Don't stop WifiAwareManager here - it's shared across fragments _binding = null } } \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/ui/LobbyFragment.kt b/app/src/main/java/com/mattintech/lchat/ui/LobbyFragment.kt index e72e620..8f476b9 100644 --- a/app/src/main/java/com/mattintech/lchat/ui/LobbyFragment.kt +++ b/app/src/main/java/com/mattintech/lchat/ui/LobbyFragment.kt @@ -11,8 +11,10 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import com.mattintech.lchat.R import com.mattintech.lchat.databinding.FragmentLobbyBinding -import com.mattintech.lchat.network.WifiAwareManager -import com.mattintech.lchat.network.WifiAwareManagerSingleton +import com.mattintech.lchat.viewmodel.LobbyEvent +import com.mattintech.lchat.viewmodel.LobbyState +import com.mattintech.lchat.viewmodel.LobbyViewModel +import com.mattintech.lchat.viewmodel.ViewModelFactory import com.mattintech.lchat.utils.LOG_PREFIX class LobbyFragment : Fragment() { @@ -24,7 +26,7 @@ class LobbyFragment : Fragment() { private var _binding: FragmentLobbyBinding? = null private val binding get() = _binding!! - private lateinit var wifiAwareManager: WifiAwareManager + private lateinit var viewModel: LobbyViewModel override fun onCreateView( inflater: LayoutInflater, @@ -40,10 +42,11 @@ class LobbyFragment : Fragment() { super.onViewCreated(view, savedInstanceState) Log.d(TAG, "onViewCreated") - Log.d(TAG, "Getting WifiAwareManager singleton") - wifiAwareManager = WifiAwareManagerSingleton.getInstance(requireContext()) + val factory = ViewModelFactory(requireContext()) + viewModel = ViewModelProvider(this, factory)[LobbyViewModel::class.java] setupUI() + observeViewModel() } private fun setupUI() { @@ -64,55 +67,61 @@ class LobbyFragment : Fragment() { } binding.actionButton.setOnClickListener { - val userName = binding.nameInput.text?.toString()?.trim() - - if (userName.isNullOrEmpty()) { - Toast.makeText(context, "Please enter your name", Toast.LENGTH_SHORT).show() - return@setOnClickListener - } + val userName = binding.nameInput.text?.toString()?.trim() ?: "" when (binding.modeRadioGroup.checkedRadioButtonId) { R.id.hostRadio -> { - val roomName = binding.roomInput.text?.toString()?.trim() - if (roomName.isNullOrEmpty()) { - Toast.makeText(context, "Please enter a room name", Toast.LENGTH_SHORT).show() - return@setOnClickListener - } - startHostMode(roomName, userName) + val roomName = binding.roomInput.text?.toString()?.trim() ?: "" + viewModel.startHostMode(roomName, userName) } R.id.clientRadio -> { - startClientMode(userName) + viewModel.startClientMode(userName) + } + } + } + } + + private fun observeViewModel() { + viewModel.state.observe(viewLifecycleOwner) { state -> + when (state) { + is LobbyState.Idle -> { + binding.noRoomsText.visibility = View.GONE + } + is LobbyState.Connecting -> { + if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) { + binding.noRoomsText.visibility = View.VISIBLE + binding.noRoomsText.text = getString(R.string.connecting) + } + } + is LobbyState.Connected -> { + if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) { + val userName = binding.nameInput.text?.toString()?.trim() ?: "" + viewModel.onConnectedToRoom(state.roomName, userName) + } + } + is LobbyState.Error -> { + binding.noRoomsText.visibility = View.VISIBLE + binding.noRoomsText.text = state.message + Toast.makeText(context, state.message, Toast.LENGTH_LONG).show() } } } - wifiAwareManager.setConnectionCallback { roomName, isConnected -> - Log.d(TAG, "Connection callback - room: $roomName, connected: $isConnected") - activity?.runOnUiThread { - if (isConnected && binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) { - val userName = binding.nameInput.text?.toString()?.trim() ?: "" - navigateToChat(roomName, userName, false) - } else if (!isConnected && binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) { - binding.noRoomsText.text = "Failed to connect to $roomName. Ensure Wi-Fi is enabled on both devices." - Toast.makeText(context, "Connection failed. Check Wi-Fi is enabled.", Toast.LENGTH_LONG).show() + viewModel.events.observe(viewLifecycleOwner) { event -> + when (event) { + is LobbyEvent.NavigateToChat -> { + navigateToChat(event.roomName, event.userName, event.isHost) + viewModel.clearEvent() } + is LobbyEvent.ShowError -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + viewModel.clearEvent() + } + null -> {} } } } - private fun startHostMode(roomName: String, userName: String) { - Log.d(TAG, "Starting host mode - room: $roomName, user: $userName") - wifiAwareManager.startHostMode(roomName) - navigateToChat(roomName, userName, true) - } - - private fun startClientMode(userName: String) { - Log.d(TAG, "Starting client mode - user: $userName") - binding.noRoomsText.visibility = View.VISIBLE - binding.noRoomsText.text = getString(R.string.connecting) - wifiAwareManager.startClientMode() - } - private fun navigateToChat(roomName: String, userName: String, isHost: Boolean) { Log.d(TAG, "Navigating to chat - room: $roomName, user: $userName, isHost: $isHost") val action = LobbyFragmentDirections.actionLobbyToChat( diff --git a/app/src/main/java/com/mattintech/lchat/ui/adapters/MessageAdapter.kt b/app/src/main/java/com/mattintech/lchat/ui/adapters/MessageAdapter.kt index 66b4d4a..cd89835 100644 --- a/app/src/main/java/com/mattintech/lchat/ui/adapters/MessageAdapter.kt +++ b/app/src/main/java/com/mattintech/lchat/ui/adapters/MessageAdapter.kt @@ -35,12 +35,12 @@ class MessageAdapter : ListAdapter(Me ) : RecyclerView.ViewHolder(binding.root) { fun bind(message: Message) { - binding.senderName.text = message.senderName + binding.senderName.text = message.userName binding.messageContent.text = message.content binding.timestamp.text = timeFormat.format(Date(message.timestamp)) val layoutParams = binding.messageCard.layoutParams as ConstraintLayout.LayoutParams - if (message.isLocal) { + if (message.isOwnMessage) { layoutParams.startToStart = ConstraintLayout.LayoutParams.UNSET layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID binding.messageCard.setCardBackgroundColor( diff --git a/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt b/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt new file mode 100644 index 0000000..88407e9 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt @@ -0,0 +1,89 @@ +package com.mattintech.lchat.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mattintech.lchat.data.Message +import com.mattintech.lchat.repository.ChatRepository +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.UUID + +sealed class ChatState { + object Connected : ChatState() + object Disconnected : ChatState() + data class Error(val message: String) : ChatState() +} + +class ChatViewModel( + private val chatRepository: ChatRepository +) : ViewModel() { + + private val _state = MutableLiveData(ChatState.Connected) + val state: LiveData = _state + + val messages: StateFlow> = chatRepository.messages + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + val connectionState = chatRepository.connectionState + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = ChatRepository.ConnectionState.Disconnected + ) + + val connectedUsers = chatRepository.connectedUsers + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + private var currentUserId: String = "" + private var currentUserName: String = "" + private var currentRoomName: String = "" + private var isHost: Boolean = false + + fun initialize(roomName: String, userName: String, isHost: Boolean) { + this.currentRoomName = roomName + this.currentUserName = userName + this.isHost = isHost + this.currentUserId = UUID.randomUUID().toString() + + // Setup message callback if needed for additional processing + chatRepository.setMessageCallback { userId, userName, content -> + // Can add additional message processing here if needed + } + } + + fun sendMessage(content: String) { + if (content.isBlank()) return + + viewModelScope.launch { + chatRepository.sendMessage(currentUserId, currentUserName, content) + } + } + + fun getRoomInfo(): Triple { + return Triple(currentRoomName, currentUserName, isHost) + } + + fun disconnect() { + viewModelScope.launch { + chatRepository.stop() + _state.value = ChatState.Disconnected + } + } + + override fun onCleared() { + super.onCleared() + disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/viewmodel/LobbyViewModel.kt b/app/src/main/java/com/mattintech/lchat/viewmodel/LobbyViewModel.kt new file mode 100644 index 0000000..fa4fe32 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/viewmodel/LobbyViewModel.kt @@ -0,0 +1,94 @@ +package com.mattintech.lchat.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mattintech.lchat.repository.ChatRepository +import kotlinx.coroutines.launch + +sealed class LobbyState { + object Idle : LobbyState() + object Connecting : LobbyState() + data class Connected(val roomName: String) : LobbyState() + data class Error(val message: String) : LobbyState() +} + +sealed class LobbyEvent { + data class NavigateToChat( + val roomName: String, + val userName: String, + val isHost: Boolean + ) : LobbyEvent() + data class ShowError(val message: String) : LobbyEvent() +} + +class LobbyViewModel( + private val chatRepository: ChatRepository +) : ViewModel() { + + private val _state = MutableLiveData(LobbyState.Idle) + val state: LiveData = _state + + private val _events = MutableLiveData() + val events: LiveData = _events + + init { + setupConnectionCallback() + } + + private fun setupConnectionCallback() { + chatRepository.setConnectionCallback { roomName, isConnected -> + viewModelScope.launch { + if (isConnected) { + _state.value = LobbyState.Connected(roomName) + } else { + _state.value = LobbyState.Error("Failed to connect to $roomName. Ensure Wi-Fi is enabled on both devices.") + } + } + } + } + + fun startHostMode(roomName: String, userName: String) { + if (roomName.isBlank()) { + _events.value = LobbyEvent.ShowError("Please enter a room name") + return + } + + if (userName.isBlank()) { + _events.value = LobbyEvent.ShowError("Please enter your name") + return + } + + viewModelScope.launch { + _state.value = LobbyState.Connecting + chatRepository.startHostMode(roomName) + _events.value = LobbyEvent.NavigateToChat(roomName, userName, true) + } + } + + fun startClientMode(userName: String) { + if (userName.isBlank()) { + _events.value = LobbyEvent.ShowError("Please enter your name") + return + } + + viewModelScope.launch { + _state.value = LobbyState.Connecting + chatRepository.startClientMode() + } + } + + fun onConnectedToRoom(roomName: String, userName: String) { + _events.value = LobbyEvent.NavigateToChat(roomName, userName, false) + } + + fun clearEvent() { + _events.value = null + } + + override fun onCleared() { + super.onCleared() + // Clean up resources if needed + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/viewmodel/ViewModelFactory.kt b/app/src/main/java/com/mattintech/lchat/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000..663ef6d --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/viewmodel/ViewModelFactory.kt @@ -0,0 +1,24 @@ +package com.mattintech.lchat.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.mattintech.lchat.repository.ChatRepository + +class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + private val chatRepository by lazy { ChatRepository.getInstance(context) } + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return when { + modelClass.isAssignableFrom(LobbyViewModel::class.java) -> { + LobbyViewModel(chatRepository) as T + } + modelClass.isAssignableFrom(ChatViewModel::class.java) -> { + ChatViewModel(chatRepository) as T + } + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } +} \ No newline at end of file