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(
|
data class Message(
|
||||||
val id: String,
|
val id: String,
|
||||||
val senderId: String,
|
val userId: String,
|
||||||
val senderName: String,
|
val userName: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val timestamp: Long,
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.mattintech.lchat.data.Message
|
|
||||||
import com.mattintech.lchat.databinding.FragmentChatBinding
|
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.ui.adapters.MessageAdapter
|
||||||
import com.mattintech.lchat.utils.LOG_PREFIX
|
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() {
|
class ChatFragment : Fragment() {
|
||||||
|
|
||||||
@@ -26,10 +27,8 @@ class ChatFragment : Fragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val args: ChatFragmentArgs by navArgs()
|
private val args: ChatFragmentArgs by navArgs()
|
||||||
private lateinit var wifiAwareManager: WifiAwareManager
|
private lateinit var viewModel: ChatViewModel
|
||||||
private lateinit var messageAdapter: MessageAdapter
|
private lateinit var messageAdapter: MessageAdapter
|
||||||
private val messages = mutableListOf<Message>()
|
|
||||||
private val userId = UUID.randomUUID().toString()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -45,8 +44,12 @@ class ChatFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Log.d(TAG, "onViewCreated - room: ${args.roomName}, user: ${args.userName}, isHost: ${args.isHost}")
|
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()
|
setupUI()
|
||||||
setupWifiAware()
|
observeViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
@@ -68,58 +71,35 @@ class ChatFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupWifiAware() {
|
private fun observeViewModel() {
|
||||||
wifiAwareManager = WifiAwareManagerSingleton.getInstance(requireContext())
|
lifecycleScope.launch {
|
||||||
|
viewModel.messages.collect { messages ->
|
||||||
wifiAwareManager.setMessageCallback { senderId, senderName, content ->
|
messageAdapter.submitList(messages)
|
||||||
Log.d(TAG, "Message received - from: $senderName, content: $content")
|
if (messages.isNotEmpty()) {
|
||||||
val message = Message(
|
binding.messagesRecyclerView.smoothScrollToPosition(messages.size - 1)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to start host mode here - already started in LobbyFragment
|
lifecycleScope.launch {
|
||||||
Log.d(TAG, "Chat setup complete - isHost: ${args.isHost}, room: ${args.roomName}")
|
viewModel.connectionState.collect { state ->
|
||||||
|
Log.d(TAG, "Connection state: $state")
|
||||||
|
// Handle connection state changes if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMessage() {
|
private fun sendMessage() {
|
||||||
val content = binding.messageInput.text?.toString()?.trim()
|
val content = binding.messageInput.text?.toString()?.trim()
|
||||||
if (content.isNullOrEmpty()) return
|
if (content.isNullOrEmpty()) return
|
||||||
|
|
||||||
val message = Message(
|
viewModel.sendMessage(content)
|
||||||
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)
|
|
||||||
|
|
||||||
binding.messageInput.text?.clear()
|
binding.messageInput.text?.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Log.d(TAG, "onDestroyView")
|
Log.d(TAG, "onDestroyView")
|
||||||
// Don't stop WifiAwareManager here - it's shared across fragments
|
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,10 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.mattintech.lchat.R
|
import com.mattintech.lchat.R
|
||||||
import com.mattintech.lchat.databinding.FragmentLobbyBinding
|
import com.mattintech.lchat.databinding.FragmentLobbyBinding
|
||||||
import com.mattintech.lchat.network.WifiAwareManager
|
import com.mattintech.lchat.viewmodel.LobbyEvent
|
||||||
import com.mattintech.lchat.network.WifiAwareManagerSingleton
|
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
|
import com.mattintech.lchat.utils.LOG_PREFIX
|
||||||
|
|
||||||
class LobbyFragment : Fragment() {
|
class LobbyFragment : Fragment() {
|
||||||
@@ -24,7 +26,7 @@ class LobbyFragment : Fragment() {
|
|||||||
private var _binding: FragmentLobbyBinding? = null
|
private var _binding: FragmentLobbyBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var wifiAwareManager: WifiAwareManager
|
private lateinit var viewModel: LobbyViewModel
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -40,10 +42,11 @@ class LobbyFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Log.d(TAG, "onViewCreated")
|
Log.d(TAG, "onViewCreated")
|
||||||
|
|
||||||
Log.d(TAG, "Getting WifiAwareManager singleton")
|
val factory = ViewModelFactory(requireContext())
|
||||||
wifiAwareManager = WifiAwareManagerSingleton.getInstance(requireContext())
|
viewModel = ViewModelProvider(this, factory)[LobbyViewModel::class.java]
|
||||||
|
|
||||||
setupUI()
|
setupUI()
|
||||||
|
observeViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
@@ -64,55 +67,61 @@ class LobbyFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.actionButton.setOnClickListener {
|
binding.actionButton.setOnClickListener {
|
||||||
val userName = binding.nameInput.text?.toString()?.trim()
|
val userName = binding.nameInput.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
if (userName.isNullOrEmpty()) {
|
|
||||||
Toast.makeText(context, "Please enter your name", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
when (binding.modeRadioGroup.checkedRadioButtonId) {
|
when (binding.modeRadioGroup.checkedRadioButtonId) {
|
||||||
R.id.hostRadio -> {
|
R.id.hostRadio -> {
|
||||||
val roomName = binding.roomInput.text?.toString()?.trim()
|
val roomName = binding.roomInput.text?.toString()?.trim() ?: ""
|
||||||
if (roomName.isNullOrEmpty()) {
|
viewModel.startHostMode(roomName, userName)
|
||||||
Toast.makeText(context, "Please enter a room name", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
startHostMode(roomName, userName)
|
|
||||||
}
|
}
|
||||||
R.id.clientRadio -> {
|
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 ->
|
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||||
Log.d(TAG, "Connection callback - room: $roomName, connected: $isConnected")
|
when (event) {
|
||||||
activity?.runOnUiThread {
|
is LobbyEvent.NavigateToChat -> {
|
||||||
if (isConnected && binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
navigateToChat(event.roomName, event.userName, event.isHost)
|
||||||
val userName = binding.nameInput.text?.toString()?.trim() ?: ""
|
viewModel.clearEvent()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private fun navigateToChat(roomName: String, userName: String, isHost: Boolean) {
|
||||||
Log.d(TAG, "Navigating to chat - room: $roomName, user: $userName, isHost: $isHost")
|
Log.d(TAG, "Navigating to chat - room: $roomName, user: $userName, isHost: $isHost")
|
||||||
val action = LobbyFragmentDirections.actionLobbyToChat(
|
val action = LobbyFragmentDirections.actionLobbyToChat(
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ class MessageAdapter : ListAdapter<Message, MessageAdapter.MessageViewHolder>(Me
|
|||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(message: Message) {
|
fun bind(message: Message) {
|
||||||
binding.senderName.text = message.senderName
|
binding.senderName.text = message.userName
|
||||||
binding.messageContent.text = message.content
|
binding.messageContent.text = message.content
|
||||||
binding.timestamp.text = timeFormat.format(Date(message.timestamp))
|
binding.timestamp.text = timeFormat.format(Date(message.timestamp))
|
||||||
|
|
||||||
val layoutParams = binding.messageCard.layoutParams as ConstraintLayout.LayoutParams
|
val layoutParams = binding.messageCard.layoutParams as ConstraintLayout.LayoutParams
|
||||||
if (message.isLocal) {
|
if (message.isOwnMessage) {
|
||||||
layoutParams.startToStart = ConstraintLayout.LayoutParams.UNSET
|
layoutParams.startToStart = ConstraintLayout.LayoutParams.UNSET
|
||||||
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
|
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
|
||||||
binding.messageCard.setCardBackgroundColor(
|
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