広告 Flutter Dart Unity アプリ開発

GoogleCloudとFirebaseを活用する。

アプリ認証とデータ永続化・課金管理はGoogleCloud・Firebaseを活用しよう

Firebase(ファイアベース)は、2011年にFirebase, Inc.が開発したモバイル・Webアプリケーション開発プラットフォームで、その後2014年にGoogleに買収され、Google Cloudに統合されました。統合後も、Firebaseという名前は残存しています。
そのため、Google Cloud と Firebaseの2つのサイトで操作が発生するので、かなりややこしいです。
特にGoogle認証を使う場合が一番ややこしい!でも、2段階認証したり、本人であることを自前で実装するのではなく、Google Cloudが代行してくれるので、大変便利で安全です。是非マスターしましょう!

Google Cloud,Firebaseでは、個人認証(2段階認証を含む)や、共有データベース、課金管理等、ありとあらゆる有用なサービスが提供されています。原則有料のサービスですが、一定量のトライフィックまでは課金されないような仕組みになってるので、恐れずに活用してみましょう。

試験運用なら問題ありませんが、大量のトラフィックが発生すると課金が発生します。実運用する場合は要注意です。

なお、この記事は、YouTubeのルビーDogさんの記事を参考にしています。Ubuntu24.04で、2025年2月に動作確認済です。
(このYouTubeの説明は古くなっている部分がありますが、大変わかり易いので、是非#1から見ることをおすすめします!ただしMacOSでの説明です。)

YouTube ルビーDogのFirebase Auth動画

firebase-toolsをインストールする

以下の解説は、過去の記事で紹介したUbuntu24.04でFlutter開発環境を整備済みであることが前提になっています。
「端末」で、以下のコマンドを1行ずつ実行する。

sudo apt install npm
........
sudo npm install -g firebase-tools
........

Google Cloudにログイン(Googleアカウントが必要)

Firebaseにログインする前に、下記Google Cloudを開き、右上の「ログイン」をクリックする。(要Googleアカウント)Chromeで作業するとスムーズです。

Google Cloudへログイン

ログインしたら、右上の「コンソール」をクリックし、プロジェクトを作成する。プロジェクト名は英字にする。

「APIとサービス」を開き、「People API」を追加する。この画面デザインは、頻繁に変更されます。何とか探してみて!

GoogleCloud People API

認証情報を作成する。この画面も頻繁に変更されます。

画面左の「認証情報」メニューの「OAuth 2.0 クライアント ID」の「Web client (auto created by Google Service)」の「クライアントID」をコピーし(紙が2枚重なったアイコンをクリック)、テキストエディタ等に控える後でソースファイル埋め込む必要があります

Google認証の場合のWEBクライアントIDは「00000000-XXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com」 みたいな感じです。
課金が発生する場合があるので、個人のIDを絶対に公開しないよう注意!

更に、OAuthクライアントを編集します。(そのWeb client クライアントIDの右側にある鉛筆アイコン
「承認済みのJavaScript生成元」URLに「http://localhost:5000」を追加する。←ここ重要。(画面例は省略)

認証を行いたいアプリを作成

「端末」もしくはvscodeの「ターミナル」で、例えば以下のように入力し、新規Flutterアプリを作成する。

cd flutter_src
flutter create my_authtest_app
......
cd my_authtest_app
......

VSCodeを起動し、作成したmy_authtest_appのlib/main.dartを開く。(間違って編集しないよう、他のファイルは閉じておく
右下の「ターミナル」も必ず「flutter_src/my_authtest_app」フォルダに移動してから、以後の作業をする

Firebaseを起動し、Authenticationで認証方法を追加

ターミナルで「firebase login」を実行。初めて実行する場合は、Google Chromeが開き、Firebaseページでfirebase_cliの利用を許可する。(googleアカウントでログインして許可する。)既にログインしていると「Already login as ...」と表示されブラウザは開かない。

flutterfire configure
......
firebase login

下記ボタンからfirebaseの公式ページを起動し、右上の「goto console」をクリックする。Google Chromeで作業するとスムーズです。

Firebase 公式ページ

GoogleCloud に関連するプロジェクトを作成する。具体的には画面下側にある「すでに Google Cloud プロジェクトがある場合 Google Cloud プロジェクトに Firebase を追加」をクリックする。(名称は英字が良い)

画面左側にある「プロダクトのカテゴリ」の「構築」の「Authentication」を開き、画面左の「認証情報」メニューの「ログイン方法」で「Google認証」等、必要なプラットホームの認証情報を追加する。今回はWEB認証でテストするので、WEBクライアントさえあればOK。

flutterfire_cliを実行

VScodeのターミナルで下記の赤字コマンドを実行。 (うまくいかずにやり直すときは、 deactivate が必要。 activate →deactivateに変更して実行。

dart pub global activate flutterfire_cli
.......
Activated flutterfire_cli 1.x.x.    ←このように表示されたらOK

firebase projects:list 
firebase_project_ID

Project IDが表示されたら、firebase への接続が出来ていることを示します。

VSCodeのターミナルで「flutterfire configure --project=プロジェクトID」 を実行。もし、コマンドが見つからない旨の警告がでたら、flutterfireコマンドにPATHが通ってないことになる。
(/home/ユーザ名/.pub-cache/bin/) flutterの最初のインストール記事を参考に、PATHを.bashrcに記載すること。

プラットホームを選択するように表示されるので、矢印キーとスペースキーで、今回は「WEB」のみチェックを付ける。(ios,androidは、事前の準備が必要。IMEが日本語モードだとスペースキーが反応しない。) 最後に「Enter」キーを押す。

firebase_cli_project2

以上で「firebase_options.dart」が「 lib」フォルダに作られるここまで出来たら、ほぼ成功です。

Google認証テスト用アプリを作成する

以下のソースは、Youtube ルビーDogさんの動画「#24 Firebase Auth編」のソースを、2025年2月時点の最新版flutterで動くように修正したものです。

「lib」フォルダに、下記ソースをVSCode等で作成して保管します。

main.dart

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

// YouTube ルビーDog #24 Firebase Auth サンプルソースを参考にしています。

void main() async {
  // Firebase の準備
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // アプリを動かす
  const app = MyApp();
  const scope = ProviderScope(child: app);
  runApp(scope);
}

router.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:go_router/go_router.dart';
import 'pages.dart';
import 'state.dart';
part 'router.g.dart';

/// ---------------------------------------------------------
/// ページごとのパス
/// ---------------------------------------------------------
class PagePath {
  // サインイン画面のパス
  static const signIn = '/sign-in';
  // ホーム画面のパス
  static const home = '/home';
}

/// ---------------------------------------------------------
/// GoRouter
/// ---------------------------------------------------------
@riverpod
GoRouter router(RouterRef ref) {
  // パスと画面の組み合わせ
  final routes = [
    // サインイン画面
    GoRoute(path: PagePath.signIn, builder: (_, __) => const SignInPage()),

    // ユーザーIDスコープで囲むためのシェル
    ShellRoute(
      builder: (_, __, child) => UserIdScope(child: child),
      routes: [
        // ホーム画面
        GoRoute(path: PagePath.home, builder: (_, __) => const HomePage()),
        // ....
      ],
    ),
  ];

  // リダイレクト - 強制的に画面を変更する
  String? redirect(BuildContext context, GoRouterState state) {
    // 表示しようとしている画面
    final page = state.uri.toString();
    // サインインしているかどうか
    final signedIn = ref.read(signedInProvider);

    if (signedIn && page == PagePath.signIn) {
      // もうサインインしているのに サインイン画面を表示しようとしている --> ホーム画面へ
      return PagePath.home;
    } else if (!signedIn) {
      // まだサインインしていない --> サインイン画面へ
      return PagePath.signIn;
    } else {
      return null;
    }
  }

  // リフレッシュリスナブル - Riverpod と GoRouter を連動させるコード
  // サインイン状態が切り替わったときに GoRouter が反応する
  final listenable = ValueNotifier<Object?>(null);
  ref.listen<Object?>(signedInProvider, (_, newState) {
    listenable.value = newState;
  });
  ref.onDispose(listenable.dispose);

  // GoRouterを作成
  return GoRouter(
    initialLocation: PagePath.signIn,
    routes: routes,
    redirect: redirect,
    refreshListenable: listenable,
  );
}

/// ---------------------------------------------------------
/// アプリ本体
/// ---------------------------------------------------------
class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.watch(routerProvider);
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: router,
    );
  }
}

pages.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'service.dart';
import 'state.dart';

/// ---------------------------------------------------------
/// サインイン画面
/// ---------------------------------------------------------
class SignInPage extends ConsumerWidget {
  const SignInPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ボタン
    final button = ElevatedButton(
      onPressed: () async {
        // サービスを呼び出す
        final service = AuthService();
        await service.signIn().catchError((e) {
          debugPrint('You cannot Sign in. $e');
        });
      },
      child: const Text('Sign in'),
    );

    /// 画面全体
    return Scaffold(
      appBar: AppBar(title: const Text('Sign in')),
      body: Center(child: button),
    );
  }
}

/// ---------------------------------------------------------
/// ホーム画面
/// ---------------------------------------------------------
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ユーザーID
    final userId = ref.watch(userIdProvider);
    final userIdText = Text('Google ID: $userId');

    // ボタン
    final button = ElevatedButton(
      onPressed: () async {
        // サービスを呼び出す
        final service = AuthService();
        await service.signOut().catchError((e) {
          debugPrint('You cannot Sign out. $e');
        });
      },
      child: const Text('Sign out'),
    );

    // 画面全体
    return Scaffold(
      appBar: AppBar(title: const Text('Home'), backgroundColor: Colors.red),
      body: Center(
        // 縦に並べる
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            // テキスト
            userIdText,
            // ボタン
            button,
          ],
        ),
      ),
    );
  }
}

service.dart clientIdは、保管していたものを入力

import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';

/// 通信の流れをまとめておくサービスクラス
class AuthService {
  /// サインイン
  Future<void> signIn() async {
    /* Google OAuth と通信 */

    // あらかじめ登録しておいたクライアントID
    const clientId = 'xxxxx.apps.googleusercontent.com';


    // アプリが知りたい情報
    const scopes = [
      'openid', // 他サービス連携用のID
      'profile', // 住所や電話番号
      'email', // メールアドレス
    ];

    // Googleでサインイン の画面へ飛ばす
    final request = GoogleSignIn(clientId: clientId, scopes: scopes);
    final response = await request.signIn();

    // 受け取ったデータの中からアクセストークンを取り出す
    final authn = await response?.authentication;
    final accessToken = authn?.accessToken;

    // アクセストークンが null だったら中止
    if (accessToken == null) {
      return;
    }

    /* Firebase と通信 */

    // Firebaseへアクセストークンを送る
    final oAuthCredential = GoogleAuthProvider.credential(
      accessToken: accessToken,
    );
    await FirebaseAuth.instance.signInWithCredential(oAuthCredential);
  }

  /// サインアウト
  Future<void> signOut() async {
    await FirebaseAuth.instance.signOut();
  }
}

state.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:firebase_auth/firebase_auth.dart';
part 'state.g.dart';

///
/// FirebaseのユーザーをAsyncValue型で管理するプロバイダー
///
@riverpod
Stream<User?> userChanges(UserChangesRef ref) {
  // Firebaseからユーザーの変化を教えてもらう
  return FirebaseAuth.instance.authStateChanges();
}

///
/// ユーザー
///
@riverpod
User? user(UserRef ref) {
  final userChanges = ref.watch(userChangesProvider);
  return userChanges.when(
    loading: () => null,
    error: (_, __) => null,
    data: (d) => d,
  );
}

///
/// サインイン中かどうか
///
@riverpod
bool signedIn(SignedInRef ref) {
  final user = ref.watch(userProvider);
  return user != null;
}

/* スコープ内の画面からのみ使える */

///
/// ユーザーID
///
@riverpod
String userId(UserIdRef ref) {
  throw 'You cannot use in scope.';
}

/// ---------------------------------------------------------
/// ユーザーIDを使えるスコープ
/// ---------------------------------------------------------
class UserIdScope extends ConsumerWidget {
  const UserIdScope({super.key, required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// サインインしているユーザーの情報
    final user = ref.watch(userProvider);
    if (user == null) {
      // ユーザーが見つからない場合
      return const CircularProgressIndicator();
    } else {
      // ユーザーが見つかった場合
      return ProviderScope(
        // ユーザーIDを上書き
        overrides: [userIdProvider.overrideWithValue(user.uid)],
        child: child,
      );
    }
  }
}

「pubspec.yaml」ファイルに、下記赤字モジュールの追記が必要です。バージョンは明記していませんが、今後追記するかも。

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod:
  riverpod_annotation:
  json_annotation:
  go_router:
  firebase_core:
  firebase_analytics:
  firebase_auth:
  google_sign_in:

  cupertino_icons: ^1.0.8

dev_dependencies:
  build_runner:
  riverpod_generator:
  analyzer: ^7.1.0

このソースではriverpodのジェネレータ機能を利用しているので、VSCodeターミナルで下記の赤字コマンドを実行する。
「flutter upgrade」は、1日1回でもOK

flutter upgrade
flutter clean
flutter pub get

flutter pub run build_runner build

アプリを実行する場合は、ポートの指定が必須。(当然、Google Cloudで許可しているポートでないと動きません)

flutter run --web-port 5000

androidアプリの場合はSHA1キーの作成が必要になるっぽい(未テスト)

-Flutter Dart Unity アプリ開発