Flutterから利用できるFirebaseサービスをカウンターアプリで実践(Cloud Storage編)
はじめに
Flutterから利用できるFirebaseの機能をカウンターアプリに実装し、内容をまとめたシリーズの第8回目です。
今回はユーザーが作成した写真やビデオのようなコンテンツを保存するCloud Storage for Firebaseについてです。
目次
シリーズの内容
回数 | 内容 | リンク |
---|---|---|
第0回 | 準備編 | ブログ |
第1回 | Analytics |
ブログ |
第2回 | Firebase Crashlytics |
ブログ |
第3回 | Firebase Performance Monitoring |
ブログ |
第4回 | Firebase Remote Config |
ブログ |
第5回 | Firebase Authentication |
ブログ |
第6回 | Cloud Firestore |
ブログ |
第7回 | Firebase Realtime Database |
ブログ |
第8回 | Cloud Storage for Firebase |
イマココ |
第9回 | Firebase Cloud Messaging |
ブログ |
第10回 | Firebase In-App Messaging |
ブログ |
第11回 | Firebase ML |
ブログ |
第12回 | Cloud Functions for Firebase |
ブログ |
第13回 | Firebase Hosting |
ブログ |
第14回 | Firebaseのその他のサービス | ブログ |
開発環境
項目 | 内容 |
---|---|
PC | Macbook Air(M1) |
Flutter | 3.0.1 |
Firebase | 11.0.1 |
FlutterFire | 0.2.2+2 |
デバッグデバイス | Android 12(APIレベル31) , Chrome |
準備
準備編が完了出来ているものとします。
導入方法
Firebase ConsoleからStorage
を選択し、開始します。
前回と前々回の記事にも書きましたが、Cloud StorageでもFirebaseセキュリティルールを設定する必要があります。
今回はコンテンツ所有者のみがアクセスできるように、以下の通り設定しました。
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /users/{userId}/{allPaths=**} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
Firebase Console
での設定が終わったら、プロジェクトにfirebase_storageを導入するため、pubspec.yaml
に追記しインポートします。
今回はAndroid
で画像のアップロードを行うため、image_picker
もまとめてインポートします。
なお、web
はdart:io
パッケージに対応していないため、以降のコードは利用できません。
dependencies: image_picker: ^0.8.5 firebase_storage: ^10.2.16
画像のCloud Storageへのアップロードはimage_picker
で画像を選択し、putFile
を使います。
import 'package:image_picker/image_picker.dart'; import 'package:firebase_storage/firebase_storage.dart'; /// ユーザIDの取得 final userID = FirebaseAuth.instance.currentUser?.uid ?? ''; void uploadPic() async { try { /// 画像を選択 final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); File file = File(image!.path); /// Firebase Cloud Storageにアップロード String uploadName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$uploadName'); final task = await storageRef.putFile(file); } catch (e) { print(e); }
アップロードの管理として、アップロードを一時停止、再開、キャンセルすることができます。 このほか、アップロードの進行状況を監視することもできます。
/// アップロードの一時停止 bool paused = await task.pause(); print('paused, $paused'); /// アップロードの再開 bool resumed = await task.resume(); print('resumed, $resumed'); /// アップロードのキャンセル bool canceled = await task.cancel(); print('canceled, $canceled');
画像がアップロード出来たかの確認は、Firebase Consoleから可能です。
画像をCloud Storageからダウンロードするには、メモリにダウンロードする方法とローカルに直接ダウンロードする方法の2種類があります。
今回はメモリにダウンロードし、画像をアプリ内で表示するようにしました。
なお、読み書きにはRiverpod
を使用しています。
final imageStateProvider = StateProvider<Uint8List?>((ref) => null); /// 画像のダウンロード void downloadPic(WidgetRef ref) async { try { /// 参照の作成 String downloadName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$downloadName'); /// 画像をメモリに保存し、Uint8Listへ変換 const oneMegabyte = 1024 * 1024; ref.read(imageStateProvider.state).state = await storageRef.getData(oneMegabyte); } catch (e) { print(e); } }
表示されれば、ダウンロードが出来ています。
Cloud Storageのファイルの削除にはdelete()
を使用します。
/// 画像の削除 void deletePic() async { /// 参照の作成 String deleteName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$deleteName'); /// Cloud Storageから削除 await storageRef.delete(); }
コード全文
前回からの変更点です。
Cloud Storage
を実装- Cloud Storageのページを追加
- その他、コードの変更
main.dart
抜粋
/// Flutter関係のインポート import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:async'; /// Firebase関係のインポート import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; import 'package:counter_firebase/cloud_storage.dart'; import 'package:counter_firebase/firestore_page.dart'; import 'package:counter_firebase/realtime_database_page.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_auth/firebase_auth.dart'; /// 他ページのインポート import 'package:counter_firebase/normal_counter_page.dart'; import 'package:counter_firebase/crash_page.dart'; import 'package:counter_firebase/auth_page.dart'; import 'package:counter_firebase/remote_config_page.dart'; /// プラットフォームの確認 final isAndroid = defaultTargetPlatform == TargetPlatform.android ? true : false; final isIOS = defaultTargetPlatform == TargetPlatform.iOS ? true : false; /// メイン void main() async { /// クラッシュハンドラ runZonedGuarded<Future<void>>(() async { /// Firebaseの初期化 WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( name: isAndroid || isIOS ? 'counterFirebase' : null, options: DefaultFirebaseOptions.currentPlatform, ); /// クラッシュハンドラ(Flutterフレームワーク内でスローされたすべてのエラー) FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; /// runApp w/ Riverpod runApp(const ProviderScope(child: MyApp())); }, /// クラッシュハンドラ(Flutterフレームワーク内でキャッチされないエラー) (error, stack) => FirebaseCrashlytics.instance.recordError(error, stack, fatal: true)); } /// Providerの初期化 /// カウンター用のプロバイダー final counterProvider = StateNotifierProvider.autoDispose<Counter, int>((ref) { return Counter(); }); class Counter extends StateNotifier<int> { Counter() : super(0); /// カウントアップ void increment() => state++; } /// MaterialAppの設定 class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Counter Firebase', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), debugShowCheckedModeBanner: false, ); } } /// ホーム画面 class MyHomePage extends ConsumerWidget { const MyHomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { /// ログイン状態の確認 FirebaseAuth.instance.authStateChanges().listen( (User? user) { if (user == null) { ref.watch(userEmailProvider.state).state = 'ログインしていません'; } else { ref.watch(userEmailProvider.state).state = user.email!; } }, ); return Scaffold( appBar: AppBar( title: const Text('My Homepage'), ), body: ListView( padding: const EdgeInsets.all(10), children: <Widget>[ /// ユーザ情報の表示 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.person), Text(ref.watch(userEmailProvider)), ], ), /// 各ページへの遷移 _PagePushButton( context, 'ノーマルカウンター', const NormalCounterPage(), Colors.blue), _PagePushButton(context, 'クラッシュページ', const CrashPage(), Colors.blue), _PagePushButton(context, 'Remote Configカウンター', const RemoteConfigPage(), Colors.blue), _PagePushButton(context, '認証ページ', const AuthPage(), Colors.red), /// 各ページへの遷移(認証後利用可能) /// 認証されていなかったらボタンを押せない状態にする FirebaseAuth.instance.currentUser?.uid != null ? _PagePushButton(context, 'Firestoreカウンター', const FirestorePage(), Colors.green) : Container( alignment: Alignment.center, child: const Text('Firestoreカウンターを開くためには認証してください。'), ), FirebaseAuth.instance.currentUser?.uid != null ? _PagePushButton(context, 'Realtime Databaseカウンター', const RealtimeDatabasePage(), Colors.green) : Container( alignment: Alignment.center, child: const Text('Realtime Databaseカウンターを開くためには認証してください。'), ), FirebaseAuth.instance.currentUser?.uid != null ? _PagePushButton(context, 'Cloud Storageページ', const CloudStoragePage(), Colors.green) : Container( alignment: Alignment.center, child: const Text('Cloud Storageページを開くためには認証してください。'), ), ], ), ); } } /// ページ遷移ボタン class _PagePushButton extends Container { _PagePushButton( BuildContext context, String buttonTitle, pagename, Color bgColor) : super( padding: const EdgeInsets.all(10), child: ElevatedButton( onPressed: () { AnalyticsService().logPage(buttonTitle); Navigator.push( context, MaterialPageRoute(builder: (context) => pagename)); }, style: ButtonStyle( backgroundColor: MaterialStateProperty.all(bgColor)), child: Text(buttonTitle), ), ); } /// Analyticsの実装 class AnalyticsService { /// ページ遷移のログ Future<void> logPage(String screenName) async { await FirebaseAnalytics.instance.logEvent( name: 'screen_view', parameters: { 'firebase_screen': screenName, }, ); } }
cloud_storage.dart
/// Flutter import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'dart:typed_data'; /// Firebase import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_auth/firebase_auth.dart'; /// 画像表示用Provider final imageStateProvider = StateProvider<Uint8List?>((ref) => null); class CloudStoragePage extends ConsumerStatefulWidget { const CloudStoragePage({Key? key}) : super(key: key); @override CloudStoragePageState createState() => CloudStoragePageState(); } class CloudStoragePageState extends ConsumerState<CloudStoragePage> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Cloud Storageページ'), ), body: ListView( padding: const EdgeInsets.all(10), children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ ElevatedButton( onPressed: () { CloudStorageService().uploadPic(); }, child: const Icon(Icons.upload), ), ElevatedButton( onPressed: () { CloudStorageService().downloadPic(ref); }, child: const Icon(Icons.download), ), ], ), ref.watch(imageStateProvider) == null ? const Text('No Image') : Image.memory(ref.watch(imageStateProvider)!), TextButton( onPressed: () { CloudStorageService().deletePic(ref); }, child: const Text('画像をクリア'), ), ], ), ); } } /// Realtime Databaseの設定 class CloudStorageService { /// UserIDの取得 final userID = FirebaseAuth.instance.currentUser?.uid ?? ''; void uploadPic() async { try { /// 画像を選択 final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); File file = File(image!.path); /// Firebase Cloud Storageにアップロード String uploadName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$uploadName'); final task = await storageRef.putFile(file); } catch (e) { print(e); } } /// 画像のダウンロード void downloadPic(WidgetRef ref) async { try { /// 参照の作成 String downloadName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$downloadName'); /// 画像をメモリに保存し、Uint8Listへ変換 const oneMegabyte = 1024 * 1024; ref.read(imageStateProvider.state).state = await storageRef.getData(oneMegabyte); } catch (e) { print(e); } } /// 画像の削除 void deletePic(WidgetRef ref) async { /// 参照の作成 String deleteName = 'image.png'; final storageRef = FirebaseStorage.instance.ref().child('users/$userID/$deleteName'); /// Cloud Storageから削除 await storageRef.delete(); /// メモリから削除 ref.read(imageStateProvider.state).state = null; } }
GitHubのページを貼ります。