もふもふ技術部

IT技術系mofmofメディア

【Flutter 連載記事第3回】DB(Firestore)の設定やCRUDをする

本記事について

本記事はシリーズ連載記事の第3回になります。

アプリの仕様として外部APIなどにアクセスしてデータを取得したり、ローカルに持っているデータだけでやりくりすることでDBを使わずに実装出来るものもありますが、やはり何かしらデータを登録してそれを使うということが機能として必要になることは出てくると思います。今回はそういった機能を実現するために、DBの設定をご紹介します。

  1. flutterの環境構築
  2. widgetを使った画面表示や、画面遷移をさせる
  3. DBの設定やCRUDをする
  4. S3を使って画像アップロード・ダウンロードする
  5. TODOアプリを作成する

バージョン

  • OS: macOS Monterey 12.6
  • チップ: Apple silicon
  • Flutter SDK: flutter_macos_arm64_3.3.9-stable

FlutterのDB採用における選択肢

ネットを調べてみると、FlutterでDBを使う選択肢としてAWSのAmplify Datastoreを使う方法とFirebaseのCloud Firestoreを使う方法が多く出てくると思います。AmplifyはDatastore以外にも色々な機能を利用することが出来ますので印象としては比較的大規模なアプリ開発に向いている印象で、簡単なアプリを作成したり小規模なアプリ開発ではFirebaseの方が必要な機能だけをシンプルに使って小回りが効きそうという印象を個人的には感じています。

FlutterはiOSAndroid、Webのクロスプラットフォーム開発、つまり同じソースコードを全てのプラットフォームに適用できるというのが売りの1つなのですが、この記事を書いている2022年12月現在ではAmplifyはFlutter on the webのホスティングに対応していないがfirebaseは対応している、というような機能面での違いというのもありますのでそのあたりがfirebaseを採用する理由になる場合もあるかもしれません。

本記事ではfirebaseを使う方法を紹介していきますが、Amplifyとfirebaseの比較記事などもネットには色々ありますので、用途に応じて選択していくと良いと思います。

Firebaseのプロジェクトを作る

まずは Firebaseコンソール に入って、Firebaseのプロジェクトを作成しましょう。

flutter_firestore_crud1

「プロジェクトを作成」を押下し

flutter_firestore_crud2

必要事項にチェックしてプロジェクト名を入力して「続行」を押下

flutter_firestore_crud3

「続行」を押下

flutter_firestore_crud4

アナリティクスの地域は「日本」を選択して「プロジェクトを作成」を押下

flutter_firestore_crud5

「続行」を押下

flutter_firestore_crud6

これでFirebaseのプロジェクトを作成することが出来ました。

Firebaseは様々な機能を使うことが出来ますのでコンソールには色々な項目が表示されていると思いますが、今回はDBのFirestoreを使いたいですので、画面の「Cloud Firestore」を押下しましょう。

Cloud Firestoreを利用出来るようにする

flutter_firestore_crud7

「データベースの作成」を押下

flutter_firestore_crud8

本番環境モードを使うかテストモードを使うかを選択します。本番環境モードとテストモードではセキュリティのルール設定が違っており、本番環境モードではデフォルトではクライアントアプリからのDBの読み書きを出来ない設定になっており、テストモードではデフォルトでクライアントアプリからのDBの読み書きが出来る設定(期限が自動で設定されていて、期限になると読み書きが出来なくなるように設定されている)になっています。どちらを選んでもルール自体は後から書き換えが出来るのですが、デフォルトのクライアントアプリからのDBの読み書き設定が違うということですね。

今回はテストで使用するだけですので、最初からDBの読み書きが出来るようにテストモードを使用しようと思います。テストモードを選択して「次へ」を押下します。

flutter_firestore_crud9

Cloud Firestoreのロケーションは「asia-northeast1(Tokyo)」を選択して「有効にする」を押下

flutter_firestore_crud10

これでCloud Firestoreを利用出来るようになりました。

Firebase CLIのインストール、FlutterFire CLIでの各プラットフォームの環境設定

次はアプリとFirebaseを接続するためのセットアップをしていきます。

$ curl -sL https://firebase.tools | bash

ターミナルで上記のスクリプトを実行すると、firebaseコマンドを使用出来るようになります。firebaseコマンドを使えるようになったら

$ firebase login

上記を実行します。 Allow Firebase to collect CLI usage and error reporting information? の質問が出るので、yまたはnを入力してエンターするとブラウザでGoogleアカウントのログイン画面が開きます。

flutter_firestore_crud11

先ほどFirebaseのプロジェクトを作ったアカウントと同じアカウントでログインしましょう。

flutter_firestore_crud12

Googleアカウントへのアクセスは「許可」を押下します。

flutter_firestore_crud13

ログインに成功するとブラウザで上記のようなログインが成功した旨のメッセージが表示され、ターミナルの方でもログインに成功した旨のメッセージが表示されます。

$ firebase projects:list

上記を実行すると、ログインしたアカウントで作成したプロジェクトの情報が表示されます。「Project Display Name」の項目に先ほど作成したFirebaseのプロジェクト名が表示されていることを確認しましょう。

flutter_firestore_crud14

次にFlutterからCloud Firestoreを利用できるようにするためのライブラリの cloud_firestore を前回までに作成したFlutterのアプリに追加しましょう。Flutterアプリのプロジェクト直下で

$ flutter pub add cloud_firestore

上記を実行すると pubspec.yamlpubspec.lock に必要なライブラリが追加されます。次に同様にFlutterアプリのプロジェクト直下で

$ flutterfire configure

を実行します。実行した際にパスの指定をするようにワーニングが出た場合は .bashrc.zshrc にパスを追加してください。ワーニングのメッセージでも表示されていると思いますが、下記のパスを追加します。

export PATH="$PATH":"$HOME/.pub-cache/bin”

flutterfire configure を実行すると、ログインしたFirebaseのアカウントで作成したプロジェクトが表示されます。キーボードの↑↓で選択して新規プロジェクトをここで作成することも出来ますが、先ほど作成したプロジェクトを選択してエンターを押下します。

android
ios
macos
web

上記4つのプラットフォームでの設定をそれぞれ作成することが出来ます。↑↓キーで選択してスペースキー押下でチェックの付け外しが出来ますので、使いたいプラットフォームにチェックがついていることを確認してエンターを押下して、各プラットフォームでの設定は完了となります。

Firebaseの初期化処理を追加

lib/main.dart にFirebase関連のライブラリのインポートを追加して、 void main() メソッドにFirebaseの初期化処理を追加します。追加した部分は下記のようになります。

lib/main.dart

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

awaitの処理を使う場合はメソッドをasyncにしないといけませんので、 void main() メソッドは void main() async としましょう。

Firestoreにコレクションを作成する

FirestoreはNoSQLデータベースのため、RDBとはデータの構造が違います。今回の記事はFlutterからFirestoreにデータをCRUDする部分についてのご紹介になりますので、Firestoreのデータ構造についてはここでは割愛させて頂きます。

まずはFirestoreに最初のコレクションとドキュメントを作成します。ここではコレクション名は text としてコレクションを作成しました。ドキュメントのIDは自動IDを設定して、フィールド名は body として値は テストです。 としました。

flutter_firestore_crud15

flutter_firestore_crud16

flutter_firestore_crud17

作成したドキュメントをアプリから取得する

さて、ようやく下準備が完了しました。ここから実際にアプリからFirestoreのデータを操作していきましょう。まずはデータ取得からです。

今回Firestoreからデータを取得するメソッドは下記です。先ほどFirestoreのGUIから作成したドキュメントを取得しています。Firestoreのインスタンスを生成して、 texts コレクションの lxp0G4FL6bmnnitHeZ5S のIDのドキュメントを取得しています。returnする値はそのドキュメントの body フィールドの値となっています。

Future<String> getDoc() async {
  DocumentSnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore
      .instance
      .collection('texts')
      .doc('lxp0G4FL6bmnnitHeZ5S')
      .get();
  return snapshot.data()!['body'];
}

ここでreturnしている値は Future<String> という型の値になります。非同期処理で取得しているためFuture型になっているのですが、FlutterではこのFuture型のデータを画面表示させるための FutureBuilder というWidgetがありますので、画面表示をさせる箇所ではそちらを使用します。

FutureBuilder(
  future: getDoc(),
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data);
    } else {
      return const Center(
        child: Text('データが取得できていません'),
      );
    }
  },
)

hasData メソッドはデータが存在するかどうかを確認できるメソッドで、データが取得できた場合はそのデータを表示させ、データが取得できていない場合はその旨のテキストを表示させるようにしています。

上記のデータ取得処理については別のファイルで実装して lib/main.dart にインポートする形にしました。新規作成した lib/firestore_texts.dart は下記になります。

lib/firestore_texts.dart

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreTexts extends StatelessWidget {
  const FirestoreTexts({super.key});

  Future<String> getDoc() async {
    DocumentSnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore
        .instance
        .collection('texts')
        .doc('lxp0G4FL6bmnnitHeZ5S')
        .get();
    return snapshot.data()!['body'];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: Center(
        child: FutureBuilder(
          future: getDoc(),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.hasData) {
              return Text(snapshot.data);
            } else {
              return const Center(
                child: Text('データが取得できていません'),
              );
            }
          },
        ),
      ),
    );
  }
}

上記をインポートした lib/main.dart が下記です。

lib/main.dart

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'firestore_texts.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
        '/': (context) => const FirestoreTexts(),
      },
    );
  }
}

シミュレータで表示させると、Firestoreのドキュメントのフィールドの値が表示されていることが分かりますね。

flutter_firestore_crud18

上記でデータ取得した方法はコレクションの中の1つのドキュメントを指定して取得する方法でしたが、コレクションのデータをまとめて取得したりすることも出来ますので、必要に応じて色々試してみるのが良いと思います。

ドキュメントをアプリから更新する

先ほど表示したデータをアプリから更新しましょう。データを更新するメソッドは下記です。

void updateDoc() async {
  FirebaseFirestore.instance
      .collection('texts')
      .doc('lxp0G4FL6bmnnitHeZ5S')
      .update({'body': 'テキストを更新しました。'});
}

指定したコレクションとドキュメントを update メソッドでキーを指定して更新します。 lib/firestore_texts.dart は下記のようにしました。

lib/firestore_texts.dart

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreTexts extends StatelessWidget {
  const FirestoreTexts({super.key});

  void updateDoc() async {
    FirebaseFirestore.instance
        .collection('texts')
        .doc('lxp0G4FL6bmnnitHeZ5S')
        .update({'body': 'テキストを更新しました。'});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: Center(
        child: GestureDetector(
            onTap: () {
              updateDoc();
            },
            child: const Text('タップで更新')),
      ),
    );
  }
}

シミュレータで表示された「タップで更新」の文字をタップしてFirestoreのデータを確認すると、

flutter_firestore_crud19

bodyの値が「テキストを更新しました。」に変わっていることが分かります。

ドキュメントをアプリから削除する

今まで操作してきたドキュメントをアプリから削除しましょう。データを削除するメソッドは下記です。コレクションとドキュメントIDを指定して削除を実行しています。

void deleteDoc() async {
  FirebaseFirestore.instance
      .collection('texts')
      .doc('lxp0G4FL6bmnnitHeZ5S')
      .delete();
}

lib/firestore_texts.dart は下記のようにしました。

lib/firestore_texts.dart

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreTexts extends StatelessWidget {
  const FirestoreTexts({super.key});

  void deleteDoc() async {
    FirebaseFirestore.instance
        .collection('texts')
        .doc('lxp0G4FL6bmnnitHeZ5S')
        .delete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: Center(
        child: GestureDetector(
            onTap: () {
              deleteDoc();
            },
            child: const Text('タップで削除')),
      ),
    );
  }
}

シミュレータで表示された「タップで削除」の文字をタップしてFirestoreのデータを確認すると、

flutter_firestore_crud20

データが削除されていることが分かります。

ドキュメントをアプリから作成する

新規にドキュメントを作成しましょう。データを新規作成するメソッドは下記です。コレクションを指定してデータ作成を実行しています。

void createDoc() async {
  FirebaseFirestore.instance.collection('texts').add({'body': '新規作成データです。'});
}

lib/firestore_texts.dart は下記のようにしました。

lib/firestore_texts.dart

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreTexts extends StatelessWidget {
  const FirestoreTexts({super.key});

  void createDoc() async {
    FirebaseFirestore.instance.collection('texts').add({'body': '新規作成データです。'});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: Center(
        child: GestureDetector(
            onTap: () {
              createDoc();
            },
            child: const Text('タップで新規作成')),
      ),
    );
  }
}

シミュレータで表示された「タップで削除」の文字をタップしてFirestoreのデータを確認すると、

flutter_firestore_crud21

データが新規作成されていることが分かります。

最後に

今回はFirestoreの設定方法やアプリからのデータのCRUD方法についてご紹介しました。データ取得後のUI表示についてはWidgetの使い方を考える必要があったりはするのですが、CRUDのメソッド自体は直感的で分かりやすいなと思いました。

参考

FirebaseCLIリファレンス FlutterFire Overview