Open5

【Firebase】Cloud Storage と Cloud Firestore を使って画像管理機能を作成

カワグチミサキカワグチミサキ

画像をアップロードする(準備)

1. Cloud Storageを準備する


Storage > 始めるを選択して、ロケーションは日本国内であるasia-northeast1を選択する


ルールを修正する

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

2.Cloud Storage の機能を使うためのライブラリをインストール

pubspec.yaml
# ...
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  firebase_core: ^1.17.1
  firebase_auth: ^3.3.19
  firebase_storage: ^10.2.17 # この行を追加
カワグチミサキカワグチミサキ

画像をアップロードする

1.端末の画像ファイルを選択できるライブラリをインストール

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  firebase_core: ^1.17.1
  firebase_auth: ^3.3.19
  firebase_storage: ^10.2.17
  file_picker: ^4.6.1 # この行を追加

2.アップロードする処理を追加

  • 画像ファイル選択(pickFiles()でファイルを1つ選ぶ・FileType.imageで画像ファイルのみ選ぶ)
  • 画像ファイルが選択された場合の処理
// 画像を追加
Future<void> _onAddPhoto() async {
  // 画像ファイルを選択
  final FilePickerResult? result = await FilePicker.platform.pickFiles(
    type: FileType.image,
  );

  // 画像ファイルが選択された場合
  if (result != null) {
    // ログイン中のユーザー情報を取得
    final User user = FirebaseAuth.instance.currentUser!;

    // フォルダとファイル名を指定し画像ファイルをアップロード
    // 日時をエポックミリ秒に変換
    final int timestamp = DateTime.now().microsecondsSinceEpoch;
    // ファイルのパス
    final File file = File(result.files.single.path!);
    // パスを/で区切った最後の値をnameに入れる
    final String name = file.path.split('/').last;
    final String path = '${timestamp}_$name';
    final TaskSnapshot task = await FirebaseStorage.instance
        .ref()
        .child('users/${user.uid}/photos') // フォルダ名
        .child(path) // ファイル名
        .putFile(file); // 画像ファイル
  }
}

カワグチミサキカワグチミサキ

アップロードした画像を管理する(準備)

1. Cloud Firestoreを準備する



ルールを修正する

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

2.Cloud Firestore の機能を使うためのライブラリをインストール

pubspec.yaml
# ...
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
 firebase_core: ^1.17.1
  firebase_auth: ^3.3.19
  firebase_storage: ^10.2.17
  file_picker: ^4.6.1
  cloud_firestore: ^3.1.17 # この行を追加
カワグチミサキカワグチミサキ

アップロードした画像を管理する

  • Cloud Storage にアップロードした画像ファイルのURLを取得
  • Cloud Firestore に画像管理データを保存
  • Cloud Firestore に保存した画像管理データを元に画像を一覧表示

「コレクション」と「ドキュメント」と呼ばれるものを組み合わせて「データ」を保存する。

  • コレクション:複数のドキュメントを保存できる
  • ドキュメント:データを保存できる、複数のサブコレクションを保存できる
  • データ:複数のキーと値のペア
// パス(コレクション・ドキュメント)
/users/${userID}/photos/${photoID}

// データ
{
  // 画像のURL(string)
  "imageURL": "https://...",
  // 画像ファイルのパス(string)
  "imagePath": "users/id_xyz/photos/1599835164531849_myphoto.png",
  // お気に入り(bool)
  "isFavorite": false,
  // 作成日時(timestamp)
  "createdAt": "2020年9月11日 23:39:25 UTC+9"
}

// アップロードした画像のURLを取得
final String imageURL = await task.ref.getDownloadURL();
// アップロードした画像の保存先を取得
final String imagePath = task.ref.fullPath;

画像ファイルからURLを取得し、Cloud Firestore に画像管理データを保存する処理を追加

  • スナップショットから画像のURLと画像の保存先を取得
  • 保存するデータを作成
  • データをCloud Firestoreに保存する
Future<void> _onAddPhoto() async {
    // ...
    if (result != null) {
      // ...
      final TaskSnapshot task = await FirebaseStorage.instance
          .ref()
          .child('users/${user.uid}/photos')
          .child(path)
          .putFile(file);
      
      // アップロードした画像のURLを取得
      final String imageURL = await task.ref.getDownloadURL();
      // アップロードした画像の保存先を取得
      final String imagePath = task.ref.fullPath;
      // データ
      final data = {
        'imageURL': imageURL,
        'imagePath': imagePath,
        'isFavorite': false, // お気に入り登録
        'createdAt': Timestamp.now(), // 現在時刻
      };
      // データをCloud Firestoreに保存
      await FirebaseFirestore.instance
          .collection('users/${user.uid}/photos') // コレクション
          .doc() // ドキュメント(何も指定しない場合は自動的にIDが決まる)
          .set(data); // データ
    }
  }
}

カワグチミサキカワグチミサキ

Cloud Firestoreに保存した各画像管理データを元に画像一覧表示する処理を追加

StreamBuilderを使用して一覧を表示する

  • StreamBuilder:イベントが起きるたびその情報を自動でキャッチし、アプリの表示に反映させることができる
photo_list_screen.dart
class _PhotoListScreenState extends State<PhotoListScreen> {
  
  // ...

  
  Widget build(BuildContext context) {
    // ログインしているユーザーの情報を取得
    final User user = FirebaseAuth.instance.currentUser!;

    return Scaffold(
      // ...
      body: StreamBuilder<QuerySnapshot>(
        // Cloud Firesstoreからデータを取得
        stream: FirebaseFirestore.instance
            .collection('users/${user.uid}/photos')
            .orderBy('createdAt', descending: true)
            .snapshots(),
        builder: (context, snapshot) {
          // Cloud Firestoreからデータを取得中の場合
          if (snapshot.hasData == false) {
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          // Cloud Firestoreからデータを取得完了した場合
          final QuerySnapshot query = snapshot.data;
          // 画像のURL一覧を作成
          final List<String> imageList = query.docs
              .map((doc) => doc.get('imageURL') as String)
              .toList();
          return PageView(
            controller: _controller,
            onPageChanged: (int index) => _onPageChanged(index),
            children: [
              //「全ての画像」を表示する部分
              PhotoGridView(
                // Cloud Firestoreから取得した画像のURL一覧を渡す
                imageList: imageList,
                onTap: (imageURL) => _onTapPhoto(imageURL, imageList),
              ),
              //「お気に入り登録した画像」を表示する部分
              PhotoGridView(
                // お気に入り登録した画像は、後ほど実装
                imageList: [],
                onTap: (imageURL) => _onTapPhoto(imageURL, imageList),
              ),
            ],
          );
        },
      ),
      // ...
    );
  }

  // ...

  void _onTapPhoto(String imageURL, List<String> imageList) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => PhotoViewScreen(
          imageURL: imageURL,
          imageList: imageList,
        ),
      ),
    );
  }
}

// Widgetを新たに定義し再利用できる
class PhotoGridView extends StatelessWidget {
  const PhotoGridView({
    Key? key,
    // 引数から画像のURL一覧を受け取る
    required this.imageList,
    required this.onTap,
  }) : super(key: key);

  final List<String> imageList;
  final Function(String imageURL) onTap;

  
  Widget build(BuildContext context) {
    return GridView.count(
      // ...
      children: imageList.map((String imageURL) {
        // ...
      }).toList(),
    );
  }
}
photo_view_screen.dart
import 'package:flutter/material.dart';

class PhotoViewScreen extends StatefulWidget {
  const PhotoViewScreen({
    Key? key,
    required this.imageURL,
    // 引数から画像のURL一覧を受け取る
    required this.imageList,
  }) : super(key: key);

  final String imageURL;
  final List<String> imageList;

  
  _PhotoViewScreenState createState() => _PhotoViewScreenState();
}

class _PhotoViewScreenState extends State<PhotoViewScreen> {
  late PageController _controller;

  
  void initState() {
    super.initState();
    
    // 受け取った画像一覧から、ページ番号を特定
    final int initialPage = widget.imageList.indexOf(widget.imageURL);

    _controller = PageController(
      initialPage: initialPage,
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      // ...
      body: Stack(
        children: [
          PageView(
            // ...
            // 受け取った画像一覧を表示
            children: widget.imageList.map((String imageURL) {
              return Image.network(
                imageURL,
                fit: BoxFit.cover,
              );
            }).toList(),
          ),
          // ...
        ],
      ),
    );
  }
}