WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

リネットのiOSアプリにFlutterを導入しました

Flutter導入の背景

解決したい大きい課題は2つありました。

  1. iOS、Androidを並行して効率的に開発を行うため
  2. モダンな開発体験(DX)を導入するため

リネットではiOSとAndroidアプリの開発を社内で開発しており、両プラットフォームを同じ開発者が担当しているため、利用ユーザー数の関係で優先順位の偏りがでてしまいAndroidの開発が遅れがちです。

DX観点でも現状のネイティブ開発でUI確認をする為にシミュレーターで動作させる際のビルド時間や SwiftUIを導入するためにはiOS13以上が必要(だけどまだ下限を上げられない)など改善したい点が出ていました。

検討を始めたころちょうど良いタイミングで、急ぎではない複数画面が必要な開発要件が発生したこともあり、Flutterの導入をしての開発をチームで決定しました。

FlutterかReact Nativeか

どのマルチプラットフォームを使用するかの選定については、宣言的UIを実現可能でiOS、Androidの開発でメジャーなものとしてFlutterとReact Nativeを候補としました。

アプリ開発チームのメンバーでNodeのエコシステムを用いたフロントエンド開発経験者は1人(しかもブランクあり)しかおらず、チームのスキル的にはイチから獲得する必要があることもありFlutterとReact Nativeをモバイルアプリ開発に絞って検討しました。

比較して感覚的な部分は多いのですがFlutterの方がAndroidStudioでAndroidのGradleのようにライブラリ管理出来、ライブラリがどのプラットフォームで使えるのかわかりやすいため我々のチームではFlutterを採用する事としました。この辺は元々Androidの開発体験が好きというバイアスもあると思います。

Flutterの導入方法

Flutterをどうアプリに組み込んだのか

FlutterはFlutterのみでアプリを作成することも、ネイティブのアプリの一部として使う(Add-to-app)事もできます。 リネットアプリはすでにある程度ネイティブで開発がされており、Flutterの導入は新機能を素早くiOS、Androidに実装するためでフルリニューアルを想定していたものでは無かったため必然的にネイティブアプリの一部として導入しました。

iOSへの組み込みはFlutter部分とネイティブ部分両方とも同じ開発者が触るため、CocoaPodsを用いてネイティブビルド実行時にビルドを行う方式を採用しました。 Flutterの開発自体は単独でAndroidStudioで行い、Hot reloadなどのFlutterの開発体験のメリットを受けられるようになっています。

Flutterとネイティブの連携

Flutterの画面を開く仕組み

Flutter画面を表示するFlutterViewControllerをAppDelegateにて作成し保持しています。 表示する時にAppDelegateで持っているFlutterViewControllerにFlutter側のメソッドを呼び出して表示する画面を指定して切替をしてから。 ネイティブ画面をFlutterViewControllerを表示します。

f:id:nakamigawa:20220124114259p:plain
Flutter画面切替イメージ

Flutterからネイティブのメソッドを呼び出す仕組み

FlutterのMethodChannelという仕組みを用いています。 iOSのAppDelegateにてチャンネルとハンドラを設定していて、 このチャンネルを通してDartとiOSが双方向にメソッド呼び出しができます。 WebView表示はDart側からネイティブのメソッドを呼び出し、そのメソッド内で画面遷移する事で実現しています。

MethodChannelを使ってFlutterからネイティブの情報取得も行っており、ログイン情報やBuildFlaverなどの取得を行っています。

f:id:nakamigawa:20220124114401p:plain
Flutterとネイティブのメソッド呼び出し

Flutterからネイティブ画面へ戻る処理

開発においてネイティブとFlutterの画面を行き来する状態が発生するとおもいます。

例えば以下のような画面遷移。(テキストの表現でみづらくてすみません)

  1. [ネイティブ画面A] -> [Flutter画面A] -> [ネイティブ画面B]
  2. [ネイティブ画面A] -> [Flutter画面A] -> [Flutter画面B]

ネイティブのナビゲーションバーを表示しないようにしてFlutter側のナビゲーションバーを使いつつFlutter画面Aの時は戻るボタンを押した時にWillPopScopeWidgetでラップしMethodChannelでネイティブの画面に戻るようにしました。

  1. [ネイティブ画面A] -> [Flutter画面C] -> [Flutter画面A]

↑のように呼ばれる階層が異なる事がある場合はもう一手間必要になりそうですが現時点では存在しないので発生した時に検討しようと思います。 WillPopScopeWidgetでラップするかしないかなので対応自体は難しくなさそうなイメージです。

この仕組みにした背景

当初はネイティブのナビゲーションバーをFlutterViewControllerを開く際もつけることでネイティブとしての画面制御を行いました。

1のケースはこれで問題が無かったのですが、 2のケースではFlutter画面Bにいる時にナビゲーションバーの戻る操作でネイティブ画面Aへ戻ってしまいう事に気が付きました。

Flutterモジュール単体での動作

デバッグを目的としてFlutterモジュール単体で実行が可能です。 ただし、その場合はMethodChannelによる画面遷移や情報取得が行えません。

単体実行時にMethodChannelによる情報取得が出来ない事については コンフィグを管理するクラスにて吸収するようにしています。

Flutter導入を終えての振り返り

当初解決したい課題としていた2つのうち。 Flutterの開発体験はとても良く「モダンな開発体験(DX)を導入するため」は達成出来ました。 AndroidへのFlutter導入がまだのために「iOS、Androidを並行して効率的に開発を行うため」が未達成となっていますが、Androidへの導入はiOSでのリリース後時期を見てとなっているためこれから検討して行きます。

Flutter部分についても現在開発した画面が簡易なもので状態管理ライブラリなどを導入をしていないためこれから整える必要があります。

今回紹介したFlutterとネイティブの連携部分はやってみて出てきた課題の部分が多いです。 当初はFlutterで作成した画面を単純に表示出来れば良いと考えていたのですが実際に動かして見ると不自然な画面遷移となることや、リリースビルドがシミュレーターでは動かないのでネイティブのビルドに手を加えるなど。

当初の見積もりよりもネイティブとFlutterそれぞれで対応が必要な事が多く発生しました。

幸いリリースまで余裕のある開発案件だったため事なきを得ました。振り返ってみると導入するにあたり最適な案件で導入が出来たと思います。