Files
forkmessager/client-mobile/chats/INTEGRATION_GUIDE.md
Халимов Рустам 7c66e1c0c0 Кэш чатов
2026-04-20 00:25:40 +03:00

297 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Руководство по интеграции
## Быстрый старт
### 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) - больше примеров использования