snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

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

準備

準備編が完了出来ているものとします。

snova301.hatenablog.com

導入方法

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もまとめてインポートします。 なお、webdart: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のページを貼ります。

github.com

参考

api.flutter.dev

Google Play and the Google Play logo are trademarks of Google LLC.