Кэш чатов
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# Руководство по интеграции
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Добавление зависимостей
|
||||
|
||||
В `app/build.gradle.kts` уже добавлены:
|
||||
```kotlin
|
||||
// Paging 3
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.2.1")
|
||||
implementation("androidx.paging:paging-compose:3.2.1")
|
||||
|
||||
// WorkManager + Hilt
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("androidx.hilt:hilt-work:1.1.0")
|
||||
kapt("androidx.hilt:hilt-compiler:1.1.0")
|
||||
```
|
||||
|
||||
### 2. Обновление Application класса
|
||||
|
||||
`MainApplication.kt` уже обновлён:
|
||||
```kotlin
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application(), ImageLoaderFactory, Configuration.Provider {
|
||||
@Inject lateinit var workerFactory: WorkerFactory
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.setMinimumLoggingLevel(android.util.Log.INFO)
|
||||
.build()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Миграция базы данных
|
||||
|
||||
База данных автоматически обновится при первом запуске благодаря `MIGRATION_1_2`.
|
||||
|
||||
## Использование в ViewModel
|
||||
|
||||
### Вариант 1: Paging 3 (рекомендуется для больших чатов)
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val repository: ChatRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val chatId: String = savedStateHandle["chatId"] ?: ""
|
||||
|
||||
val messages: Flow<PagingData<Message>> = repository
|
||||
.getMessagesPaging(chatId)
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
fun sendMessage(content: String) {
|
||||
viewModelScope.launch {
|
||||
repository.sendMessage(chatId, content, "text")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMessage(messageId: String) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteLocalMessage(messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Вариант 2: Простой Flow (для небольших чатов)
|
||||
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val repository: ChatRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
|
||||
private val chatId: String = savedStateHandle["chatId"] ?: ""
|
||||
|
||||
val messages: Flow<List<Message>> = repository
|
||||
.getMessagesFlow(chatId)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
}
|
||||
```
|
||||
|
||||
## Использование в UI (Compose)
|
||||
|
||||
### С Paging 3
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: ChatViewModel = hiltViewModel()) {
|
||||
val messages by viewModel.messages.collectAsLazyPagingItems()
|
||||
|
||||
LazyColumn(
|
||||
reverseLayout = true, // Сообщения снизу вверх
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(
|
||||
count = messages.itemCount,
|
||||
key = messages.key
|
||||
) { index ->
|
||||
messages[index]?.let { message ->
|
||||
MessageItem(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
// Индикатор загрузки
|
||||
when {
|
||||
messages.loadState.refresh is LoadState.Loading -> {
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
messages.loadState.append is LoadState.Loading -> {
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
|
||||
// Ошибки
|
||||
messages.loadState.append.let { loadState ->
|
||||
if (loadState is LoadState.Error) {
|
||||
item {
|
||||
Text("Ошибка: ${loadState.error.message}")
|
||||
Button(onClick = { messages.retry() }) {
|
||||
Text("Повторить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### С простым Flow
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: ChatViewModel = hiltViewModel()) {
|
||||
val messages by viewModel.messages.collectAsState()
|
||||
|
||||
LazyColumn(reverseLayout = true) {
|
||||
items(messages) { message ->
|
||||
MessageItem(message = message)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Отправка сообщения
|
||||
|
||||
```kotlin
|
||||
// Мгновенное отображение в UI
|
||||
viewModel.sendMessage("Привет!")
|
||||
|
||||
// Сообщение сохраняется локально и появляется в UI сразу
|
||||
// WorkManager отправит его на сервер в фоне
|
||||
```
|
||||
|
||||
## Удаление сообщения
|
||||
|
||||
```kotlin
|
||||
// Мягкое удаление (через WorkManager)
|
||||
viewModel.deleteMessage(messageId)
|
||||
|
||||
// Или немедленное удаление
|
||||
viewModelScope.launch {
|
||||
repository.deleteMessage(messageId, forEveryone = false)
|
||||
}
|
||||
```
|
||||
|
||||
## Редактирование сообщения
|
||||
|
||||
```kotlin
|
||||
viewModelScope.launch {
|
||||
repository.editLocalMessage(messageId, "Новый текст")
|
||||
}
|
||||
```
|
||||
|
||||
## Мониторинг синхронизации
|
||||
|
||||
```kotlin
|
||||
// В ViewModel
|
||||
val syncStatus: Flow<List<MessageEntity>> = messageDao
|
||||
.getPendingSyncMessagesFlow()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
// В UI
|
||||
val pendingMessages by syncStatus.collectAsState()
|
||||
if (pendingMessages.isNotEmpty()) {
|
||||
Text("${pendingMessages.size} сообщений ожидают отправки")
|
||||
}
|
||||
```
|
||||
|
||||
## Обработка офлайн-режима
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun MessageItem(message: Message) {
|
||||
val isPending = message.id.startsWith("local_")
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = message.content ?: "",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Индикатор отправки
|
||||
if (isPending) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
// Статус прочтения
|
||||
Icon(
|
||||
imageVector = if (message.isRead) Icons.Default.DoneAll else Icons.Default.Done,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Проверка сборки
|
||||
|
||||
```bash
|
||||
cd client-mobile
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## Возможные проблемы и решения
|
||||
|
||||
### 1. Ошибка: "WorkerFactory not found"
|
||||
**Решение:** Убедитесь, что `MainApplication` реализует `Configuration.Provider`
|
||||
|
||||
### 2. Ошибка: "Table messages has no column named syncStatus"
|
||||
**Решение:** Проверьте, что миграция `MIGRATION_1_2` добавлена в `DatabaseModule`
|
||||
|
||||
### 3. Paging не загружает данные
|
||||
**Решение:** Проверьте логи `MessageRemoteMediator` - возможны проблемы с API
|
||||
|
||||
### 4. Сообщения не синхронизируются
|
||||
**Решение:** Проверьте WorkManager логи и наличие сетевого подключения
|
||||
|
||||
### 5. SignalR не подключается
|
||||
**Решение:** Проверьте `ChatHubClient.connect()` - должен вызываться после авторизации
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Юнит-тесты
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `message saved locally should have SYNCING status`() = runTest {
|
||||
val message = MessageEntity(
|
||||
id = "test",
|
||||
chatId = "chat1",
|
||||
// ...
|
||||
syncStatus = SyncStatus.SYNCING
|
||||
)
|
||||
dao.insertMessage(message)
|
||||
|
||||
val saved = dao.getMessageById("test")
|
||||
assertEquals(SyncStatus.SYNCING, saved?.syncStatus)
|
||||
}
|
||||
```
|
||||
|
||||
### Интеграционные тесты
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `sending message should save locally and sync to server`() = runTest {
|
||||
// Arrange
|
||||
val repository = ChatRepositoryImpl(...)
|
||||
|
||||
// Act
|
||||
val message = repository.sendMessage("chat1", "Hello", "text")
|
||||
|
||||
// Assert
|
||||
assertTrue(message.id.startsWith("local_"))
|
||||
|
||||
// Wait for sync
|
||||
delay(5000)
|
||||
|
||||
val synced = dao.getMessageById(message.id)
|
||||
assertEquals(SyncStatus.SYNCED, synced?.syncStatus)
|
||||
}
|
||||
```
|
||||
|
||||
## Дополнительные ресурсы
|
||||
|
||||
- [Paging 3 Documentation](https://developer.android.com/topic/libraries/architecture/paging/v3-overview)
|
||||
- [WorkManager Documentation](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
- [Room Documentation](https://developer.android.com/training/data-storage/room)
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - детальное описание архитектуры
|
||||
- [USAGE_EXAMPLES.md](USAGE_EXAMPLES.md) - больше примеров использования
|
||||
Reference in New Issue
Block a user