8.4 KiB
8.4 KiB
Руководство по интеграции
Быстрый старт
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)
}
Дополнительные ресурсы
- Paging 3 Documentation
- WorkManager Documentation
- Room Documentation
- ARCHITECTURE.md - детальное описание архитектуры
- USAGE_EXAMPLES.md - больше примеров использования