297 lines
8.4 KiB
Markdown
297 lines
8.4 KiB
Markdown
# Руководство по интеграции
|
||
|
||
## Быстрый старт
|
||
|
||
### 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) - больше примеров использования
|