Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【Mac不要】iOSとAndroidのFlutterアプリから利用者のGoogle Photosにアクセスする手順 - Codemagic編 (前編)

はじめに

※この記事は、以下の記事の続編です。

【Mac不要】FlutterアプリにFirebase Crashlyticsを組み込んでiOSとAndroidに実機配備する手順 - Codemagic編

この記事ではその後、Flutterアプリから利用者のGoogle Photoにアクセスするための手順を説明しますが、この(前編)では、利用者のGoogleアカウントにアクセスできるまでの手順を説明します。

では早速始めてみましょう。

前提

この記事は、以下に当てはまる人向けの iOS/Android 対応アプリにGoogle Sign-in APIを組み込む手順を紹介します。

  • 開発機はWindowsだ
  • Macを持っていない
  • Flutterで開発したい
  • Apple Developer Programに既に登録している。

開発環境の前提

以下の3本の記事の手順に沿ってFlutterアプリをCodemagicでビルドし、さらに自身のiOS端末/Android端末のアプリの利用状況(Analytics)やクラッシュ情報(Crashlytics)がFirebaseに連携されている状態にしておいてください。

  1. 【Mac不要】開発中のFlutter製アプリをiOSとAndroidに実機配備する手順 - Codemagic編
  2. 【Mac不要】FlutterアプリにFirebase SDKを組み込んでiOSとAndroidに実機配備する手順 - Codemagic編
  3. 【Mac不要】FlutterアプリにFirebase Crashlyticsを組み込んでiOSとAndroidに実機配備する手順 - Codemagic編

ソースコードの公開

この記事の手順で作成したソースコードはGithubで公開しています。併せてご参照ください。
https://github.com/atsuteru/flutter_firebase_0507/tree/Qiita-GooglePhoto-v1.0
5/14 修正 Firebase認証を利用するように修正しました。
https://github.com/atsuteru/flutter_firebase_0507/tree/Qiita-GooglePhoto-v1.1

本文

自身のアプリに対しFirebase上でGoogle Photos Library APIを有効化する

Firebaseコンソールにサインインし、Photos Library APIのページを開きます。

ページ最上部の「Google APIs」のタイトルのすぐ右で、自身のFirebase Appを選択しましょう。そして「有効にする」ボタンをクリックしてください。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_40_59.png

すると、以下の画面に切り替わります。これで有効化できました。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_41_25.png

次に、OAuth同意画面の構成を行います。OAuth同意画面とは、アプリの利用者に対して表示する、「このアプリはこれらの権限を要求します」ことを通知する同意画面のことです。

ではページ左側のメニューから「認証情報」をクリックし、認証情報のページを開いてください。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_47_59.png

このページの右上付近「同意画面を構成」をクリックします。するとOAuth 同意画面の設定ウィザードが開きます。

最初のページでは、この記事の場合はすべての利用者をターゲットとすので、「外部」を選択し、「作成」をクリックします。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_49_03.png

次のページでは、最低限、アプリケーション名サポートメールと、Google API のスコープを設定します(他の項目はリリースに向けて追々設定しましょう)。アプリケーション名を入力したら、「スコープの追加」ボタンを押してください。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_55_12.png

するとスコープの追加画面が表示されます。ここで必要なスコープを選択するのですが、Google Photos APIのスコープはリストに用意されていないので、画面下部の「手動で貼り付け」のリンクをクリックします。
API とサービス - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 18_55_56.png

すると入力欄が出現しますので、以下を貼り付けてください。

スコープの追加
https://www.googleapis.com/auth/photoslibrary
https://www.googleapis.com/auth/photoslibrary.sharing

貼り付けたら、画面下部の「追加」をクリックしましょう。すると「スコープの追加」ボタンの上に2行、photoslibraryが追加されているのが分かるはずです。
同意画面 - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 19_01_12.png

※ここで「プライベート データにかかわるスコープを追加したため、スコープの公開前に、同意画面で Google による確認が必要です」という警告が表示されます。これは開発段階のアプリではまだ不要なので、このままスキップして構いません。もしGoogleによる確認を受けたい場合は、このFAQページのVerification for appsを参考にご準備ください。

最後に保存を押せば、設定は完了です。
同意画面 - flutter-firebase-0507 - Google API コンソール - Google Chrome 2020_05_12 19_03_32.png

では続けて、アプリの準備に取り掛かりましょう。

Flutterアプリ側では、google_sign_inプラグインを利用します。google_sign_inプラグインのページを確認し、以下の手順を進めていきましょう。

Android向けの設定

以下、Android integrationのページより。

Android向けの追加の設定は不要です。

iOS向けの設定

以下、iOS integrationのページより。

iOS向けに、以下のファイルにGoogle Sign-in Sectionの設定を追加します。

ios/Runner/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...(中略)...
    <!-- Google Sign-in Section -->
    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>COPY_REVERSED_CLIENT_ID_HERE</string>
        </array>
      </dict>
    </array>
    <!-- End of the Google Sign-in Section -->
</dict>

上記を貼り付け後、COPY_REVERSED_CLIENT_ID_HEREの部分を、以下のファイルのkey=REVERSED_CLIENT_IDの値(string)に貼り換えましょう。※以下、マスクしてありますので、あなたのファイルの値を貼り付けてください

ios/Runner/GoogleService-Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CLIENT_ID</key>
    <string>***.apps.googleusercontent.com</string>
    <key>REVERSED_CLIENT_ID</key>
    <string>com.googleusercontent.apps.***-***</string>
...(中略)...
</dict>

iOS向けの追加の設定は以上です。

Flutterアプリの実装

以下、Exampleのページより。

コーディングは、説明しやすいように若干アレンジしています。

Google Sign-inライブラリを読み込む設定を追加する

pubspec.yamldependenciesに、google_sign_inの行を追加しましょう。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^0.4.4+3
  firebase_crashlytics: ^0.1.3+3
  google_sign_in: ^4.0.7

アプリの実装にGoogle Sign-in処理を組み込む

本記事では説明しやすいように、あえてmain.dartのみで実装しました。実際はお好みのデザインパターンで実装されてください。

5/14 追記

Flutter1.7 (AndroidX対応)以降では、以下のコードでは不足があることが分かりました。
最新のコードはもう少し先にある「5/14 追加手順 Flutter1.7 (AndroidX対応)」の章をご利用ください。

import宣言の追加と、サインイン用のインスタンス生成

lib/main.dart
import 'dart:async';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
// ▼▼▼ importの追加ココカラ ▼▼▼
import 'package:google_sign_in/google_sign_in.dart';
// ▲▲▲ importの追加ココマデ ▲▲▲

// ▼▼▼ サインイン用のインスタンス生成の追加ココカラ ▼▼▼
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: <String>[
  'profile',
  'https://www.googleapis.com/auth/photoslibrary',
  'https://www.googleapis.com/auth/photoslibrary.sharing'
]);
// ▲▲▲ サインイン用のインスタンス生成の追加ココマデ ▲▲▲

void main() {
  Crashlytics.instance.enableInDevMode = true;
  FlutterError.onError = Crashlytics.instance.recordFlutterError;
  runZoned<Future<void>>(() async {
    runApp(MyApp());
  }, onError: Crashlytics.instance.recordError);

Stateクラスへの変数とメソッドの追加

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
// ▼▼▼ 変数の追加ココカラ ▼▼▼
  GoogleSignInAccount _currentUser;
// ▲▲▲ 変数の追加ココマデ ▲▲▲

// ▼▼▼ メソッドの追加ココカラ ▼▼▼
  @override
  void initState() {
    super.initState();
    _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) {
      setState(() {
        _currentUser = account;
      });
      if (_currentUser != null) {
        _handleGetContact();
      }
    });
    _googleSignIn.signInSilently();
  }

  Future<void> _handleGetContact() async {
  }

  Future<void> _handleSignIn() async {
    try {
      await _googleSignIn.signIn();
    } catch (e, s) {
      Crashlytics.instance
          .recordError(e, s, context: 'Google SignIn error');
    }
  }

  Future<void> _handleSignOut() => _googleSignIn.disconnect();
// ▲▲▲ メソッドの追加ココマデ ▲▲▲

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

サインインを実行するためのWidgetの追加

lib/main.dart
// ▼▼▼ Widget の追加ココカラ ▼▼▼
  Widget _buildGoogleSignInBody() {
    if (_currentUser != null) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          ListTile(
            leading: GoogleUserCircleAvatar(
              identity: _currentUser,
            ),
            title: Text(_currentUser.displayName ?? ''),
            subtitle: Text(_currentUser.email ?? ''),
          ),
          const Text("Signed in successfully."),
          RaisedButton(
            child: const Text('SIGN OUT'),
            onPressed: _handleSignOut,
          ),
          RaisedButton(
            child: const Text('REFRESH'),
            onPressed: _handleGetContact,
          ),
        ],
      );
    } else {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          const Text("You are not currently signed in."),
          RaisedButton(
            child: const Text('SIGN IN'),
            onPressed: _handleSignIn,
          ),
        ],
      );
    }
  }
// ▲▲▲ Widget の追加ココマデ ▲▲▲

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            Text(
              'Firebase Crashlytics Tests:',
            ),
            FlatButton(
                child: const Text('Key'),
                onPressed: () {
                  Crashlytics.instance.setString('foo', 'bar');
                }),
            FlatButton(
                child: const Text('Log'),
                onPressed: () {
                  Crashlytics.instance.log('baz');
                }),
            FlatButton(
                child: const Text('Crash'),
                onPressed: () {
                  Crashlytics.instance.crash();
                }),
            FlatButton(
                child: const Text('Throw Error'),
                onPressed: () {
                  throw StateError('Uncaught error thrown by app.');
                }),
            FlatButton(
                child: const Text('Async out of bounds'),
                onPressed: () {
                  Future<void>.delayed(const Duration(seconds: 2), () {
                    final List<int> list = <int>[];
                    print(list[100]);
                  });
                }),
            FlatButton(
                child: const Text('Record Error'),
                onPressed: () {
                  try {
                    throw 'error_example';
                  } catch (e, s) {
                    // "context" will append the word "thrown" in the
                    // Crashlytics console.
                    Crashlytics.instance
                        .recordError(e, s, context: 'as an example');
                  }
                }),
// ▼▼▼ Widget の呼び出しの追加ココカラ ▼▼▼
            _buildGoogleSignInBody(),
// ▲▲▲ Widget の呼び出しの追加ココマデ ▲▲▲
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

動作確認

実装が完了したら、pushしてCodemagicのビルド結果を待ちましょう。

Codemagicから届いたビルド成功の通知メールから、再インストールしましょう。

※iOSアプリは、TestFlightの手順(記事:【Mac不要】開発中のFlutter製アプリを仲間のiOS端末に実機配備する手順 - Codemagic&TestFlight編)をすでに実施している場合は、TestFlightから「Update」することでこのバージョンをインストールしましょう。それ以外はの方はCodemagicから届くメールからインストールしてください。

Androidでの動作確認

  • SIGN INボタンが追加されていますね!早速押してみましょう。
  • Googleアカウントへのサインイン画面が表示されました!デバイスのアカウントでそのままサインインしてみます。
  • 次はGoogleアカウントに対してアプリに与えられる権限の確認が続きます。
  • まだ権限の確認が続きます。最後に「許可」をクリックしましょう。
  • すると・・・サインインできましたね!

注記)上記はUSBデバッグにて確認を行いました。現在、CodemagicのビルドからインストールしたAndroidアプリでは、「SIGN INボタンクリック」→「アカウントの選択」の後、ホーム画面に戻ってしまう不具合が発生しており、解決方法は見つけられていません。。。どなたか、解決のヒントをお持ちであればご教授いただけると有難いです。
5/14追記 解決方法が見つかりました。この後に追加の手順を記載してありますので、お読みください。

iOSで確認

  • SIGN INボタンが追加されていますね!早速押してみましょう。
    IMG_1527.jpeg
  • するとiOSからの確認ダイアログが表示されます。続けましょう。
    IMG_1528.jpeg
  • Googleアカウントへのサインイン画面が表示されました!入力していきましょう。
    IMG_1529.jpeg IMG_1530.jpeg
  • すると物騒な警告画面が表示されました。これは、開発中のためGoogleによる確認をスキップしているからです。詳細から安全ではないページ委に移動を行いましょう。
    IMG_1531.jpeg IMG_1532.jpeg
  • 次はGoogleアカウントに対してアプリに与えられる権限の確認が続きます。
    IMG_1533.jpeg IMG_1534.jpeg
  • まだ権限の確認が続きます。最後に「許可」をクリックしましょう。
    IMG_1535.jpeg IMG_1536.jpeg
  • すると・・・サインインできましたね!
    IMG_1538.jpeg

5/14 追加手順 Flutter1.7 (AndroidX対応)

ここまでの実装では、Android端末での実機の動作に問題がありました。原因は、Flutter1.7以降に必要な手順の漏れです。以下の記事から、Flutter1.7以降では、GoogleアカウントにサインインするためにはFirebase認証を経由しなければならなくなったことを知りました。

Flutter: Implementing Google Sign In

では早速その手順をすすめましょう。以下の手順は、次のサイトを参考にしています。

Firebase authentication & Google sign in using Flutter - Codemagic blog

Firebaseコンソール上でGoogleアカウントのAuthenticationを有効にする

  • Firebaseコンソールから、アプリのAuthenticationのページを開き、「ログイン方法の設定」ボタンをクリックしましょう。 flutter-firebase-0507 - Authentication - Firebase コンソール - Google Chrome 2020_05_13 21_51_02.png
  • 対応しているログインプロバイダの一覧が表示され、その中のGoogleを選択します。 flutter-firebase-0507 - Authentication - Firebase コンソール - Google Chrome 2020_05_13 21_51_34.png
  • 「有効にする」に切り替えたら、「保存」をクリックしましょう。手順は以上です。 flutter-firebase-0507 - Authentication - Firebase コンソール - Google Chrome 2020_05_13 21_52_22.png

FlutterアプリにFirebase Authenticationのライブラリを組み込む

pubspec.yamlにプラグインfirebase_authを追加します。最新のバージョンはfirebase_authプラグインのページで確認しましょう。
※ついでにgoogle_sign_inのバージョンも最新にしました。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^0.4.4+3
  firebase_crashlytics: ^0.1.3+3
  firebase_auth: ^0.16.0
  google_sign_in: ^4.4.6

Flutterアプリの実装を、Firebase Authenticationを使う形に書き換える

以下、main.dartを編集していきます。

import宣言の追加と、サインイン用のインスタンス生成

lib/main.dart
import 'dart:async';
// ▼▼▼ importの追加ココカラ ▼▼▼
import 'package:firebase_auth/firebase_auth.dart';
// ▲▲▲ importの追加ココマデ ▲▲▲
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
// ▼▼▼ importの追加ココカラ ▼▼▼
import 'package:google_sign_in/google_sign_in.dart';
// ▲▲▲ importの追加ココマデ ▲▲▲

// ▼▼▼ サインイン用のインスタンス生成の追加ココカラ ▼▼▼
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: <String>[
  'profile',
  'https://www.googleapis.com/auth/photoslibrary',
  'https://www.googleapis.com/auth/photoslibrary.sharing'
]);
// ▲▲▲ サインイン用のインスタンス生成の追加ココマデ ▲▲▲

void main() {
  Crashlytics.instance.enableInDevMode = true;
  FlutterError.onError = Crashlytics.instance.recordFlutterError;
  runZoned<Future<void>>(() async {
    runApp(MyApp());
  }, onError: Crashlytics.instance.recordError);

Stateクラスへの変数とメソッドの追加

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
// ▼▼▼ 変数の追加ココカラ ▼▼▼
  FirebaseUser _currentUser;
// ▲▲▲ 変数の追加ココマデ ▲▲▲

// ▼▼▼ メソッドの追加ココカラ ▼▼▼
  @override
  void initState() {
    super.initState();
    new Future(() async{
      if (await _handleSignIn(await _googleSignIn.signInSilently())) {
        _handleGetContact();
      }
    });
  }

  Future<void> _handleGetContact() async {
  }

  Future<bool> _handleSignIn(GoogleSignInAccount googleSignInAccount) async {
    if (googleSignInAccount == null) {
      return false;
    }
    final GoogleSignInAuthentication googleSignInAuthentication = await googleSignInAccount.authentication;
    final AuthCredential credential = GoogleAuthProvider.getCredential(
      accessToken: googleSignInAuthentication.accessToken,
      idToken: googleSignInAuthentication.idToken,
    );
    final AuthResult authResult = await _auth.signInWithCredential(credential);
    final FirebaseUser user = authResult.user;
    if (user.isAnonymous) {
      return false;
    }
    if (await user.getIdToken() == null) {
      return false;
    }
    final FirebaseUser currentUser = await _auth.currentUser();
    if (user.uid != currentUser.uid) {
      return false;
    }
    setState(() {
      _currentUser = currentUser;
    });
    return true;
  }

  Future<void> _handleSignOut() async {
    await _googleSignIn.signOut();
    setState(() {
      _currentUser = null;
    });
  }
// ▲▲▲ メソッドの追加ココマデ ▲▲▲

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

サインインを実行するためのWidgetの追加

lib/main.dart
// ▼▼▼ Widget の追加ココカラ ▼▼▼
  Widget _buildGoogleSignInBody() {
    if (_currentUser != null) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          Card(
            color: Colors.lightBlueAccent,
            child: ListTile(
              leading: CircleAvatar(
                backgroundImage: NetworkImage(
                  _currentUser.photoUrl,
                ),
                radius: 30,
                backgroundColor: Colors.transparent,
              ),
              title: Text(_currentUser.displayName ?? ''),
              subtitle: Text(_currentUser.email ?? ''),
            ),
          ),
          const Text("Signed in successfully."),
          RaisedButton(
            child: const Text('SIGN OUT'),
            onPressed: _handleSignOut,
          ),
        ],
      );
    } else {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          const Text("You are not currently signed in."),
          RaisedButton(
            child: const Text('SIGN IN'),
            onPressed: () async{
              if (await _handleSignIn(await _googleSignIn.signIn())) {
                _handleGetContact();
              }
            }
          ),
        ],
      );
    }
  }
// ▲▲▲ Widget の追加ココマデ ▲▲▲

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            Text(
              'Firebase Crashlytics Tests:',
            ),
            FlatButton(
                child: const Text('Key'),
                onPressed: () {
                  Crashlytics.instance.setString('foo', 'bar');
                }),
            FlatButton(
                child: const Text('Log'),
                onPressed: () {
                  Crashlytics.instance.log('baz');
                }),
            FlatButton(
                child: const Text('Crash'),
                onPressed: () {
                  Crashlytics.instance.crash();
                }),
            FlatButton(
                child: const Text('Throw Error'),
                onPressed: () {
                  throw StateError('Uncaught error thrown by app.');
                }),
            FlatButton(
                child: const Text('Async out of bounds'),
                onPressed: () {
                  Future<void>.delayed(const Duration(seconds: 2), () {
                    final List<int> list = <int>[];
                    print(list[100]);
                  });
                }),
            FlatButton(
                child: const Text('Record Error'),
                onPressed: () {
                  try {
                    throw 'error_example';
                  } catch (e, s) {
                    // "context" will append the word "thrown" in the
                    // Crashlytics console.
                    Crashlytics.instance
                        .recordError(e, s, context: 'as an example');
                  }
                }),
// ▼▼▼ Widget の呼び出しの追加ココカラ ▼▼▼
            _buildGoogleSignInBody(),
// ▲▲▲ Widget の呼び出しの追加ココマデ ▲▲▲
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

これで対応は完了です。

おわりに

以上で(前編)は終わりです。

(後編)では、いよいよ、Google Photo APIをコールして、アルバムの情報などを取得していきたいと思います。乞うご期待!

最後に、この記事がFlutterを始められる方にとって少しでも助けになれば、幸いです。

kami_teru
SIを楽しむプログラマです。いろんなものを作るのが好き。
peoplesoftware
主にJavaやC#を使った受託開発や、BaaSなどのクラウドサービス、スマホアプリの自社開発を行っている会社です。※各記事の内容は個人の見解であり、所属する会社の公式見解ではありません。
http://www.pscsrv.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away