Реализация истории изменений (Version History) в мобильном приложении
История изменений — это не просто лог. Это возможность вернуться к предыдущей версии документа, сравнить изменения и понять, кто и когда что поменял. Для документов, таблиц, заметок или настроек это может быть критически важная функция. Неправильная реализация — либо взрывной рост размера базы данных, либо потеря данных при конфликтах.
Два подхода: snapshot vs event sourcing
Snapshot: при каждом сохранении создаём полную копию объекта. Просто в реализации, легко восстанавливать. Проблема — размер хранилища. Если документ 50 КБ и пользователь редактирует его 50 раз в день — 50 × 50 КБ = 2.5 МБ в день только для одного документа.
Event sourcing / delta: сохраняем только diff между версиями. Компактно, но сложнее восстановить произвольную версию — нужно воспроизвести все дельты от начального состояния. Для текстового контента — unified diff через DiffMatchPatch (доступен на Android и iOS как порт Google's diff-match-patch).
Для большинства мобильных приложений — гибрид: snapshot каждые N версий или каждые M дней, между ними — дельты. Восстановление: берём ближайший snapshot, применяем дельты.
Схема данных
@Entity(tableName = "document_versions")
data class DocumentVersion(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val documentId: String,
val versionNumber: Int,
val deltaJson: String?, // null если snapshot
val snapshotJson: String?, // null если delta
val authorId: String,
val deviceId: String,
val createdAt: Long = System.currentTimeMillis(),
val comment: String? = null // "Автосохранение" / "Сохранено вручную"
)
Индекс по (documentId, versionNumber) — обязателен для быстрой выборки истории конкретного документа.
Ограничение глубины истории
Хранить все версии вечно — нецелесообразно. Стратегии:
- Фиксированное количество: хранить последние N версий (например, 50). Старые удаляются через scheduled job.
-
Временное окно: версии за последние 30 дней. Через
WorkManager(Android) /BGProcessingTask(iOS) раз в сутки удаляем устаревшие. - Умное прореживание: полные версии за сегодня, одна в день за последнюю неделю, одна в неделю за последний месяц.
// Android — удаление старых версий через WorkManager
@Transaction
suspend fun pruneVersions(documentId: String, keepCount: Int) {
val versions = getVersionsByDocument(documentId)
if (versions.size > keepCount) {
val toDelete = versions.drop(keepCount)
deleteVersions(toDelete.map { it.id })
}
}
UI истории версий
Список версий с датой, автором, устройством и типом (автосохранение / ручное). Тап на версию — предпросмотр. Две кнопки: «Восстановить» и «Сравнить с текущей».
Сравнение версий — diff-view с подсветкой добавленного (зелёный) и удалённого (красный). На мобильном это обычно side-by-side или inline diff. Inline проще для небольших экранов: стандартный подход с SpannableString (Android) / NSAttributedString (iOS), цветные вставки и зачёркивания.
Автосохранение и дебаунс
Автосохранение не должно создавать версию на каждый символ. Дебаунс 2–3 секунды после последнего изменения:
// iOS — дебаунс автосохранения
private var saveTask: Task<Void, Never>?
func textDidChange(_ text: String) {
saveTask?.cancel()
saveTask = Task {
try? await Task.sleep(nanoseconds: 2_000_000_000)
guard !Task.isCancelled else { return }
await saveVersion(text, type: .auto)
}
}
Что входит в работу
- Выбор стратегии (snapshot / delta / гибрид) под объём данных
- Схема данных с индексами и внешними ключами
- Дебаунс автосохранения
- UI списка версий с метаданными
- Diff-view для сравнения версий
- Очистка устаревших версий по расписанию
Сроки
Snapshot-история с базовым UI: 1,5–2 дня. Гибрид с дельтами, diff-view и умным прореживанием: 4–5 дней. Стоимость зависит от формата хранимых данных и требований к глубине истории.







