「React Native」「Kotlin Multiplatform」「Flutter」を勧めると返ってくる反応

垰尚太朗氏:まず自己紹介からすると、CL事業部にiOSエンジニアとして所属しているTao Shotaroです。今回は『FlutterにiOS14などの新機能を取り込むには by iOSエンジニア』というタイトルで話していきたいと思います。少しiOSについての話が多いですが、ご了承ください。内容も簡単なのでサラッと見ていただけると幸いです。

それでは課題から話します。React Native、Kotlin Multiplatform、Flutterのようなネイティブコードを使わずにアプリ開発ができる言語を人に勧めると、「え、UIってどうなるの?」とか「機能が制限されそう」とか「iOS・Androidに出る新しい機能も取り入れたい」とかの反応が返ってくるのが多いので、今回はiOS・Androidの新しい機能も取り入れるという点について、実際どのように対応していくのかを紹介したいと思います。

MethodChannelとEventChannelのメソッドを使い分ける

まずFlutterには、MethodChannelというクライアント側とプラットフォーム側が通信するためのメソッドが用意されています。Flutter側はこのMethodChannelを利用して、プラットフォームであるiOSやAndroid側にメッセージを送ります。

するとiOS側はFlutterMethodChannel、Android側はMethodChannelというクラスがそのメッセージを受け取り、必要なデータを返します。

このMethodChannelを利用すると、比較的簡単にネイティブのコードを呼び出せます。またデータを定期的に流す場合は、MethodChannelのストリーム版であるEventChannelというメソッドを利用できます。

デバイスのバージョンや名前などの情報を取得したいときはMethodChannel、ユーザーの位置情報を定期的に取得したい場合はEventChannelなど、必要に応じて使い分けるのがいいです。

データの受け渡しですが、MessageCodecに準拠したエンコーダによってバイトデータに変換されます。MethodChannelとEventChannelは標準でStandardMessageCodecというエンコーダを利用しています。

このStandardMessageCodecが対応している型はここに表示してあるとおりで、null、Boolean、Int、Double、String、List、Mapです。独自のデータ型を使ってやりとりしたい場合は、MessageCodecに準拠した独自のエンコーダも用意できます。

MethodChannelとEventChannelの使い方は簡単

次にMethodChannelとEventChannelの使い方です。使い方は簡単で、まずアプリケーションを用意して、プラットフォーム側のAPIを利用したい画面、もしくはウィジェットのStateでMethodChannelを宣言します。このとき、チャンネルの名前はFlutter側とiOS側、Android側で統一しなければいけません。

そして、先ほど宣言したチャンネルを使ってプラットフォーム側にメッセージを送ります。今回の場合は、getDeviceInfoです。すると、このinvokeMethodが非同期で結果を返します。今回はMapを返すように実装しているので、dynamicMapにキャストしています。

dynamicタイプのMapが返ってくるので、それを_deviceInfoで保持しています。stateの中に入っている_deviceInfoですね。最後にこの関数はinit stateで呼んでいます。

iOS側の実装は先ほどの同じ名前を使って、FlutterMethodChannelを宣言します。今回はdeviceinfoと設定しています。

このときアプリ上で動いているFlutterエンジンが1つとは限らないので、windowから先ほど書いたFlutter側のコードが動いているFlutterViewControllerを取得して、channelに設定しま す。このbinaryMessengerというところです。

このdeviceInfoChannelを使って、メッセージのハンドリングを行います。このとき気をつけないといけないことは、このメソッドはメインスレッドで実行されているので、非同期処理をここで行う場合は別スレッドに移動しないといけないということです。

今回の場合はdeviceInfoのDictionary型を返しました。これで実行すると、きちんとiOSの情報が表示されるのですが、すみません、実行結果の画像を忘れてしまいました(笑)。今回はiOSだけの紹介になりましたが、Androidでも受け渡すデータを揃えれば同じ方法で実装できます。

公式のライブラリ device_infoとshared_preferences

先ほどの実装を1つのパッケージにしてプラグインとして公開しているのが、公式から出ているdevice_infoというライブラリです。このライブラリを見ると、Objective-Cで書かれているものの、ほとんど同じような実装をしていることがわかります。

ここの部分でFlutterMethodChannelを宣言して、handleMethodCallという関数でイベントのハンドリングを行なっています。

shared_preferencesというUser Defaultの管理などに使われるライブラリも同じで、内部でFlutterMethodChannelを用意してgetAllやsteBoolやsetIntなどのイベントをハンドリングしています。

基本的なネイティブAPIは公式ライブラリか有志のオープンソースを活用

まとめです。プラットフォーム側のAPIを利用したい場合は、MethodChannelやEventChannelを利用することでデータのやりとりができることがわかりました。

コードの半分ほどは、プラットフォーム側のネイティブコードを書いているのですが、通常のアプリで使用するような位置情報の取得や、デバイス情報の取得や、ローカルステージのデータの保存、ブラウザの表示など、基本的なネイティブAPIはすでにライブラリとして、公式からもしくは有志によってオープンソースとして公開されています。

ライブラリがないときや、すでに自分がiOS、Androidのライブラリとして公開しているものをFlutterで使いたいときは、このMethodChannelやEventChannelを利用することで使えます。

新機能のApp ClipsとWidgetsはどう対応するか

今回は『iOS14の新機能を取り込むには』というお題だったので、続いて「App Clips」と「Widgets」について話していきたいと思います。

MethodChannelとEventChannelは、ある特定のAPIを利用したいときに使用できるのですが、App ClipsやWidgetsのようにメインプロジェクトとはターゲットが異なり、起動するタイミングも違って、別プロジェクトのような機能が追加されたときはどのように対応したらいいでしょうか。

やり方は2つあります。基本的にネイティブですべて実装して、データのやりとりだけMethodChannelやEventChannelを利用する方法と、メインアプリとは別にFlutterEngineやFlutterViewControllerを用意してFlutterで実装する方法です。

後者のFlutterEngineやFlutterViewControllerを用意してFlutterで実装する方法は少し複雑なので、順番に解説していきたいと思います。

Flutterの仕組み

まずFlutterの仕組みを簡単に説明します。FlutterのアプリケーションのコアがFlutterEngineです。真ん中にある青いやつですね。これはC++で書かれていて、画面の描画やレイアウトの計算など、ネットワークに関する低レイヤーの実装がされています。

そして、その上に実装されているのが、Dart UIというフレームワークです。FlutterのC++のコードをラップしてくれます。Flutterでアプリを作る場合、基本的にはこのフレームワークか、この上に存在するWidgetsやMaterialなどのUIフレームワークを使用します。

FlutterEngineの下にはネイティブのコードが実装されていて、先ほどのMethodChannelで定義したネイティブのコードは、このPlatform-SpecificのNative Pluginsにあたります。これはFlutterのドキュメントに詳しく説明が書かれているので、勉強したい方は読んでみるといいと思います。

「App Clips」は一部の機能を切り取ったアプリ

App Clipsがどういうものかというと、本体のアプリとは別の必要最低限の部分だけを取ったアプリです。

App Clipsは10MBの制限があるので、FlutterEngineをこの中で実装するのは難しいと言われています。ただ、Flutterの開発者のがんばりによって、今は10.6MBまで下げられたので、もう少ししたらApp Clips上でFlutterEngineを動かせるんじゃないかなと言われています。

ローカルで試すだけであれば10MBを超えても大丈夫みたいなので、実際にやり方を紹介していきたいと思います。けっこう簡単で、まずApp Clipsのターゲットを作成して、そこの中でFlutterEngineとFlutterViewControllerを宣言して、FlutterViewControllerを表示するだけです。

FlutterViewControllerはFlutterEngineを動かしている画面です。Androidにも同じようなものがあって、確かFlutterViewがその役割を担っています。

このようにFlutterEngineとFlutterViewControllerを宣言します。Entrypointとして初期画面を指定することもできるので、App Clips専用の画面を用意して、そこに流すこともできます。

App Clipsを実装するときは、FlutterのフレームワークをApp Clipsのターゲットへ追加したり、ビルドの設定をしたりなど細かい設定が必要なんですが、実は公式のガイドが存在していて、それに従って設定すれば実行できるので、それを見るといいと思います。

これは先ほど言ったとおり、10MBを超えていて、まだ実験段階です。実際にAppStoreで公開できている事例は見たことがありません。仮に10MBの制限をクリアできなかった場合、選択肢の1つ目の、基本的にネイティブで実装してデータのやりとりだけMethodChannelやEventChannelを利用する方法を取るしかなさそうです。

「Widget」を実装する現実的な方法

続いてiOS14から追加されたWidgetsですが、基本的には先ほどと同じでネイティブで実装する方法と、メインアプリとは別にFlutterEngineを用意する方法があります。後者のFlutterEngineを利用する方法は、UIをレンダリングするのにメモリがたくさん必要で、確かシミュレーターでは動くのですが、実機では動かないみたいな制限があるので、現実的ではありません。

現実的な方法として、データはshared_preferencesというUserDefaultのライブラリを使ってFlutter側から共有して、Widgetsでそのデータを取得して画面に表示するのが現実的な方法かなと思います。

MethodChannelを利用して、Widgetsの更新タイミングをFlutter側から呼び出すこともできます。ちなみにそのWidgetsの更新タイミングをFlutter側から呼び出すライブラリは、すでに有志によってオープンソースで公開されています。Flutter界隈が活発なのはすごくありがたいですね。

最後です。少し短かったのですが今回はMethodChannelとEventChannelの使い方と、別ターゲットでのFlutterEngineの動かし方について紹介しました。MethodChannelの使い方を理解しておくと、どうしてもネイティブのAPIを利用しないといけないときも幅広い対応を行えるので、ぜひ試してみてください。以上で終わります。ありがとうございました。