Prerequisite
- Know basic dart grammar
- Know basic Flutter workflow
- Have a google developer account
- Have an Apple developer account
- Registered the app on your target platform
Android : Set up on a Google Cloud Console
- Go to Google Cloud Platform and select your project
- On a search bar, type
google drive
and select below.
- Click Enable.
IOS : Set up on an Apple App Store Connect
- Go to Apple Developer and login
-
On your account page, select
Certificates, IDs & Profiles
→Identifiers
, make sure you have you app id.
-
On right side dropdown, select
iCloud Containers
-
Click blue plus button to create iCloud Container identifier.
-
Note that your iCloud Container identifier should have a form like below.
iCloud.com.example.yourappname
-
Save.
-
On right side dropdown, select
App IDs
-
Select your target app id to edit.
-
On
Capabilities
, check iCloud and click Edit button -
Select your iCloud Container identifier
-
Save.
Code Part
Install packages
-
For Google Drive
flutter pub add google_sign_in flutter pub add googleapis flutter pub add http
-
For iCloud
flutter pub add icloud_storage
-
Common
flutter pub add path_provider
BaseCloud class
Define methods that should be implemented on both side.
// base-cloud-repository.dart
import 'package:flutter/foundation.dart';
import 'package:googleapis/drive/v3.dart';
import 'package:http/http.dart';
class BaseCloudRepository {
Client? client;
dynamic cloud;
BaseCloudRepository();
initGoogleDriveCloud(Client newClient) {
client = newClient;
cloud = DriveApi(client!);
}
clearData() {
client = null;
cloud = null;
}
isBackupExist({String? path}) {}
upload(String jsonString, {String? path}) {}
download({String? path}) {}
}
Set Constants
// config.dart
final Map<String, dynamic> config = {
'api': {
...
},
'auth': {
...
},
'cloud': {
'common': {
'DRIVE_BACKUP_DIR_PARENT': 'appDataFolder',
'DRIVE_BACKUP_FILE_NAME': 'flutter_app_starter__backup',
'DRIVE_BACKUP_FILE_EXT': 'json',
},
'apple': {'ICLOUD_CONTAINER_ID': 'iCloud.com.example.flutter_app_starter'},
'google': {}
},
'payment': {
...
}
};
Google Drive implementation
First read about the full code and I’ll explain method by method with inline comments.
// google-drive/google-auth.dart
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart';
class GoogleAuth {
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: [
'https://www.googleapis.com/auth/drive.file',
DriveApi.driveAppdataScope
]);
logout() async {
await _googleSignIn.signOut();
}
}
// google-drive/repository.dart
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:googleapis/drive/v3.dart';
import 'package:collection/collection.dart';
import '../../../../config/config.dart';
import '../../../util/json-converter.dart';
import '../base-cloud-repository.dart';
class GoogleDriveRepository extends BaseCloudRepository {
GoogleDriveRepository() : super();
initGoogleDriveCloud(Client newClient) {
client = newClient;
cloud = DriveApi(client!);
if (kDebugMode) {
print('cloud : set');
}
}
@override
Future<File?> isBackupExist({String? path}) async {
final driveFileList = await (cloud as DriveApi)
.files
.list(spaces: config['cloud']['common']['DRIVE_BACKUP_DIR_PARENT']);
final isExist = driveFileList.files?.firstWhereOrNull((element) =>
element.name == config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
if (isExist == null) {
return null;
}
return isExist;
}
@override
upload(String jsonString, {String? path}) async {
final filePath = path ?? (await getApplicationDocumentsDirectory()).path;
final jsonFile = await JsonFile.stringToJsonFile(jsonString,
dirPath: filePath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
final fileToUpload = File();
fileToUpload.name = config['cloud']['common']['DRIVE_BACKUP_FILE_NAME'];
final existFile = await isBackupExist();
try {
if (existFile != null) {
await (cloud as DriveApi).files.update(fileToUpload, existFile.id!,
uploadMedia: Media(jsonFile.openRead(), jsonFile.lengthSync()));
} else {
fileToUpload.parents = [
config['cloud']['common']['DRIVE_BACKUP_DIR_PARENT']
];
await (cloud as DriveApi).files.create(fileToUpload,
uploadMedia: Media(jsonFile.openRead(), jsonFile.lengthSync()));
}
if (kDebugMode) {
print('cloud : uploaded');
}
} catch (err) {
if (kDebugMode) {
print(err);
throw Exception('failed to upload to google drive');
}
}
}
@override
Future<String?> download({String? path}) async {
final existFile = await isBackupExist();
if (existFile == null) {
return null;
}
final Media driveFile = await cloud.files.get(existFile.id!,
downloadOptions: DownloadOptions.fullMedia) as Media;
final jsonFileString = await JsonFile.streamToJson(driveFile.stream,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
return jsonFileString;
}
}
1) Google Sign In
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: [
'https://www.googleapis.com/auth/drive.file',
DriveApi.driveAppdataScope
]);
In order to use google drive service, you need for user to login with their google account. Make sure you use these scopes. The backup file of the app will be placed in App Data.
2) Google Drive : Create method for checking exist backup file
@override
Future<File?> isBackupExist({String? path}) async {
// 1. list all files in given path(path when you used on upload)
final driveFileList = await (cloud as DriveApi)
.files
.list(spaces: config['cloud']['common']['DRIVE_BACKUP_DIR_PARENT']);
// 2. check if backup file is exist
final isExist = driveFileList.files?.firstWhereOrNull((element) =>
element.name == config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
// 3. return file if exist, or return null
if (isExist == null) {
return null;
}
return isExist;
}
3) Google Drive : Create method for upload
@override
upload(String jsonString, {String? path}) async {
final filePath = path ?? (await getApplicationDocumentsDirectory()).path;
// 1. make your json data to json file
final jsonFile = await JsonFile.stringToJsonFile(jsonString,
dirPath: filePath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
// 2. instanciate File(this is not from io, but from googleapis
final fileToUpload = File();
// 3. set file name for file to upload
fileToUpload.name = config['cloud']['common']['DRIVE_BACKUP_FILE_NAME'];
// 4. check if the backup file is already exist
final existFile = await isBackupExist();
try {
// 5. if exist, call update
if (existFile != null) {
await (cloud as DriveApi)
.files.update(
fileToUpload,
existFile.id!,
uploadMedia: Media(
jsonFile.openRead(),
jsonFile.lengthSync()
)
);
} else {
// 6. if is not exist, set path for file and call create
fileToUpload.parents = [
config['cloud']['common']['DRIVE_BACKUP_DIR_PARENT']
];
await (cloud as DriveApi)
.files.create(
fileToUpload,
uploadMedia: Media(
jsonFile.openRead(),
jsonFile.lengthSync()
)
);
}
if (kDebugMode) {
print('cloud : uploaded');
}
} catch (err) {
// 7. catch any error while uploading process
if (kDebugMode) {
print(err);
throw Exception('failed to upload to google drive');
}
}
}
4) Google Drive : Create method for download
@override
Future<String?> download({String? path}) async {
// 1. check backup before download
final existFile = await isBackupExist();
if (existFile == null) {
return null;
}
// 2. get drive file
final Media driveFile = await cloud.files.get(
existFile.id!,
downloadOptions: DownloadOptions.fullMedia
) as Media;
// 3. read stream from Google Drive to json string
// In there, I used JsonFile(I didn't mentioned in there)
// You can use any json package you want to use
final jsonFileString = await JsonFile.streamToJson(
driveFile.stream,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']
);
return jsonFileString;
}
Apple ICloud implementation
Also, read about the full code and I’ll explain method by method with inline comments.
// apple-icloud/repository.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:icloud_storage/icloud_storage.dart';
import '../../../../config/config.dart';
import '../../../util/json-converter.dart';
import '../base-cloud-repository.dart';
class AppleICloudRepository extends BaseCloudRepository {
ICloudStorage? _icloud;
AppleICloudRepository() : super();
_precheck() async {
_icloud ??= await ICloudStorage.getInstance(
config['cloud']['common']['ICLOUD_CONTAINER_ID']);
}
@override
upload(String jsonString, {String? path}) async {
await _precheck();
StreamSubscription<double>? _subscription;
Future<dynamic>? _subscriptionFuture;
bool _isDone = false;
final dirPath = path ?? (await getApplicationDocumentsDirectory()).path;
final jsonFile = await JsonFile.stringToJsonFile(jsonString,
dirPath: dirPath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
await _icloud!.startUpload(
filePath: jsonFile.path,
destinationFileName:
'${config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']}.json',
onProgress: (stream) {
_subscription = stream.listen((progress) {
if (kDebugMode) {
print('upload progress : $progress');
}
}, onDone: () {
if (kDebugMode) {
_isDone = true;
print('upload done!');
}
}, onError: (err) {
if (kDebugMode) {
print('upload error : $err');
}
}, cancelOnError: true);
_subscriptionFuture = _subscription!.asFuture();
});
Future.delayed(const Duration(seconds: 7), () {
if (!_isDone) {
_subscription?.cancel();
}
});
await Future.wait([_subscriptionFuture!]);
if (kDebugMode) {
print('ios cloud : upload start');
}
return dirPath;
}
@override
Future<String?> download({String? path}) async {
await _precheck();
StreamSubscription<double>? _subscription;
Future<dynamic>? _subscriptionFuture;
bool _isDone = false;
final filePath = (await getApplicationDocumentsDirectory()).path;
try {
await _icloud!.startDownload(
fileName:
'${config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']}.json',
destinationFilePath: JsonFile.getFullPath(
filePath, config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']),
onProgress: (stream) {
_subscription = stream.listen((progress) {
if (kDebugMode) {
print('download progress : $progress');
}
}, onDone: () {
if (kDebugMode) {
_isDone = true;
print('download done!');
}
}, onError: (err) {
if (kDebugMode) {
print('download error : $err');
}
}, cancelOnError: true);
_subscriptionFuture = _subscription!.asFuture();
});
Future.delayed(const Duration(seconds: 7), () async {
if (!_isDone) {
_isDone = true;
_subscription?.cancel();
}
});
await Future.wait([_subscriptionFuture!]);
final jsonString = await JsonFile.readFileAsJson(
dirPath: path ?? filePath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
if (kDebugMode) {
print('ios cloud : downloaded from icloud dir');
}
return jsonString;
} catch (e) {
if (kDebugMode) {
print(e);
}
return null;
}
}
}
1) Apple iCloud : Create check method for iCloud instance
_precheck() async {
_icloud ??= await ICloudStorage.getInstance(
config['cloud']['common']['ICLOUD_CONTAINER_ID']);
}
Make sure you have iCloud instance before accessing it.
2) Apple iCloud : Create method for upload
@override
upload(String jsonString, {String? path}) async {
// 1. check you have instance
await _precheck();
// 2. create StreamSubscription to handle file stream from iCloud
StreamSubscription<double>? _subscription;
// 3. for checking is subscription end
Future<dynamic>? _subscriptionFuture;
bool _isDone = false;
final dirPath = path ?? (await getApplicationDocumentsDirectory()).path;
final jsonFile = await JsonFile.stringToJsonFile(jsonString,
dirPath: dirPath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
await _icloud!.startUpload(
filePath: jsonFile.path,
destinationFileName:
'${config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']}.json',
onProgress: (stream) {
// 4. set _subscription
_subscription = stream.listen((progress) {
if (kDebugMode) {
print('upload progress : $progress');
}
// 5. success
}, onDone: () {
if (kDebugMode) {
_isDone = true;
print('upload done!');
}
// 6. on error, cancel the stream
}, onError: (err) {
if (kDebugMode) {
print('upload error : $err');
}
}, cancelOnError: true);
// 7. mark as stream started
_subscriptionFuture = _subscription!.asFuture();
});
// 8. check after 7 seconds, if stream is still not ended, just cancel
Future.delayed(const Duration(seconds: 7), () {
if (!_isDone) {
// 9. this will end stream, so _subscriptionFuture also will be end
_subscription?.cancel();
}
});
// 10. wait for _subscriptionFuture to end(success or error)
await Future.wait([_subscriptionFuture!]);
if (kDebugMode) {
print('ios cloud : upload start');
}
return dirPath;
}
3) Apple iCloud : Create method for download
@override
Future<String?> download({String? path}) async {
// 1. check you have instance
await _precheck();
// 2. create StreamSubscription to handle file stream from iCloud
StreamSubscription<double>? _subscription;
// 3. for checking is subscription end before accessing a downloaded file
Future<dynamic>? _subscriptionFuture;
bool _isDone = false;
final filePath = (await getApplicationDocumentsDirectory()).path;
try {
await _icloud!.startDownload(
fileName:
'${config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']}.json',
destinationFilePath: JsonFile.getFullPath(
filePath, config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']),
// 4. set _subscription
onProgress: (stream) {
_subscription = stream.listen((progress) {
if (kDebugMode) {
print('download progress : $progress');
}
// 5. success
}, onDone: () {
if (kDebugMode) {
_isDone = true;
print('download done!');
}
// 6. on error, cancel the stream
}, onError: (err) {
if (kDebugMode) {
print('download error : $err');
}
}, cancelOnError: true);
// 7. mark as stream started
_subscriptionFuture = _subscription!.asFuture();
});
// 8. check after 7 seconds, if stream is still not ended, just cancel
Future.delayed(const Duration(seconds: 7), () async {
if (!_isDone) {
_isDone = true;
// 9. this will end stream, so _subscriptionFuture also will be end
_subscription?.cancel();
}
});
// 10. wait for _subscriptionFuture to end(success or error)
await Future.wait([_subscriptionFuture!]);
// 11. read downloaded file content
// In there, I used JsonFile(I didn't mentioned in there)
// You can use any json package you want to use
final jsonString = await JsonFile.readFileAsJson(
dirPath: path ?? filePath,
fileName: config['cloud']['common']['DRIVE_BACKUP_FILE_NAME']);
if (kDebugMode) {
print('ios cloud : downloaded from icloud dir');
}
return jsonString;
} catch (e) {
// 12. catch any error occured on using iClould package
if (kDebugMode) {
print(e);
}
return null;
}
}
Conclusion
That’s it! If you want to see the full source code, you can find them here.