Реализация синхронизации данных через Google Drive
Google Drive API позволяет мобильному приложению сохранять данные пользователя в его личном облачном хранилище. В отличие от Firebase или собственного бэкенда, данные хранятся на аккаунте самого пользователя — он контролирует их и может удалить в любой момент. Это популярный подход для приложений с заметками, документами, данными для переноса между устройствами.
App Data folder vs Drive Files
Google Drive API предоставляет два типа хранилища для приложений:
Application Data folder — скрытая папка, видная только вашему приложению. Пользователь не видит файлы в Drive UI, но они занимают его квоту. Идеально для резервных копий и синхронизации настроек.
Drive Files — обычные файлы в Drive пользователя, видимые в интерфейсе. Нужен scope drive.file — приложение может работать только с файлами, которые само создало.
Для синхронизации данных приложения — Application Data folder. Для работы с пользовательскими документами — Drive Files.
Аутентификация через Google Sign-In
// Android: настройка Google Sign-In с Drive scope
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(Scope(DriveScopes.DRIVE_APPDATA))
.requestEmail()
.build()
val googleSignInClient = GoogleSignIn.getClient(activity, signInOptions)
// После успешного входа получаем credentials
fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
val account = completedTask.getResult(ApiException::class.java)
val credential = GoogleAccountCredential.usingOAuth2(
context, listOf(DriveScopes.DRIVE_APPDATA)
)
credential.selectedAccount = account.account
// Drive service для API-запросов
driveService = Drive.Builder(
NetHttpTransport(),
GsonFactory.getDefaultInstance(),
credential
).setApplicationName("MyApp").build()
}
На iOS — GoogleSignIn-iOS SDK с аналогичной настройкой scope.
Создание и обновление файлов резервной копии
class GoogleDriveBackupManager(private val driveService: Drive) {
suspend fun saveBackup(data: AppBackupData) = withContext(Dispatchers.IO) {
val json = Json.encodeToString(data)
val content = ByteArrayContent("application/json", json.toByteArray(Charsets.UTF_8))
// Ищем существующий файл резервной копии
val existingFileId = findBackupFile()
if (existingFileId != null) {
// Обновляем существующий
driveService.files().update(existingFileId, null, content)
.execute()
} else {
// Создаём новый в appDataFolder
val fileMetadata = File().apply {
name = "app_backup.json"
parents = listOf("appDataFolder")
}
driveService.files().create(fileMetadata, content)
.setFields("id, name, modifiedTime")
.execute()
}
}
private fun findBackupFile(): String? {
val result = driveService.files().list()
.setSpaces("appDataFolder")
.setFields("files(id, name, modifiedTime)")
.setQ("name = 'app_backup.json'")
.execute()
return result.files?.firstOrNull()?.id
}
suspend fun loadBackup(): AppBackupData? = withContext(Dispatchers.IO) {
val fileId = findBackupFile() ?: return@withContext null
val outputStream = ByteArrayOutputStream()
driveService.files().get(fileId)
.executeMediaAndDownloadTo(outputStream)
val json = outputStream.toString(Charsets.UTF_8.name())
Json.decodeFromString<AppBackupData>(json)
}
}
Синхронизация нескольких файлов
Для приложений с множеством сущностей — отдельный файл на каждый тип данных или версионированные снепшоты. Удобно сравнивать modifiedTime файлов при определении того, какое устройство последним изменяло данные:
data class DriveFileInfo(
val id: String,
val name: String,
val modifiedTime: com.google.api.client.util.DateTime,
val size: Long
)
suspend fun listBackupFiles(): List<DriveFileInfo> = withContext(Dispatchers.IO) {
val result = driveService.files().list()
.setSpaces("appDataFolder")
.setFields("files(id, name, modifiedTime, size)")
.setOrderBy("modifiedTime desc")
.execute()
result.files?.map { file ->
DriveFileInfo(
id = file.id,
name = file.name,
modifiedTime = file.modifiedTime,
size = file.getSize() ?: 0L
)
} ?: emptyList()
}
Фоновая синхронизация
WorkManager для периодической синхронизации в фоне:
class DriveBackupWorker(
context: Context,
params: WorkerParameters,
private val backupManager: GoogleDriveBackupManager,
private val localDataManager: LocalDataManager
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val localData = localDataManager.exportAll()
backupManager.saveBackup(localData)
Result.success()
} catch (e: UserRecoverableAuthIOException) {
// Токен истёк — нужна переаутентификация
notifyAuthRequired()
Result.failure()
} catch (e: IOException) {
// Сетевая ошибка — retry
Result.retry()
}
}
}
// Регистрация периодического бэкапа
val backupRequest = PeriodicWorkRequestBuilder<DriveBackupWorker>(6, TimeUnit.HOURS)
.setConstraints(Constraints(
requiredNetworkType = NetworkType.UNMETERED, // только WiFi
requiresBatteryNotLow = true
))
.build()
Обработка квоты и ошибок
Google Drive квота у пользователя обычно 15 ГБ, но не бесконечна. Практики:
- Сжимать данные перед сохранением (gzip JSON-файлов даёт 60-80% экономии)
- Хранить только последние N версий резервных копий
- Показывать пользователю размер занятого места
suspend fun getAppDataFolderSize(): Long = withContext(Dispatchers.IO) {
val files = listBackupFiles()
files.sumOf { it.size }
}
UserRecoverableAuthIOException — токен истёк или пользователь отозвал доступ. Нельзя игнорировать. Перехватываем, показываем UI для повторной авторизации.
Rate limiting. Drive API ограничен 1000 запросов в 100 секунд на пользователя. Батчевые запросы, кэширование на клиенте, не синхронизировать при каждом изменении — по таймеру или при выходе из приложения.
Реализация синхронизации через Google Drive с фоновым бэкапом, версионированием и обработкой авторизации: 2–3 недели. Стоимость рассчитывается индивидуально.







