Xamarin やめて Flutter 試す。結構(いやかなり)よいんじゃないか。

GAE(Spring Boot) + Webアプリ(Vuetify) + モバイル(Android、iOS) + Firebaseで、アプリケーションのテンプレートを作成しようと少しずつ作業していたけのだが、Xamarin に Firebaseを統合するところではまって進まない。iOS環境の知識がないのもあるのだが、なかなかまとまった時間が取れない中、丸一日とかかけて進捗がないのはつらい。

  1. Google App Engine Java Standard + Spring Boot 環境構築
  2. Spring Bootのテンプレートエンジン Thymeleafを適用
  3. Spring BootにVue.jsを利用する
  4. Spring BootにVuetify導入からクロスドメインAjax通信
  5. Spring Boot+Vuetify GAEへデプロイ、動作確認
  6. Xamarin.FormsアプリをiOS用にビルド
  7. Xamarin.Forms ポップアップの表示と Http通信
  8. Spring Boot+Vuetify ファイルのアップロードとJSONで結果を返す
  9. Google Google Cloud Vision でOCR。VuetifyからSpringBoot経由で呼び出し
  10. Xamarin ファイル選択
  11. Xamarin ファイルのアップロード
  12. Vue Router を導入し画面遷移
  13. Firebaseでログイン機能

Xamarinなかなかハードルが高いと思っていたところで、Flutterがよさそうなので、試してみる。よさそうならXamarinやめてFlutterにする。

Flutter

Flutterは、Googleが開発しているAndroidやiOS用のネイティブアプリケーションをDart言語で開発できるフレームワーク。Webアプリも生成できるようになりそうだし、うまいこと行けば、これ一つで全部行ける。うまいこと行けばだけど。そんなうまい話があればうれしい。Write onece run anywhere. のコンセプトは20年たっても有効だ。

半額キャンペーンをやっていたので、購入して斜め読み。

イメージはつかめるが、発展途上の環境などはすぐに手順など変わるので、上記書籍の通りやればできると甘く考えないほうがよい。(結局いろいろはまりまくりました)

Dartの書籍も、Kindleで購入して斜めよみして大まかな感触をとらえる。まぁ今どきの言語はだいたいできることは同じだし、文法的にはほぼJavaっぽい。IDEが補完してくれて、ネットでリファレンスがみられるのでなんでもよい。インテリセンスがなくて、インターネットがないころは、書籍を手元において、文法を頭に叩き込むしかなかったけど。

Android Studio

Android Studio に Flutterプラグインを導入。Dart のSDKも合わせてインストールされる

flutter01

Flutterプロジェクトを作成する。

flutter02

flutter03

このまま起動すると、ボタンを押すと、カウンターがインクリメントされるデモアプリケーションが実行される。

Firebase

FlutterがGoogle謹製なので、Firebaseとの親和性に大いに期待。Xamarinでの不満は、Firebase関連のライブラリを使うのがややこやしいのもある。

書籍でもネットでも参考にFirebaseに、Android および iOSのアプリケーションを追加する。

https://firebase.google.com/docs/?authuser=1

https://console.firebase.google.com/u/0/

Firebaseのプロジェクトに、AndroidやiOSを追加するときに、ウィザード的に手順(どのファイルにどういうコードを追加する)が出るのだが、そこでまずひとはまり。

Firebase エラー

I/flutter ( 3823): PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10: , null)

https://stackoverflow.com/questions/54557479/flutter-and-google-sign-in-plugin-platformexceptionsign-in-failed-com-google

手順通りに作業して、疎通用のコードを書いて、GoogleログインをFirebaseでしようと思うのだが、上記のエラー。さんざんStackoverflowなど参考に試すのだが、解決せず、、、

結局原因は、Flutter プラグインが想定する Firebaseのバージョン(ちと古い)と、Firebaseサイトの手順で出てくるバージョン(当然最新)が不一致だったことによる。プラグイン側に合わせて事なきを得る。

Firebase証明書

Firebase で認証のデバッグをするには、以下のコマンドで署名証明書の発行および登録が必要。

https://developers.google.com/android/guides/client-auth

リリース用

keytool -exportcert -list -v \
-alias <your-key-name> -keystore <path-to-production-keystore>

デバッグ用

Windows

keytool -list -v \
-alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore

Mac/linux

keytool -list -v \
-alias androiddebugkey -keystore ~/.android/debug.keystore

iOS

https://flutter.dev/docs/get-started/install/macos

Flutter の iOS 手順は、上記を参照したのだが、おおはまりしたのが、

Deploy to iOS devices - Follow the Xcode signing flow to provision your project:

おそらくこの手順。ここまでの手順は必要

(flutter doctor で足りない内容の実施 メモ)

ios_error

まぁ、xcode自体触ったことがなく意味が分かっていないからなのだろうが、xcodeの画面からこの辺りを編集したためか、info.plist の内容が消えてしまっていたと思われる。(git diff を取ったら、Flutter プラグインで新規プロジェクトを作成したときに生成された内容が消えていた) いったん、git checkout . したうえで、手動で、CFBundleURLSchemes エントリを、元に戻したinfo.plist に転記。

あ、これは、以下のエラーが出たので追加

flutter: PlatformException(google_sign_in, Your app is missing support for the following URL schemes:

ちなみに、以下のようなエラーがつぶしてもつぶしても出てきて、Flutter、お前もか。。とあきらめかけていました。

Ensure that the application's Info.plist contains a value for CFBundleIdentifier.

The bundle identifier of the application could not be determined. Ensure that the application's Info.plist contains a value for CFBundleIdentifier.

とか

Runner.app has missing or invalid CFBundleExecutable in its Info.plist
Underlying error (domain=MIInstallerErrorDomain, code=11):
     Bundle at path

Source

最終的に、Firebaseを統合して、Android(実機) と iOS(シミュレータ)で動作確認を取ったソースコードをメモ。

アプリケーションレベル build.gradle

buildscript {
    ext.kotlin_version = '1.2.71'
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath 'com.google.gms:google-services:3.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

プロジェクトレベルの build.gradle

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 27

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    lintOptions {
        disable 'InvalidPackage'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "info.typea.favo_phrase"
        minSdkVersion 22
        targetSdkVersion 27
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.google.firebase:firebase-analytics:16.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

apply plugin: 'com.google.gms.google-services'

/lib/main.dart

Googleサインインを行うサンプル実装

ログイン、ログアウトボタンを配置し、処理を実装。ログインユーザーの表示名を画面に表示させる。

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


final GoogleSignIn _googleSignIn = GoogleSignIn();
final FirebaseAuth _auth = FirebaseAuth.instance;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  FirebaseUser _user = null;
  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  Future<FirebaseUser> _handleSignIn() async {
    GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    GoogleSignInAuthentication googleAuth = await googleUser.authentication;
    FirebaseUser user = (await _auth.signInWithCredential(
      GoogleAuthProvider.getCredential(
          idToken: googleAuth.idToken,
          accessToken: googleAuth.accessToken)
    )).user;
    print("sign in ${_user?.displayName}");

    return user;
  }

  Future<void> _handleSignOut() async {
    await _auth.signOut();
    await _googleSignIn.signOut();
    return;
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${_user?.displayName} $_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            RaisedButton(
              onPressed: () {
                _handleSignIn()
                    .then((user) {
                      print(user);
                      setState(() {
                        _user = user;
                      });
                    }
                ).catchError(
                  (e) => print(e)
                );
              },
              child: const Text(
                'Login',
              ),
            ),
            RaisedButton(
              onPressed: (){
                _handleSignOut().then(
                    (e){setState((){_user=null;});}
                );
              },
              child: const Text('Logout'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

うーん。確かにアプリケーション起動までは、通常と同じく時間がかかるが、起動してしまった後のUIの修正は、ほぼ即時反映される。これは捗る。

Android実機で確認

起動時、ログインされていない状態。ユーザー=null

flutter_firebase_auth01

loginボタンを押すことで、Googleアカウント選択ダイアログが表示される。

flutter_firebase_auth02

ログインすると、ユーザー情報が取得されて、名前が表示される!!

いいね。

flutter_firebase_auth03

iOS

Android実機と同じことを、Mac上のAndroid Studioとシミュレーターで確認

$ open –a Simulator

でシミュレータを起動すると、Android Studioに表示されるので、実行

firebase1

login ボタンで同様に、Google ログインダイアログ表示。

firebase2

ログインすると、ユーザー情報取得されて、名前が表示された。

うーん。一週間ぐらいかかってしまったけど、いいんじゃないかな。

Android側で確認した内容が修正なしで、iOSで動いてくれるなら非常に捗る(予定)!