はじめに
Flutterでネイティブな機能を呼び出すためにはMethodChannelを使用することが一般的ですが、
その場合非同期での処理が前提になります。
そのため頻繁に呼び出しが発生するような場面にはあまり向かなかったり、そもそも非同期処理だと使えない場面というのもありそうです。
今回はそんな状況に対応するためのネイティブコードの同期的な呼び出しについて考えます。
【そんな状況】 ロケールに応じたソートをしたい
つい先日、文字列のロケールに応じたソートが必要になりました。
記事執筆時点では
- 文字列のロケールに応じたソートができる標準の機能はDartから提供されていない
- 公開されているパッケージにも該当するものがなさそう
そうなるとネイティブで提供されている以下のメソッドをDartから呼び出すのが良さそうです。
- AndroidではCollatorクラスの
compare
- iOSではNSStringクラスの
localizedCompare
これらをMethodChannelで呼び出しておしまい、なら話が早いですが
Dartのsortの比較関数には非同期処理は使用できないため、今回は同期的に処理する必要があります。
jnigen / jniとobjective_cを使用してみる
ネイティブコードを同期的に呼び出すとしたら、以前はFFIを自分で実装したり、さらにJavaメソッドだったらまずそれをCで呼べるように自分でJNIを準備するところから...だったかと思いますが、
最近はAndroidではjnigen/jni、iOSではobjective_cというパッケージがあるようです。
プラットフォームごとに早速導入していきます。
jnigen / jni
- jnigen 0.9.0: https://pub.dev/packages/jnigen
- jni 0.9.0: https://pub.dev/packages/jni
jnigenはJavaのライブラリにアクセスするためのDartコードを生成します。
生成されたDartコードは依存関係になっているjniを通じて、Java Native Interface (JNI) を経由してJavaメソッドを呼び出します。
以前はCのバインディングを生成する必要がありましたが、Dartのバインディングのみで完結できるオプションが足され、jnigen 0.9.0からはCのバインディングを生成する機能自体がなくなりました。
まずはプロジェクトのルートで以下コマンドを実行し、パッケージを導入します。
dart pub add jni dev:jnigen
次にJavaからどんなクラスを呼び出したいのか、生成したDartファイルをどこに置くのかなどを決める、基本設定用のYAMLファイルを作成します。
配置位置、名称については任意に決めることができます。
今回はプロジェクトのルートに配置、名称をjnigen.yaml
とします。
output:
dart:
path: lib/collator_lib.dart
structure: single_file
classes:
- 'java.text.Collator'
- 'java.util.Locale'
android_sdk_config:
add_gradle_deps: true
# プラグインとして使用する場合は追加
# android_example: 'example/'
preamble: |
// ignore_for_file: type=lint
上から順番に見ていくと
output: >> dart: >> path *
生成されたファイル置くpathを指定します。今回はlib直下にcollator_lib.dart
として配置します。
output:>> dart:>>structure
今回指定しているsingle_file
にすると参照するクラスのバインディングが一つのファイルにまとめられ、package_structure
にするとクラスごとにファイルを分けてくれます。
(package_structure
を使用する場合はpathの末尾がディレクトリになるようにする)
classes:
参照したいネイティブのクラスパスを指定します。
今回はCollatorクラスと、Collatorで日本語のロケールを設定するためにLocaleクラスを書いています。
android_sdk_config:>>add_gradle_deps
add_gradle_deps:
をtrueにするとAndroidプロジェクトで使用しているJARの依存関係のpathを自動で取得し、それを元にバインディングファイルを生成できるようになります。
これについては制限があり、一度リリースビルドを行ってビルドキャッシュを作成する必要があります。
This requires a release build to have happened before, so that all dependencies are cached appropriately.
ちなみにビルドなしでdart run jnigen
でバインディングを生成しようとすると
This can be because the Android build is not yet cached. Please run `flutter build apk` in ./ and try again
のエラーが表示され、flutter build apk
することを勧められます。
また、プラグインにjnigenを置く場合はプラグイン本体だけではビルドができないため、android_example: 'example/'
のようにビルド可能な環境を借りる形でexampleのルートを指定します。
preamble:
生成されるバインディングの先頭に配置する文字列を指定できます。
今回はflutter analyze
した時に警告まみれになることを避けるため、全てのLintルールを無視しています。
YAMLの用意ができたら--config
でpathとファイル名を指定し、以下のコマンドを実行します。
dart run jnigen --config jnigen.yaml
libの中を確認するとcollator_lib.dart
が生成され、以下のような形でDartからCollatorクラスのcompare
が使用できるようになりました。
import 'collator_lib.dart';
import 'package:jni/jni.dart';
...
# Localeを引数としたgetInstanceは1のsuffixがついていた
# 使用するメソッドによっては名称が変わる可能性があるため
# 生成されたファイルの中身を確認すること
final collator = Collator.getInstance1(Locale.JAPANESE);
# JavaのStringに変換するtoJString()がjniパッケージに用意されている
final result = collator.compare('hoge'.toJString(), 'fuga'.toJString());
objective_c
- objective_c 1.0.0: https://pub.dev/packages/objective_c
jnigenときたらffigen...の流れですが、ffigen 12.0.0のChangeLogを見ると
Core classes such as NSString have been moved intpu package:objective_c
https://pub.dev/packages/ffigen/changelogより
とあり、NSString
クラスをobjective_cパッケージから直接呼べるようになっていそうです。
プロジェクトに導入
dart pub add objective_c
以下コードで呼び出し
import 'package:objective_c/objective_c.dart';
...
final result = NSString("hoge").localizedCompare_(NSString("fuga"));
ffigenで実装した場合、jnigenと同じくらいの実装コストがありましたが
obejective_cパッケージを使用すると最低限のコードでネイティブの機能を利用することができました。
実際にDartから呼んでみる
以下のように書くことができます。
// [あ, ア, い, イ, う, ウ, え, エ, お, オ] にソートされる
['う', 'え', 'あ', 'お', 'い', 'ア', 'エ', 'イ', 'オ', 'ウ'].sort((a, b) {
if (Platform.isIOS) {
return NSString(a).localizedCompare_(NSString(b));
}
if (Platform.isAndroid) {
return collator.compare(a.toJString(), b.toJString());
}
// fallback
return a.compareTo(b);
});
同期的に並び替えができました🙌
※ NSStringのlocalizedCompare
とCollatorのcompare
はひらがな、カタカナの優先順位が同じなので、元の要素の並びによってはひらがな、カタカナの順番が入れ替わることがあります。