Flutterでアプリなどを開発しています。
■AppStore
https://apps.apple.com/jp/app/id1517535550
■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja
■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/
3つの機能を実装しようと取り組みました。この記事では、その中のバックアップ機能を実装する方法を書いていきたいと思います。
■パスワードロック機能実装ついてはこちら
https://qiita.com/YuKiO-OO/items/bf2d1d107d1a66211619
■アプリ内課金についてはこちら
https://qiita.com/YuKiO-OO/items/a0fe8e0a256afbb69fc7
実装する機能
ユーザーがGoogleアカウントでログイン。
バックアップボタンを押すと、ユーザーのGoogleドライブにDBの情報を保存。
リストアボタンを押すと、バックアップしたデータが復元される。
Googleドライブ採用の理由
とりあえずでもGoogleアカウントを持っている人が多いと考えたからです。特に全世界のスマホシェアではAndroidが多く、AndroidユーザーはすべてGoogleアカウント持っていますしね。
その他ストレージはユーザーが限定されるので、選択肢に入れませんでした。
Firebaseを使って、こちら側でストレージを用意してあげる方法もありますが、サービスの維持、仮にサービスを止めることになった場合に、データの扱いが難しいことや、ユーザーデータの管理の観点でもリスクがあるので、今回は採用しませんでした。
参考記事
この記事を参考に構築しました。
GoogleAPI利用の設定:参考
Google Firebase Email/Password And Google Login In Flutter
Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/
特に2番目の記事は、部分部分を抜き出しているだけなので、分かりづらいと思います。
こちらの構成をみて、全体像を把握しておくことをオススメします。
Github
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart
今回はこの記事を元にして書いていきますので、上記、記事を教科書だと思ってください。
この記事と異なる点は、データの取り扱い方です。
教科書では画像ファイルの管理が目的なので、ユーザーが保存データのリストからデータを選んでダウンロードする機能が実装されています。
ただバックアップ機能では、ユーザーにファイルを触らせないので、今回の実装ではバックグランドでファイルの選択をしていきます。
ユーザーは指定された操作のボタンを押すだけになります。
全体の流れ
今回は、参考記事を中心に、実装について詰まったところにフォーカスして書いていきます。
-FirebaseとGoogleドライブのAPIの設定
-Googleアカウントでログイン
-DBのデータを抜き出して、Googleドライブに保存
-Googleドライブに保存されているリストを取得
-保存されているデータをダウンロード、DBを上書き
知っておいたほうがいい知識
Googleドライブの仕様について
今回、Googleドライブを使用しますが、今回の方法は普通にGoogleドライブに保存するわけではありません。
アプリ専用の隠しフォルダが作られます。
その隠しフォルダにデータ保存するのですが、保存したデータのリストも同時に生成されます。
リストにはファイル名は、ファイル固有のIDなどが記載されています。
そのリストのIDからデータをダウンロードしたりします。
ファイル名を指定して、ダウンロードするわけでないので、ご注意ください。
またユーザーはファイルを見る事も、触ることもできません。
DBについて
今回は、DBの操作についてパッケージのMoorを使用しています。
バックアップ実装では、DBの設定が完了しているものします。。。。と初心者の方は、言われても困りますよね。
時間はかかりますが書こうと思うので、取り急ぎ下記の公式ドキュメントに挑戦してみてください。
公式ドキュメント通りにやれば、できるはず。
あなたならできる!
何やっているか、分からないところもあるかもしれませんが、それも学びだったりしますし 汗
https://moor.simonbinder.eu/docs/getting-started/
この方が書いている記事を参考にすれば行けるかなと思います。
https://qiita.com/niusounds/items/e4d731af58201ad5fe6f
Youtubeにもやり方がありました。
https://www.youtube.com/watch?v=zpWsedYMczM&t=387s
他のパッケージでDBを構築されている方は、DBのデータが保存されているディレクトリのパスとファイル名がわかれば、大丈夫だと思います。(だいたい同じところかな?)
注意事項
※20年6月時点での情報をもとに作成しています。
※試行錯誤の結果、まだリファクタリング等できていません。処理が冗長的なところや一部無駄な処理もありますので、ご了承ください。
※iOS側の設定の記述が少ない理由ですが、資料があまり残ってませんでした。おそらくiOSでは、そこまでハマりポイントがなかったと記憶しています。エラー表示を解決するだけで、問題なく進めただけかもしれないですが・・・。記述少なめですが、ご了承ください。
もし、分かりづらいところがあれば遠慮なくコメントなどに残して頂けると助かります。
みなさんと一緒に、Flutterを学習している人のためになる記事を作っていければと思います。
バックアップの実装
firebaseの設定
まず、教科書はこれを使います。
Google Firebase Email/Password And Google Login In Flutter
Firebaseとは、難しいことをしなくても、オンラインでサーバーと連携する必要がある機能を簡単に構築できるサービスです。
それで、Firebaseを使用する理由は、Googleアカウントのログイン状態を管理するためです。
Firebase側が、端末を識別してログインしている状態を判断してくれるので、楽なんです。
今回使うFirebaseのAuthenticationという機能は無料で使えるのですが、月1万(たぶん回数、単位がない)を超えると従量課金にする必要があるようです。
Firebase Pricing
ちなみにFirebase以外に、GoogleDriveのAPIを使うために「google cloud platform」、通称GCPも使っていきます。GoogleのAPIの中には有料なものがありますが、GoogleDriveは無料のようです。
教科書ではEmailの登録をしていますが、Emailの登録はしないので今回はGoogleアカウントだけをオンにすれば大丈夫です。
Emailの実装に関しては飛ばしてください。
教科書ではAndroidしか触れてませんよね。
ただ私の場合、他の機能でもFirebaseと連携するので、iOS側でもFirebaseに登録しています。(必須かは不明)
実装はこちらを参考にしてください。
https://firebase.google.com/docs/flutter/setup?hl=ja
Android側の設定で、追加する場所がいくつか分かりづらかったところを補足します。
Firebaseの設定途中でAndroidは「google-services.json」をダウンロードしますが、
**android/app/**のところに保存してください。
dependencies {
classpath ‘com.android.tools.build:gradle:3.5.0’
classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version”
classpath ‘com.google.gms:google-services:4.3.3’
}
dependencies {
implementation “org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version”
implementation ‘com.google.firebase:firebase-analytics:17.2.2’
}
//下記の位置に追加することになると思います。
apply plugin: ‘com.google.gms.google-services’
にそれぞれ記載します。
教科書では、ログインの処理まで書かれていますが、これは次のGoogleDriveの実装記事にもしっかりと書かれているので、とりあえずFirebaseの設定までで大丈夫です。
Google Drive APIの設定
ここからは、下記の教科書を使います。
Googleドライブの実装:参考
https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/
補足でこのブログの作者のGithubを見ておくと、全体像が掴みやすいと思います。
オリジナルで画面を作りたい場合、とりあえずGithub通りに作ってから、部分部分のデザインを変更したほうが楽かもしれません。
https://github.com/myvsparth/flutter_gdrive/blob/master/lib/main.dart
実装方法は、STEPで順に説明されていますが、補足していきたいと思います。
STEP3の補足事項
さらっとAPIの設定しておいてね!と言われますが、補足だけしておきます。
firebaseを登録して、同じアカウントでGCP(Google Cloud Platfom APIのサービス)に登録すると、Firebaseのプロジェクトが紐づいています。
そのプロジェクトを選択して、下記の記事の通りすれば問題なくできると思います。
OAuth 同意画面などは設定しました。しかし、Outhクライアントをどうしても作ることができませんでした。ただ問題なく動作できているので、この操作は不要なのかもしれません。(おそらくテスト用するためのアカウントなのだと思いますが、自分のGoogleアカウントを利用して問題ありませんでした)
STEP4の補足事項
一応念のため、パッケージは最新のものをインストール。6月時点ではパッケージのバージョンによる不具合はありませんでした。
STEP5の補足
アンドロイド側で、STEP5を追加する場合、SdkVersionが対応していないというエラーが出るかもしれません。この場合minSdkVersionのバージョンをあげることで、エラーが解消されました。
defaultConfig {
applicationId "アプリのIDが入ります"
minSdkVersion 21 //ここが21以上でエラーが消えました。
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
STEP6の補足
これはStatefulウィジェットの外側に記載するので、くれぐれもClass内に書かないようにご注意ください。
STEP7 ログインの補足
基本この通りに書いて行けば問題ありません。この処理ではfirebaseを使ってユーザーのログイン状態を確認しています。
通常Webであればセッション等でログイン中を確認していますが、firebaseでは端末を識別して、ログイン状態を把握しているそうです。
https://firebase.google.com/docs/auth?hl=ja
なので、一度ログインしてアプリを閉じても、ログイン状態を維持します。
STEP7 ログアウトの補足
ログアウト処理についても、そのまま使用させてもられば大丈夫です。
STEP7のその他について
この記事に書いてあるその他実装については、バックアップとは関係ないためそのまま流用できません。下記に修正した処理を記載していきます。
バックアップ機能の実装(SETP7を改良)
バックアップ機能は、全体の流れで説明した通りです。保存されたファイルにはそれぞれ固有のIDがふられていて、リストで管理されています。ここでは直接ファイルを触るというよりは、保存されているファイルのリストを元に、ファイルを管理する形になります。
まず、リストを取得するメソッドを作成します。
Googleドライブ内のアプリ専用領域から保存されているファイル情報をリストで取得
Future<void> _listGoogleDriveFiles() async {
var client = GoogleHttpClient(
await googleSignInAccount.authHeaders); //承認情報を取得
var drive = ga.DriveApi(client); //GoogleDrive APIにアクセス
//このアプリ専用のフォルダスペースから保存しているファイル情報(データじゃない)を取得。
drive.files.list(spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)').then((value) {
setState(() {
list = value; //ファイルのリスト情報を取得
lastUpdateTime = list.files[0].modifiedTime.toLocal();//最新更新日付をセットします。
});
});
}
教科書ではログイン後にボタンを押したらこの処理を呼ぶようになっていますが、このリストはバックグランドで取得できればいいので、ログインが確認できた時点で呼ぶようにしています。
つぎはアップロードですが、DB情報を保存してるディレクトリを取得してファイルをアップロードしていきます。教科書を元に変更しています。
Googleドライブへファイルをアップロード
//これは関数でclass外に書いてます。
//DBが保存されているディレクトリのパスを取得する処理
Future<String> get getDbPath async {
final dbDir = await getApplicationDocumentsDirectory();//ファイルが保存されている領域のパスを取得
final dbPath = join(dbDir.path, 'defult.db');//保存されているDBのファイル名を調べて、取得したパスと合体させて絶対パスにする。
return dbPath;
}
//class外処理終了
//ここらからstatefulウィジェット内の処理
_uploadFileToGoogleDrive() async {
//ローディング画面を表示。別の操作をされたくないので。
showGeneralDialog(
context: context,
barrierDismissible: false,
transitionDuration: Duration(seconds: 2),
barrierColor: Colors.black.withOpacity(0.5),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return Center(
child: CircularProgressIndicator(),
);
}
);
//ファイルのアップロードを始めます。
var client = GoogleHttpClient(await googleSignInAccount.authHeaders);//承認情報を取得
var drive = ga.DriveApi(client);//APIヘアクセス
drive.files.list(spaces: 'appDataFolder');
ga.File fileToUpload = ga.File();//ドライブ用のファイルのインスタンスを作成
var filePath = await getDbPath;//DBファイルのパスを関数で取得
await _listGoogleDriveFiles();//Googleドライブのリストを取得
//保存するファイルの加工
var file = await File(filePath);//ファイルをセット
fileToUpload.name = path.basename(filePath);//ファイルの名前をセット
fileToUpload.modifiedTime = DateTime.now().toUtc();//アップロードの日付
fileToUpload.parents = ["appDataFolder"];//アプリ専用フォルダを指定
//常に保存されるバックアップファイルは一つにしたいので、以下の処理を入れています。
//すでにファイルがある場合
//すべてのファイルを事前に削除。エラーで複数ある場合もあるので。
if (list.files.length > 0) {
for (var i = 0; i < list.files.length; i++) {
ga.Media file = await drive.files //削除用
.delete(list.files[i].id);
}
print('ファイルを削除しました。');
}
//ファイルのアップロード処理
var response;
response = await drive.files.create(
fileToUpload,
uploadMedia: ga.Media(file.openRead(), file.lengthSync()),
);
await _listGoogleDriveFiles();//リストの再取得
await Future.delayed(Duration(seconds: 1));
Navigator.pop(context);//ローディング画面を消す
//デバック用
print("ファイル保存が完了しました。レンスポンスは$response");
}
ここではDBのファイルがあるディレクトリのパスを指定してから、ファイルをアップロードするために情報を加工してアップロードしています。
教科書では日付を取得していませんが、今回バックアップした日付を表示したかったので、日付も保存しています。
どのような項目があるかはこちらでチェックできます。
https://developers.google.com/drive/api/v3/reference/files
「toUtc()」で世界協定時にしているのは、googleのapiが受け付ける形式が世界協定時だからです。
Googleドライブからダウンロード
Future<void> _downloadGoogleDriveFile() async {
//処理中は触ってほしくないのでローディング画面を表示
showGeneralDialog(
context: context,
barrierDismissible: false,
transitionDuration: Duration(seconds: 1),
barrierColor: Colors.black.withOpacity(0.5),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return Center(
child: CircularProgressIndicator(),
);
}
);
//ダウンロードを開始します。
//いつもの処理がスタートします。
var client = GoogleHttpClient(await googleSignInAccount.authHeaders);
//常にファイルは1つのなので、リストの先頭のIDで取得。
//IDはファイルそれぞれ固有の番号がふられています。
var drive = ga.DriveApi(client);
ga.Media file = await drive.files
.get(list.files[0].id,
downloadOptions: ga.DownloadOptions.FullMedia);
final filePath = await getDbPath; //ディレクトり取得
final saveFile = File(filePath); //ファイルの保存場所
//ここは教科書通りに。
List<int> dataStore = [];
file.stream.listen((data) {
dataStore.insertAll(dataStore.length, data);
}, onDone: () async {
await saveFile.writeAsBytes(dataStore);
_listGoogleDriveFiles();//リストを再取得
await Future.delayed(Duration(seconds: 1));
Navigator.pop(context); //ローディングを閉じる
//バックアップをダウンロードして、保存が成功した場合の処理
}, onError: (error) {
//エラー処理
});
}
バックアップファイルは常に1つしかないので、リストの先頭のみを引っ張ってきます。
処理はアップロードと共通でDBのファイルパスをとってきて、そこに保存します。
GoogleDriveのデータを削除
ユーザー自らデータを削除したい場合もあると思うので、一応削除ボタンも作っておきます。
Future<void> _deleteFileToGoogleDrive() async{
//実行中は触らせたくないので、ローディング画面
showGeneralDialog(
context: context,
barrierDismissible: false,
transitionDuration: Duration(seconds: 1),
barrierColor: Colors.black.withOpacity(0.5),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return Center(
child: CircularProgressIndicator(),
);
}
);
_listGoogleDriveFiles(); //保存リストを取得。
var client = GoogleHttpClient(await googleSignInAccount.authHeaders);
var drive = ga.DriveApi(client); //GoogleDriveのAPIに接続
//繰り返しでリストの全ファイルを削除
for (var i = 0; i < list.files.length; i++) {
ga.Media file = await drive.files
.delete(list.files[i].id);
}
await _listGoogleDriveFiles();//ファイルを再取得
setState(() {
lastUpdateTime = null; //表示用更新日付の空に
});
await Future.delayed(Duration(seconds: 1));
Navigator.pop(context); //ローディング画面を閉じる
//削除完了後の処理を書く
}
基本的にリストで取得して、その中にあるファイルをすべて繰り返し処理で削除するようにしています。
まとめ
いかがでしたか?
バックアップの概要がわかるまでは意味不明かと思いますが、どのように動作しているかわかってくれば、そこまで難しいものではないと思います。(改めてコードを見返してみると、リファクタリングしないとなと実感してます笑)
分からないところがあれば、コメントください。
どのように動作しているかは、アプリをダウンロードしてチェックしてみてください。
■AppStore
https://apps.apple.com/jp/app/id1517535550
■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja
またツイッターでもFlutterや個人開発についていろいろ呟いています。
チェックください!
Webサービスやアプリ!
— YuKiO | 個人開発&Flutter (@oo_forward) July 2, 2020
斬新なアイデアをひらめきたい人に!
アイデアを発想するためのメモアプリ✍️
IdeaShuffleMemoをリリースしました!
■AppStorehttps://t.co/gEPzEEJ7mt
■Google Playhttps://t.co/w0vTiOanGE
使ってみてください😁#駆け出しエンジニアと繋がりたい #プログラミング pic.twitter.com/gQ0dOMNgGB