Xamarin やめて Flutter 試す。結構(いやかなり)よいんじゃないか。
GAE(Spring Boot) + Webアプリ(Vuetify) + モバイル(Android、iOS) + Firebaseで、アプリケーションのテンプレートを作成しようと少しずつ作業していたけのだが、Xamarin に Firebaseを統合するところではまって進まない。iOS環境の知識がないのもあるのだが、なかなかまとまった時間が取れない中、丸一日とかかけて進捗がないのはつらい。
- Google App Engine Java Standard + Spring Boot 環境構築
- Spring Bootのテンプレートエンジン Thymeleafを適用
- Spring BootにVue.jsを利用する
- Spring BootにVuetify導入からクロスドメインAjax通信
- Spring Boot+Vuetify GAEへデプロイ、動作確認
- Xamarin.FormsアプリをiOS用にビルド
- Xamarin.Forms ポップアップの表示と Http通信
- Spring Boot+Vuetify ファイルのアップロードとJSONで結果を返す
- Google Google Cloud Vision でOCR。VuetifyからSpringBoot経由で呼び出し
- Xamarin ファイル選択
- Xamarin ファイルのアップロード
- Vue Router を導入し画面遷移
- 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も合わせてインストールされる
Flutterプロジェクトを作成する。
このまま起動すると、ボタンを押すと、カウンターがインクリメントされるデモアプリケーションが実行される。
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)
手順通りに作業して、疎通用のコードを書いて、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 で足りない内容の実施 メモ)
まぁ、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
loginボタンを押すことで、Googleアカウント選択ダイアログが表示される。
ログインすると、ユーザー情報が取得されて、名前が表示される!!
いいね。
iOS
Android実機と同じことを、Mac上のAndroid Studioとシミュレーターで確認
$ open –a Simulator
でシミュレータを起動すると、Android Studioに表示されるので、実行
login ボタンで同様に、Google ログインダイアログ表示。
ログインすると、ユーザー情報取得されて、名前が表示された。
うーん。一週間ぐらいかかってしまったけど、いいんじゃないかな。
Android側で確認した内容が修正なしで、iOSで動いてくれるなら非常に捗る(予定)!
Firebase 証明書のくだり、デフォルトのパスワードは android