Flutter × Firebase:Shared Clipboard Webアプリを作成・公開する話

コンピュータ

Flutter と Firebase を組み合わせると、Web・Android・iOS に対応したアプリを、ほぼ同じコードで作成できます。今回はその学習テーマとして「Shared Clipboard(共有クリップボード)」という単純なアプリを作り、実際に Firebase Hosting へ公開するところまで試してみました。一応動くところまでは確認できたのですが、試行錯誤を繰り返したので再現性のある記録が残せませんでした。手元にには動く環境が有るので個人的には良しとします。

自分では記事に起こせませんでしたが、せっかくなのでChat-GPTに丸投げで記事にしてもらいました。

最終的に動くアプリはこちらです:

→ Shared Clipboard(Web版)

ログインすると、メモ欄が表示され、入力した内容が Firestore に保存されます。別端末で開けば即時反映されるため、PC とスマホ間での簡易メモ共有ツールとして使えます。


今回のゴール

  • Flutter Web アプリを作る
  • Firebase Authentication(Googleログイン)を使う
  • Firestore にメモ内容を保存して同期
  • Firebase Hosting で Web 公開

必要な知識は “ほぼゼロ” で OK。Flutter と Firebase の「最小構成」を通して、Web アプリの流れが理解できます。


Flutter プロジェクトを作成

flutter create shared_clipboard
cd shared_clipboard

作成した時点では、Flutter のデモページが起動できます。この段階で一度ビルドできるか確認しておくと安心です。


Firebase プロジェクトを作成

Firebase Console で新規プロジェクトを作成します。

次に 「ウェブアプリを追加(</>)」 を選択して登録します。アプリ名は任意で構いません。

登録が完了したら、今後 Flutter から参照するための構成情報がブラウザに表示されますが、この時点では触らずに進めます。


FlutterFire CLI で Firebase 設定を自動生成

Flutter が Firebase を扱うためには、FlutterFire CLI が必要です。

dart pub global activate flutterfire_cli
firebase login

次に、Flutter プロジェクトへ Firebase 構成を注入します。

flutterfire configure

登録した Firebase プロジェクトを選ぶと、lib/firebase_options.dart が生成されます。これで Flutter 側の準備は完了です。


必要なパッケージを追加

Firestore と Auth を使うので、次の 3 つは必須です。

flutter pub add firebase_core firebase_auth cloud_firestore

この後、flutter pub get が自動実行されます。


Firestore のセキュリティルールを設定

デフォルトでは読み書きが禁止されているため、ログイン済ユーザーのみ書けるようにします。

Firebase Console → Firestore Database → 「ルール」

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

これで、Authentication に成功したユーザーだけ読み書きできます。


メインコード(最小構成)

今回のアプリは「Google ログイン」→「メモ欄」→「Firestore保存」の三段階です。

以下は実際に動いている Shared Clipboard の最小例です。

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.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: 'Shared Clipboard',
      home: LoginGate(),
    );
  }
}

class LoginGate extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        if (snapshot.data == null) {
          return Scaffold(
            body: Center(
              child: ElevatedButton(
                onPressed: () async {
                  await FirebaseAuth.instance.signInWithPopup(
                      GoogleAuthProvider());
                },
                child: const Text('Google でログイン'),
              ),
            ),
          );
        }
        return ClipboardPage();
      },
    );
  }
}

class ClipboardPage extends StatefulWidget {
  @override
  State<ClipboardPage> createState() => _ClipboardPageState();
}

class _ClipboardPageState extends State<ClipboardPage> {
  late final DocumentReference ref;

  @override
  void initState() {
    super.initState();

    final uid = FirebaseAuth.instance.currentUser!.uid;
    ref = FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .collection('memo')
        .doc('clipboard');
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: ref.snapshots(),
      builder: (context, snapshot) {
        final text = snapshot.data()?['text'] ?? '';

        return Scaffold(
          appBar: AppBar(title: const Text('Shared Clipboard')),
          body: Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              maxLines: 20,
              controller: TextEditingController(text: text),
              onChanged: (value) {
                ref.set({
                  'text': value,
                  'updatedAt': FieldValue.serverTimestamp()
                });
              },
            ),
          ),
        );
      },
    );
  }
}

これで「ログイン → Firestore リアルタイム同期」が動きます。


Flutter Web をビルドする

Hosting へデプロイする前に、Web 版をビルドします。

flutter build web

成功すると build/web フォルダが作成されます。


Firebase Hosting へ公開

初回のみ hosting を初期化します。

firebase init hosting

質問への推奨回答:

  • 既存プロジェクトを使う → Yes
  • public ディレクトリ → build/web
  • SPA として扱う → Yes

初期化後、いよいよ公開です。

firebase deploy --only hosting

成功すると、Hosting URL が表示されます。

https://shared-clipboard-xxxx.web.app

これで誰でもアクセス可能な Web アプリになります。

動作確認

PCのブラウザとスマホ(Android)のブラウザで同じページを開きフォームに文字を書き込んだ
ところ、入力した文字がお互いに同期していることが確認できました。


まとめ

Flutter と Firebase を組み合わせると、Web・スマホ・デスクトップに対応したアプリを、ひとつのコードで開発できます。今回の Shared Clipboard は非常に小さなアプリですが、実用性は十分で、リアルタイム同期の感覚もつかみやすい題材でした。

個人開発では、 Firebase Hosting を使った Web 公開が “最短でアプリを形にできる手段” の一つです。学習テーマとしても分かりやすく、Flutter のメリットを体験するには最適だと思います。

興味があれば、次はスマホ版(Android/iOS)をビルドして、PC とスマホで本格的なクリップボード共有ツールに進化させたいところです。


以上、Flutter × Firebase で Web アプリを作成して公開するまでの手順でした。

コメント