Add Flutter to existing apps
@neonankiti
https://gyazo.com/95acc6ce0f66ea9ae1352885c2b0d351
自己紹介
https://gyazo.com/27bdad70541d1f9c6bd4867bd179fdc0
南里勇気(なんりゆうき)
あだ名
Bison
職業
Android Manager/Developer
@neonankiti (Twitter, Github)
Working at FiNC Inc.
本を出版しました!!
10/31出版!!
Flutter モバイルアプリ開発バイブル
https://gyazo.com/ac60a3528a0c61a72d7eea3e3de51021
ターゲットは初級者〜中級者
基礎的な内容 + アプリリリースのための周辺知識
Flutterが普及する上で、日本語ベースの体系だった参考書がなかったことが執筆のきっかけ
英語のドキュメンテーションは豊富で、英語に抵抗感がない人はそちらがオススメ。
献本させて頂いたmonoさんからもレビュー頂きました
FiNCについて
https://gyazo.com/cb4d096fe5f36d7e4ebdc16cd942147b
Vision: 一生に一度のかけがえのない人生の成功をサポートする
Mission: すべての人にパーソナルコーチを
ヘルステックベンチャーとして、ヘルスケアアプリを軸として事業展開
データ収集→データ分析→ユーザーへの行動促進
なぜFiNCでFlutterなのか?
既存アプリに対して、共通コンポーネントの埋め込み
スクラッチ開発での技術検証
FiNCにおけるプロジェクトの特性
FiNCのアプリ開発の詳細と特徴の説明をします。
FiNCはヘルスケアプラットフォームであり、社内のプロジェクトは大きく二つに分類される。
1. 本体アプリ
2. 事業提携プロジェクト
1の場合(検証中)
チームメンバーは職能ごとに分断されている。(iOS/Android/サーバー)
単一の機能を同タイミングでリリースしている運用のため、iOS/Androidのリソースが別々で必要。
既にプラットフォームごとの巨大なコードベースがあるため、現状これを作り直す意思決定は難しい。
また、プラットフォーム固有の実装が不要な機能がある。また、アニメーションなどのリッチなUI表現もある。
例えば、プラットフォームに依存しないUIでFiNC独自のリッチなUIはFlutterに最適ではないか?更に既存のアプリの上にFlutterを乗せるという意味でも可能性がある。
2の場合(実際に運用中)
FiNCアプリはプラットフォームアプリであり、外部サービス、または事業提携によるプロジェクトも活発である。
検証のためのモック作成や1の本体アプリとは別のアプリをプロジェクトとして推進することがある。
リソースを全面的にはれない場合などにリソース削減の手法としてFlutterは利用しやすい。
ただし、Flutterを利用するとiOS1人, Android1人の工数がFlutter1人になるわけではないことを理解すること
プラットフォーム固有のコンポーネントの実装が多い場合、プラグイン開発が必要になるため、iOS/Androidの実装経験がないと作業工数は大きくなりやすい。
またプラグイン開発を行わない場合でも、iOS/Android固有の知識は一定必要です。
Add Flutter to Exisiting Appsとは
既存のアプリ上に、Flutterのコンポーネントを描画すること
イメージ図
https://gyazo.com/247b77788230714648bdd327eacba5d2
2019/10/07現在、in preview状態です。また利用可能なchannelはmasterであるため注意しましょう。
FlutterViewは、Flutter独自のレンダリングエンジンを利用して描画を行うため、プラットフォームの描画の仕組みとは違うパラダイムを持つ。
おまけ
PlatformViewを利用するとFlutterのウィジェットツリー構造にプラットフォームのコンポーネントを埋め込めることが可能。
add2appの全体像
Step1: 環境設定
Step2: Flutterのモジュール作成
Step3: 既存アプリ(ホストアプリ)からの参照
Step4: 既存アプリ側でFlutterの利用
Step5: Flutter側の実装
https://gyazo.com/abe094f910268139e6f8ce1a8bce1e0e
今回はAndroid側について話します。
Step1 環境設定
iOSでも動かす必要があるため、Flutterのバージョンは、Flutter 1.8.4-pre.21が必須。
バージョン条件を満たさない場合は、flutter upgradeコマンドで更新する。
masterチャンネルでしか、利用できないため、flutter channel コマンドで確認する。
https://gyazo.com/fff600237e6402d165746c4916a72b45
Step2 Flutterのモジュール作成
全体の構成は以下です。
code:structure
AddToAppSample
|---.android
|---AndroidHostApp
|---lib
|---pubspec.yml
|---awase_flutter_android.iml
|---awase_flutter.iml
既存アプリ(以後ホストアプリを呼びます)を作成してください。ここでは、AndroidHostAppというプロジェクト名にします。
AndroidHostAppから外部モジュールであるFlutterモジュールを利用するため、親ディレクトリ(AddToAppSample)上に移動します。
既存アプリ上にのせるFlutterコンポーネントを提供するモジュールを作成します。
今回はawase_flutterというFlutterモジュールを作成したいため、以下のコマンドを実行します。
flutter create -t module --org com.awaseflutter awase_flutter
.android や iml拡張子のファイルが生成されます。
注意点
モジュールを作成するflutterコマンドはAndroidX対応を行っていません。Android Studioで既存アプリを作成する場合、もしAndroid SDKのバージョンがAndroid Q以上(29)だと、AndroidXがデフォルト設定として強制的に選択されます。そのため、現時点ではandroidxを無効にするために Tools->SDK Manager->Appearence & Behavior->System Settings->Android SDK で29を削除しましょう。
AndroidHostApp直下のappディレクトリ内のbuild.gradleファイルに以下を追記しましょう。
code:build.gradle
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
Step3: 既存アプリ(ホストアプリ)からの参照
ホストアプリに依存性を追加しFlutterコンポーネントを参照出来るようにします。依存性の追加方法は二つあります。AAR(Android Archive)とモジュールソースコードを利用する方法です。ここでは後者のモジュールソースコードの方法について説明します。
モジュールソースコードに追加する方法はホストアプリ(AndroidHostApp)のsettings.gradleに以下を追加してください。
code:settings.gradle(AndroidHostApp)
include ':app' // AndroidHostAppがappモジュールを参照している
rootProject.name = 'AndroidHostApp'
setBinding(new Binding(gradle: this))
evaluate(new File(
settingsDir.parentFile,
'./.android/include_flutter.groovy'
))
Step2で作成したawase_flutterモジュールと同階層に自動生成された.androidファイル内に存在する include_flutter.groovyというスクリプトを参照し、実行するための設定を行う。
setBindingとevaluateにより、このモジュール自体が:flutterモジュールとして参照可能になります。
flutterモジュールとして参照可能になったので、ホストアプリ(AndroidHostApp)のapp以下のbuild.gradleに依存性を追加し、ホストアプリ側から参照できるようにします。
code:build.gradle(app)
dependencies {
// 略
implementation project(':flutter')
}
Step4: 既存アプリ側でFlutterの利用
完成系の確認
https://gyazo.com/b4079062c7c11196d935a892fab8be1c
ボタン1→FlutterViewの生成
ボタン2→Android→Flutterの画面起動(キャッシュなし)
ボタン3→Android→Flutterの画面起動(キャッシュあり)
全体的なソースコード
1. FlutterViewの生成
AndroidのActivity(画面)上でFlutterViewを生成して描画する
code:MainActivity.kt
create_view_button.setOnClickListener {
val flutterView = Flutter.createView(
this@MainActivity,
lifecycle,
"route1"
)
val layout = FrameLayout.LayoutParams(600, 800)
layout.leftMargin = 100
layout.topMargin = 600
addContentView(flutterView, layout)
}
FlutterのViewの生成時に、ライフサイクルとルートの情報を保持する。
2. Android→Flutterの画面起動(キャッシュなし)
FlutterEngineのキャッシュを行わずFlutterの画面を起動&描画する。
code:MainActivity.kt
start_activity_without_engine_cache.setOnClickListener {
val defaultFlutter = FlutterActivity.createDefaultIntent(this)
startActivity(defaultFlutter)
}
code:AndroidManifest.xml
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:exported="true"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize">
Androidでは、Activityの詳細をManifest.xmlに記述する必要がある。(なければクラッシュ)
Flutterアプリケーションを初期化して、FlutterEngineを起動するのはオーバーヘッドが大きいため時間がかかる。シンプルな起動方法
Flutterエンジンとは?/neonankiti-portfolio/Flutter Engineことはじめ
3. Android→Flutterの画面起動(キャッシュあり)
FlutterEngineのキャッシュを行いFlutterの画面を起動&描画する。2のキャッシュがない場合と違い、かなり高速化している。
code:MainActivity.kt
// Flutterエンジンの初期化
val flutterEngine = FlutterEngine(this)
// Dartのエントリ-ポイントの決定(Flutterのアプリケーションを起動した際に、dartのどの関数を呼ぶか)
// デフォルトはmain()関数
flutterEngine
.dartExecutor
.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
// 提供されているキャッシュの仕組みを利用してFlutterEngineをメモリに書き込む
FlutterEngineCache
.getInstance()
.put(
ENGINE_ID,
flutterEngine
)
start_activity_with_engine_cache.setOnClickListener {
val intent = FlutterActivity
// キャッシュしているFlutterエンジンを取得する
.withCachedEngine(ENGINE_ID)
.build(this)
startActivity(intent)
}
AndroidManifest.xmlは既に2で記述しているので省略
Flutterから提供されるキャッシュの仕組みを利用して、FlutterEngine初期化時のコストをなくす。
Flutterへの指定パラメータ
Flutterの画面(Activity)を起動する際に、いくつかのパラメータを指定することができる。
指定パラメータは以下の四つ
Entrypoint
Dartの呼び出し関数のこと。デフォルトはmainが呼ばれるが、その他のメソッドを呼びたい場合は、関数名をそのまま指定する。
InitialRoute
Flutter側でルーティングを行うためのパラメータを渡す。これによりFlutterのどの画面を開くのか決定できる。
SplashScreenDrawable
Flutterを起動する際のスプラッシュ画面のリソースを指定する。
NormalTheme
Flutterアプリのテーマを設定する。この仕組みを使って透過なども実現できる。
AndroidManifest.xmlに指定するnameは、ここを参照で以下の四つ。
io.flutter.Entrypoint
io.flutter.InitialRoute
io.flutter.embedding.android.SplashScreenDrawable
io.flutter.embedding.android.NormalTheme
注意
内部クラスにおいて実装ミスに思える実装が。。
code:FlutterActivity.java
private Drawable getSplashScreenFromManifest() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(
getComponentName(),
PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES
);
Bundle metadata = activityInfo.metaData;
Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
return splashScreenId != null
? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
? getResources().getDrawable(splashScreenId, getTheme())
: getResources().getDrawable(splashScreenId)
: null;
} catch (PackageManager.NameNotFoundException e) {
// This is never expected to happen.
return null;
}
}
透過画面を設定できると書いているが、FlutterActivityLaunchConfigsのコンストラクタがprivateなため透過を実現できない。
コード箇所
デフォルトのbackgroundモードがenumの文字列指定なため、AndroidManifest.xml側のNormalThemeでtransparentを与えてあげると実現できそう。(上記のクラッシュの影響で未確認)
Step5: Flutter側の実装
Step4で見ると様々な呼び出し方が存在するが、気をつける部分は何をパラメータとして利用したいか?である
つまり、Entrypoint、InitialRoute、SplashScreenDrawable、NormalThemeに関して明確なルールを持っていれば問題ない。
code:main.dart
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return Center(
child: Text('route: $route', textDirection: TextDirection.ltr),
);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
window.defaultRouteNameには呼び出し側で動的もしくは静的(AndroidManifest.xml)にセットした値が振り当てられる。
次回までに検証したいこと
iOSの同様の実装(Experimentalなのでいい塩梅で)
画面またぎのFlutterEngineのキャッシュ
SingletonもしくはApplicationのサブクラスでデータを保持する。Flutterと言うよりかは、Androidの実装
参考
プラットフォームとFlutter間のChannelを利用したデータメッセージング
Plugin開発によるプラットフォームとFlutter間のインターフェース定義
Flutterには利用するデータのみを渡す? = ロジックはプラットフォーム側のみに残し、Viewコンポーネント(UI層のみ)として利用するべきなのか、検証が必要。
知見がないので、もし経験ある人の意見聞きたいです。
エンジニア募集
Flutter一緒にやりませんか?
ぜひ、お話だけでも!DMください!全開放してます!
#flutter