Table of contents
Backing up app data is a common action for mobile app. This article will show the steps about backing up Android and IOS files to Google Drive.
Google SignIn
The app data read and write scope require Google Account SignIn. The detail implementation may reference to Google SignIn without Firebase.
Enable Drive API
Goto Enabled APIs & Services
on the side menu and click + ENABLE APIS AND SERVICES
.
Search google drive api
.
Click Enable Google Drive API
.
Flutter Project Config
Run flutter pub add googleapis
to add googleapis plugin to the project.
Create a class called GoogleDriveAppData
. The sign in logic will also put inside this class. Since we are going to use the app data storage in Google Drive, we will add the scope to drive.DriveApi.driveAppdataScope
.
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;
class GoogleDriveAppData {
/// sign in with google
Future<GoogleSignInAccount?> signInGoogle() async {
GoogleSignInAccount? googleUser;
try {
GoogleSignIn googleSignIn = GoogleSignIn(
scopes: [
drive.DriveApi.driveAppdataScope,
],
);
googleUser =
await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
} catch (e) {
debugPrint(e.toString());
}
return googleUser;
}
///sign out from google
Future<void> signOut() async {
GoogleSignIn googleSignIn = GoogleSignIn();
await googleSignIn.signOut();
}
}
To use the drive API with its client, we will create a GoogleAuthClient
class for passing the auth info with the drive API request.
Run flutter pub add http
to import the http package.
import 'package:http/http.dart' as http;
class GoogleAuthClient extends http.BaseClient {
final Map<String, String> _headers;
final _client = http.Client();
GoogleAuthClient(this._headers);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
request.headers.addAll(_headers);
return _client.send(request);
}
}
Go back to the GoogleDriveAppData
class.
Add a function to create the Drive client instance.
import 'package:google_drive_app_data/google_auth_client.dart';
class GoogleDriveAppData {
...
///get google drive client
Future<drive.DriveApi?> getDriveApi(GoogleSignInAccount googleUser) async {
drive.DriveApi? driveApi;
try {
Map<String, String> headers = await googleUser.authHeaders;
GoogleAuthClient client = GoogleAuthClient(headers);
driveApi = drive.DriveApi(client);
} catch (e) {
debugPrint(e.toString());
}
return driveApi;
}
}
Google Drive has an app data folder especially for the app’s file storage. The file will save to the appDataFolder
.
Run flutter pub add path
to add path package for easily get file’s base name.
drive.File
is storing the file’s information the drive having.
io.File
is the io file which is the local file the device having.
If the file is previously uploaded to the drive, we can use the driveFileId
to update the latest version file to the drive.
import 'dart:io' as io;
import 'package:path/path.dart' as path;
class GoogleDriveAppData {
...
/// upload file to google drive
Future<drive.File?> uploadDriveFile({
required drive.DriveApi driveApi,
required io.File file,
String? driveFileId,
}) async {
try {
drive.File fileMetadata = drive.File();
fileMetadata.name = path.basename(file.absolute.path);
late drive.File response;
if (driveFileId != null) {
/// [driveFileId] not null means we want to update existing file
response = await driveApi.files.update(
fileMetadata,
driveFileId,
uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
);
} else {
/// [driveFileId] is null means we want to create new file
fileMetadata.parents = ['appDataFolder'];
response = await driveApi.files.create(
fileMetadata,
uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
);
}
return response;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
}
To get the file information of drive like id, version and modified time, we can use the following function to get the file list and compare the filename with the uploaded files.
class GoogleDriveAppData {
...
/// get drive file info
Future<drive.File?> getDriveFile(
drive.DriveApi driveApi, String filename) async {
try {
drive.FileList fileList = await driveApi.files.list(
spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)');
List<drive.File>? files = fileList.files;
drive.File? driveFile =
files?.firstWhere((element) => element.name == filename);
return driveFile;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
}
To download the drive file to local, we can use the following function. Basically download the data stream and write to a new file.
class GoogleDriveAppData {
...
/// download file from google drive
Future<io.File?> restoreDriveFile({
required drive.DriveApi driveApi,
required drive.File driveFile,
required String targetLocalPath,
}) async {
try {
drive.Media media = await driveApi.files.get(driveFile.id!,
downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media;
List<int> dataStore = [];
await media.stream.forEach((element) {
dataStore.addAll(element);
});
io.File file = io.File(targetLocalPath);
file.writeAsBytesSync(dataStore);
return file;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
}
The complete GoogleDriveAppData
class will be like this:
import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:google_drive_app_data/google_auth_client.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;
import 'package:path/path.dart' as path;
class GoogleDriveAppData {
/// sign in with google
Future<GoogleSignInAccount?> signInGoogle() async {
GoogleSignInAccount? googleUser;
try {
GoogleSignIn googleSignIn = GoogleSignIn(
scopes: [
drive.DriveApi.driveAppdataScope,
],
);
googleUser =
await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
} catch (e) {
debugPrint(e.toString());
}
return googleUser;
}
///sign out from google
Future<void> signOut() async {
GoogleSignIn googleSignIn = GoogleSignIn();
await googleSignIn.signOut();
}
///get google drive client
Future<drive.DriveApi?> getDriveApi(GoogleSignInAccount googleUser) async {
drive.DriveApi? driveApi;
try {
Map<String, String> headers = await googleUser.authHeaders;
GoogleAuthClient client = GoogleAuthClient(headers);
driveApi = drive.DriveApi(client);
} catch (e) {
debugPrint(e.toString());
}
return driveApi;
}
/// upload file to google drive
Future<drive.File?> uploadDriveFile({
required drive.DriveApi driveApi,
required io.File file,
String? driveFileId,
}) async {
try {
drive.File fileMetadata = drive.File();
fileMetadata.name = path.basename(file.absolute.path);
late drive.File response;
if (driveFileId != null) {
/// [driveFileId] not null means we want to update existing file
response = await driveApi.files.update(
fileMetadata,
driveFileId,
uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
);
} else {
/// [driveFileId] is null means we want to create new file
fileMetadata.parents = ['appDataFolder'];
response = await driveApi.files.create(
fileMetadata,
uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
);
}
return response;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
/// download file from google drive
Future<io.File?> restoreDriveFile({
required drive.DriveApi driveApi,
required drive.File driveFile,
required String targetLocalPath,
}) async {
try {
drive.Media media = await driveApi.files.get(driveFile.id!,
downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media;
List<int> dataStore = [];
await media.stream.forEach((element) {
dataStore.addAll(element);
});
io.File file = io.File(targetLocalPath);
file.writeAsBytesSync(dataStore);
return file;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
/// get drive file info
Future<drive.File?> getDriveFile(
drive.DriveApi driveApi, String filename) async {
try {
drive.FileList fileList = await driveApi.files.list(
spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)');
List<drive.File>? files = fileList.files;
drive.File? driveFile =
files?.firstWhere((element) => element.name == filename);
return driveFile;
} catch (e) {
debugPrint(e.toString());
return null;
}
}
}
Result
Let’s make a simple widget with sign in and call the upload function.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GoogleDriveAppData _googleDriveAppData = GoogleDriveAppData();
GoogleSignInAccount? _googleUser;
drive.DriveApi? _driveApi;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GoogleAuthButton(
onPressed: () async {
if (_googleUser == null) {
_googleUser = await _googleDriveAppData.signInGoogle();
if (_googleUser != null) {
_driveApi =
await _googleDriveAppData.getDriveApi(_googleUser!);
}
} else {
await _googleDriveAppData.signOut();
_googleUser = null;
_driveApi = null;
}
setState(() {});
},
),
ElevatedButton(
onPressed: _driveApi != null
? () {
FilePicker.platform.pickFiles().then((value) {
if (value != null && value.files[0] != null) {
File selectedFile = File(value.files[0].path!);
_googleDriveAppData.uploadDriveFile(
driveApi: _driveApi!,
file: selectedFile,
);
}
});
}
: null,
child: Text('Save sth to drive'),
),
],
),
),
);
}
}
After the upload success, if you goto the drive web portal and click setting.
Choose Manage Apps
from the side bar, you will find the app appear in the app list.
Notice: if you signed in once in the previous article which means your account does not included the drive app data permission may causing 403 when upload file. You need to sign out once and sign in or other methods to let the account allow the app data permission.