refactoring to a proper MVVM
This commit is contained in:
75
TODO.md
Normal file
75
TODO.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<List<Message>>(emptyList())
|
||||
val messages: StateFlow<List<Message>> = _messages.asStateFlow()
|
||||
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
private val _connectedUsers = MutableStateFlow<List<String>>(emptyList())
|
||||
val connectedUsers: StateFlow<List<String>> = _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()
|
||||
}
|
||||
}
|
||||
@@ -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<Message>()
|
||||
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())
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,53 +67,59 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.startClientMode(userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 observeViewModel() {
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is LobbyState.Idle -> {
|
||||
binding.noRoomsText.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun startClientMode(userName: String) {
|
||||
Log.d(TAG, "Starting client mode - user: $userName")
|
||||
is LobbyState.Connecting -> {
|
||||
if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
||||
binding.noRoomsText.visibility = View.VISIBLE
|
||||
binding.noRoomsText.text = getString(R.string.connecting)
|
||||
wifiAwareManager.startClientMode()
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 navigateToChat(roomName: String, userName: String, isHost: Boolean) {
|
||||
|
||||
@@ -35,12 +35,12 @@ class MessageAdapter : ListAdapter<Message, MessageAdapter.MessageViewHolder>(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(
|
||||
|
||||
@@ -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>(ChatState.Connected)
|
||||
val state: LiveData<ChatState> = _state
|
||||
|
||||
val messages: StateFlow<List<Message>> = 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<String, String, Boolean> {
|
||||
return Triple(currentRoomName, currentUserName, isHost)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
viewModelScope.launch {
|
||||
chatRepository.stop()
|
||||
_state.value = ChatState.Disconnected
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
@@ -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>(LobbyState.Idle)
|
||||
val state: LiveData<LobbyState> = _state
|
||||
|
||||
private val _events = MutableLiveData<LobbyEvent?>()
|
||||
val events: LiveData<LobbyEvent?> = _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
|
||||
}
|
||||
}
|
||||
@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user