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

8.4 KiB
Raw Blame History

Руководство по интеграции

Быстрый старт

1. Добавление зависимостей

В app/build.gradle.kts уже добавлены:

// 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 уже обновлён:

@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 (рекомендуется для больших чатов)

@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 (для небольших чатов)

@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

@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

@Composable
fun ChatScreen(viewModel: ChatViewModel = hiltViewModel()) {
    val messages by viewModel.messages.collectAsState()
    
    LazyColumn(reverseLayout = true) {
        items(messages) { message ->
            MessageItem(message = message)
        }
    }
}

Отправка сообщения

// Мгновенное отображение в UI
viewModel.sendMessage("Привет!")

// Сообщение сохраняется локально и появляется в UI сразу
// WorkManager отправит его на сервер в фоне

Удаление сообщения

// Мягкое удаление (через WorkManager)
viewModel.deleteMessage(messageId)

// Или немедленное удаление
viewModelScope.launch {
    repository.deleteMessage(messageId, forEveryone = false)
}

Редактирование сообщения

viewModelScope.launch {
    repository.editLocalMessage(messageId, "Новый текст")
}

Мониторинг синхронизации

// В 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} сообщений ожидают отправки")
}

Обработка офлайн-режима

@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
        )
    }
}

Проверка сборки

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() - должен вызываться после авторизации

Тестирование

Юнит-тесты

@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)
}

Интеграционные тесты

@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)
}

Дополнительные ресурсы