Кэш чатов
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
# Примеры использования системы кэширования
|
||||
|
||||
## 1. Paging 3 в ViewModel
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val repository: ChatRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val chatId: String = savedStateHandle["chatId"] ?: ""
|
||||
|
||||
// Paging 3 поток для UI
|
||||
val messages: Flow<PagingData<Message>> = repository
|
||||
.getMessagesPaging(chatId)
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
// Простой Flow для небольших чатов
|
||||
val messagesList: Flow<List<Message>> = repository
|
||||
.getMessagesFlow(chatId)
|
||||
}
|
||||
```
|
||||
|
||||
## 2. UI с Paging 3 (Jetpack Compose)
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: ChatViewModel = hiltViewModel()) {
|
||||
val messages by viewModel.messages.collectAsLazyPagingItems()
|
||||
|
||||
LazyColumn {
|
||||
items(
|
||||
count = messages.itemCount,
|
||||
key = messages.key
|
||||
) { index ->
|
||||
val message = messages[index]
|
||||
message?.let {
|
||||
MessageItem(message = it)
|
||||
}
|
||||
}
|
||||
|
||||
// Индикаторы загрузки
|
||||
when {
|
||||
messages.loadState.refresh is LoadState.Loading -> {
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
messages.loadState.append is LoadState.Loading -> {
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
messages.loadState.prepend is LoadState.Loading -> {
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка ошибок
|
||||
messages.loadState.append.let { loadState ->
|
||||
if (loadState is LoadState.Error) {
|
||||
item {
|
||||
ErrorView(
|
||||
message = loadState.error.message,
|
||||
onRetry = { messages.retry() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Отправка сообщения (Offline-first)
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val repository: ChatRepository
|
||||
) : ViewModel() {
|
||||
|
||||
fun sendMessage(chatId: String, content: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// Сообщение сразу сохраняется локально и появляется в UI
|
||||
val message = repository.sendMessage(
|
||||
chatId = chatId,
|
||||
content = content,
|
||||
type = "text"
|
||||
)
|
||||
|
||||
// UI обновляется мгновенно через Flow
|
||||
Log.d("ChatViewModel", "Message saved locally: ${message.id}")
|
||||
|
||||
// Синхронизация с сервером произойдёт в фоне
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChatViewModel", "Failed to send message", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Удаление сообщения
|
||||
|
||||
```kotlin
|
||||
fun deleteMessage(messageId: String) {
|
||||
viewModelScope.launch {
|
||||
// Локальное удаление (сообщение скрывается из UI)
|
||||
repository.deleteLocalMessage(messageId)
|
||||
|
||||
// WorkManager удалит сообщение на сервере в фоне
|
||||
// При получении подтверждения - сообщение удаляется из БД
|
||||
}
|
||||
}
|
||||
|
||||
// Или немедленное удаление (если онлайн)
|
||||
fun deleteMessageImmediately(messageId: String, forEveryone: Boolean) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteMessage(messageId, forEveryone)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Редактирование сообщения
|
||||
|
||||
```kotlin
|
||||
fun editMessage(messageId: String, newContent: String) {
|
||||
viewModelScope.launch {
|
||||
// Локальное редактирование
|
||||
repository.editLocalMessage(messageId, newContent)
|
||||
|
||||
// UI обновляется мгновенно
|
||||
// WorkManager отправит изменения на сервер в фоне
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Отслеживание статуса синхронизации
|
||||
|
||||
```kotlin
|
||||
// Наблюдение за сообщениями, ожидающими синхронизации
|
||||
fun observePendingMessages() {
|
||||
viewModelScope.launch {
|
||||
messageDao.getPendingSyncMessagesFlow().collect { messages ->
|
||||
if (messages.isNotEmpty()) {
|
||||
Log.d("Sync", "${messages.size} messages pending sync")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка статуса конкретного сообщения
|
||||
fun isMessageSynced(messageId: String): Boolean {
|
||||
return runBlocking {
|
||||
val message = messageDao.getMessageById(messageId)
|
||||
message?.syncStatus == SyncStatus.SYNCED
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Обработка ошибок синхронизации
|
||||
|
||||
```kotlin
|
||||
fun retryFailedMessages() {
|
||||
viewModelScope.launch {
|
||||
val failedMessages = messageDao.getFailedSyncMessages()
|
||||
|
||||
failedMessages.forEach { message ->
|
||||
when {
|
||||
message.isDeletedLocally -> {
|
||||
// Повторить удаление
|
||||
MessageSyncWorker.scheduleSync(context)
|
||||
}
|
||||
message.isEditedLocally -> {
|
||||
// Повторить редактирование
|
||||
MessageSyncWorker.scheduleSync(context)
|
||||
}
|
||||
else -> {
|
||||
// Повторить отправку
|
||||
MessageSyncWorker.scheduleSync(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Прочтение сообщений
|
||||
|
||||
```kotlin
|
||||
fun markAsRead(chatId: String, lastMessageId: String, lastReadSequenceId: Int) {
|
||||
viewModelScope.launch {
|
||||
// Отправляем статус прочтения через SignalR
|
||||
repository.markMessagesAsRead(chatId, lastMessageId, lastReadSequenceId)
|
||||
|
||||
// Локальная база обновляется автоматически
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Real-time обновления
|
||||
|
||||
SignalR события обрабатываются автоматически:
|
||||
- Новые сообщения появляются в UI мгновенно
|
||||
- Редактирования/удаления синхронизируются
|
||||
- Статусы прочтения обновляются
|
||||
- Реакции отображаются в реальном времени
|
||||
|
||||
```kotlin
|
||||
// Обработчик SignalR уже интегрирован в ChatRepositoryImpl
|
||||
// Дополнительные действия можно добавить в MessageSignalRHandler
|
||||
```
|
||||
|
||||
## 10. Кэширование в ViewModel
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val repository: ChatRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val chatId: String = savedStateHandle["chatId"] ?: ""
|
||||
|
||||
// Кэшируем PagingData в scope ViewModel
|
||||
val messages: Flow<PagingData<Message>> = repository
|
||||
.getMessagesPaging(chatId)
|
||||
.cachedIn(viewModelScope) // Важно для сохранения состояния
|
||||
|
||||
// При повороте экрана пагинация сохраняется
|
||||
}
|
||||
```
|
||||
|
||||
## Рекомендации
|
||||
|
||||
### 1. Выбор между Paging 3 и Flow
|
||||
- **Paging 3** - для больших чатов (>100 сообщений)
|
||||
- **Flow<List>** - для небольших чатов или когда нужна вся история сразу
|
||||
|
||||
### 2. Обработка офлайн-режима
|
||||
```kotlin
|
||||
// UI должен показывать статус сообщения
|
||||
@Composable
|
||||
fun MessageItem(message: Message) {
|
||||
val isPending = message.id.startsWith("local_")
|
||||
|
||||
Row {
|
||||
Text(text = message.content)
|
||||
if (isPending) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Конфликты данных
|
||||
- Локальные изменения имеют приоритет во время отправки
|
||||
- Серверные данные перезаписывают локальные после SYNCED
|
||||
- SignalR события всегда применяются к актуальным данным
|
||||
|
||||
### 4. Производительность
|
||||
- Используйте `cachedIn(viewModelScope)` для PagingData
|
||||
- Избегайте частых вызовов `getMessages()` из сети
|
||||
- Позволяйте WorkManager управлять синхронизацией
|
||||
Reference in New Issue
Block a user