Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Flutterで同期的にJava, Objective-Cのネイティブコードを呼び出す(2024年5月版)

はじめに

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は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

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はひらがな、カタカナの優先順位が同じなので、元の要素の並びによってはひらがな、カタカナの順番が入れ替わることがあります。

関連記事

Flutter: RenderObjectWidgetを使いはみ出た分が切り取られるウィジェットを作る

Flutterのテキスト下線位置を調節する方法


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。