
Most apps store important user data locally in Room DB or any other local — but what if users switch phones or reinstall your app?
In this guide, you’ll learn how to add a Google Drive–based backup and restore feature to your Android app — using the Drive appDataFolder, so the backup stays private to your app in 10 simple steps.
We’ll use:
- Google Sign-In for authentication
- Google Drive REST API (v3) for upload/download
- Coroutines + ViewModel for clean background operations
🧩 What We’ll Cover
- Quick setup on the Google Cloud Console.
- Building a
GoogleDriveService to handle all API interactions. - Connecting the service to your UI using ViewModels.
- Handling the core backup and restore logic.
⚙️ Step 1: Quick Google Cloud Setup (2 minutes)
- Go to Google Cloud Console.
- Create a new project or select an existing one.
- Enable the Google Drive API from the API Library.
- Go to Credentials → Create Credentials → OAuth client ID → Android.
- Enter your app’s package name, e.g.
com.example.app. - Enter your SHA-1 fingerprint (you can get it from Android Studio or using
keytool). - Done ✅ — this enables your app to use Drive’s
appDataFolderscope via Google Sign-In.
📦 Step 2: Add Required Dependencies
In your app/build.gradle file:
dependencies {
// Google Sign-In
implementation 'com.google.android.gms:play-services-auth:20.7.0'
// Google API Client for Drive
implementation 'com.google.api-client:google-api-client-android:2.2.0'
implementation 'com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0'
implementation('com.google.api-client:google-api-client-gson:2.2.0') {
exclude group: 'org.apache.httpcomponents'
}
}
Add permission in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
🔐 Step 3: Google Drive Setup
To keep our code clean, we’ll create a dedicated service class to handle all Google Drive API calls.
First, we need a way to create an authorized Drive service object. This function uses the user’s signed-in account to create credentials scoped specifically to the appDataFolder.
class GoogleDriveService(private val context: Context) {
private fun getDriveService(account: GoogleSignInAccount): Drive {
val credential = GoogleAccountCredential.usingOAuth2(
context, Collections.singleton(DriveScopes.DRIVE_APPDATA)
).setSelectedAccount(account.account)
return Drive.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory.getDefaultInstance(),
credential)
.setApplicationName("YourAppName") // Replace with your app name
.build()
}
}
🧠 Step 4: Handling User Sign-In
To perform any action, the user must first sign in. This function creates the Intent that launches the standard Google Sign-In flow.
fun getSignInIntent(): Intent {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(Scope(DriveScopes.DRIVE_APPDATA)) // Request access to the AppData folder
.build()
val client = GoogleSignIn.getClient(context, gso)
return client.signInIntent
}
This initializes the Drive client authenticated with the signed-in user.

💾 Step 5: Upload the backup of Local Database
This is the core backup function. It takes the local database file, creates metadata for it, and uploads it to the appDataFolder. To avoid clutter, we first delete any old backup that might exist.
suspend fun uploadBackup(account: GoogleSignInAccount, databasePath: String): File? {
val driveService = getDriveService(account)
val dbFile = java.io.File(databasePath)
if (!dbFile.exists() || !dbFile.canRead() || dbFile.length() == 0L) {
throw Exception("Database file is invalid.")
}
// Delete the previous backup file, if it exists.
getLatestBackup(account)?.id?.let { fileId ->
driveService.files().delete(fileId).execute()
}
val fileMetadata = File().apply {
name = "your_app_database_backup_${System.currentTimeMillis()}.db"
parents = listOf("appDataFolder") // This is key!
}
val mediaContent = com.google.api.client.http.FileContent("application/octet-stream", dbFile)
return driveService.files().create(fileMetadata, mediaContent)
.setFields("id, name, size, modifiedTime")
.execute()
}
This uploads your app’s .db file to Drive’s private folder.
📂 Step 6: Finding and Restoring a Backup
To restore, we first need to find the backup file. This function queries the appDataFolder for the most recent file.
suspend fun getLatestBackup(context: Context, account: GoogleSignInAccount): File? {
val drive = getDriveService(context, account)
val files = drive.files().list()
.setSpaces("appDataFolder")
.setFields("files(id, name, modifiedTime, size)")
.setPageSize(1)
.execute()
suspend fun restoreBackup(account: GoogleSignInAccount, fileId: String, destinationPath: String) {
val driveService = getDriveService(account)
val destinationFile = java.io.File(destinationPath)
val tempBackupPath = "$destinationPath.tmp"
if (destinationFile.exists()) {
destinationFile.copyTo(java.io.File(tempBackupPath), overwrite = true)
}
try {
FileOutputStream(destinationFile).use { outputStream ->
driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
}
java.io.File(tempBackupPath).delete() // Success, so delete the temp copy
} catch (e: Exception) {
// If restore fails, copy the temporary backup back
val tempBackupFile = java.io.File(tempBackupPath)
if (tempBackupFile.exists()) {
tempBackupFile.copyTo(destinationFile, overwrite = true)
tempBackupFile.delete()
}
throw Exception("Database restore failed: ${e.message}")
}
}
♻️ Step 7: Restore Backup File
Once we have the file ID, this function downloads it and replaces the local database. As a safety measure, it first creates a temporary local copy of the current database, which is restored if the download fails.
suspend fun restoreBackup(account: GoogleSignInAccount, fileId: String, destinationPath: String) {
val driveService = getDriveService(account)
val destinationFile = java.io.File(destinationPath)
val tempBackupPath = "$destinationPath.tmp"
if (destinationFile.exists()) {
destinationFile.copyTo(java.io.File(tempBackupPath), overwrite = true)
}
try {
FileOutputStream(destinationFile).use { outputStream ->
driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
}
java.io.File(tempBackupPath).delete() // Success, so delete the temp copy
} catch (e: Exception) {
// If restore fails, copy the temporary backup back
val tempBackupFile = java.io.File(tempBackupPath)
if (tempBackupFile.exists()) {
tempBackupFile.copyTo(destinationFile, overwrite = true)
tempBackupFile.delete()
}
throw Exception("Database restore failed: ${e.message}")
}
}
🧭 Step 8: Connecting to the UI with ViewModel
In your ViewModel, you’ll use the GoogleDriveService to handle the logic triggered by user actions.
The Sign-In Flow
Your Activity or Fragment will launch the sign-in intent from the ViewModel and return the result.
// In your ViewModel
fun initiateGoogleSignIn() {
// Expose this intent to your UI to be launched by an ActivityResultLauncher
_uiState.update {
it.copy(signInIntent = driveService.getSignInIntent())
}
}
fun handleSignInResult(task: Task<GoogleSignInAccount>) {
try {
val account = task.getResult(ApiException::class.java)
// Sign-in success, update UI with account info
} catch (e: ApiException) {
// Sign-in failed
}
}
🧩 Step 9: Performing the backup
This is the most critical part. To safely back up a Room database, you must close the database connection before accessing the file on disk.
After the backup is complete, restarting the app is the simplest way to ensure the database connection is cleanly re-established.
// In your BackupViewModel
private fun performBackup() {
viewModelScope.launch {
_uiState.update { it.copy(isBackingUp = true) }
try {
val account = _uiState.value.account ?: throw Exception("No account found")
val dbPath = getApplication<Application>().getDatabasePath("your_app_database.db").absolutePath
withContext(Dispatchers.IO) {
// IMPORTANT: Close the database before accessing the file
database.close()
kotlinx.coroutines.delay(500) // Brief delay to ensure file handle is released
driveService.uploadBackup(account, dbPath)
}
_uiState.update { it.copy(snackbarMessage = "Backup successful! Restarting...") }
restartApp() // Helper function to restart the application
} catch (e: Exception) {
_uiState.update { it.copy(snackbarMessage = "Backup failed: ${e.message}") }
} finally {
_uiState.update { it.copy(isBackingUp = false) }
}
}
}
🧑💻 Step 10: Triggering the Restore
The restore logic mirrors the backup process. We close the database, call the service to download and replace the file, and then restart the app to load the new data.
// In your BackupViewModel
fun startRestore() {
viewModelScope.launch {
_uiState.update { it.copy(isRestoring = true) }
try {
// ... get account and fileId from uiState ...
val destinationPath = getApplication<Application>().getDatabasePath("your_app_database.db").absolutePath
withContext(Dispatchers.IO) {
// IMPORTANT: Close the database before overwriting the file
database.close()
kotlinx.coroutines.delay(500)
driveService.restoreBackup(account, fileId, destinationPath)
}
_uiState.update { it.copy(snackbarMessage = "Restore successful! Restarting...") }
restartApp()
} catch(e: Exception) {
_uiState.update { it.copy(snackbarMessage = "Restore failed: ${e.message}") }
} finally {
_uiState.update { it.copy(isRestoring = false) }
}
}
}
Job Offers
🧹 Key Notes & Gotchas
appDataFolderis private — users won’t see it in their Drive UI.- Always close your Room DB before copying.
- Add both debug and release SHA-1 to your Cloud Console credentials.
- Test on a real device with the same Google account you used for OAuth setup.
- If restore fails, your service already keeps a
.backupcopy locally.
🏁 Final Thoughts
With under 200 lines of Kotlin, you now have a full Drive-based backup system — no external servers needed, no user file clutter, and full data privacy.
This approach works great for apps that rely on a single SQLite or Room database and want a “WhatsApp-like” backup experience.
This article was previously published on proandroiddev.com



