Flutter 3.38の隠れた目玉機能「Hooks (Native Assets)」でネイティブ連携が変わる
この記事はFlutter Advent Calendar 2025の12月15日分の記事です。書き上がったので、追加公開します。
2025年11月、Flutter v3.38がリリースされました。すでにFlutter 3.38以降にアップデートしている方も多いと思います。
Flutter 3.38で話題に上がるのは、もっぱらDot Shorthandsです。Dart 3.10以降で利用できるようになった、画期的な記述方法です。しかし、他にも大きな変更が入っていることはご存知でしょうか? それは**Hooks(Native Assets)**の正式サポートです。
Flutter 3.38からは、DartのNative Assets機能がデフォルトで有効になっています。これにより、Flutter 3.38以降をサポートすることを明示すれば、ライブラリでもNative Assetsを利用できるようになりました。公式パッケージであるobjective_cは、v9.2.1からNative Assetsを利用するようになっています。[1]
この記事では、FlutterにおけるHooks(Native Assets)の概要を紹介します。また、筆者がHooksを利用して実装したplatform_image_converterパッケージの事例を通して、Native Assetsの利用方法について説明します。
Hooks(Native Assets)とは
You can currently use hooks to do things such as compile or download native assets (code written in other languages that are compiled into machine code), and then call these assets from the Dart code of a package.
Hooks(Native Assets)は、Dartパッケージにネイティブコードを組み込むための仕組みです。従来、DartやFlutterでネイティブコードを利用するには、FFIやPlatform Channelsを使って手動で連携する必要がありました。しかし、Hooks(Native Assets)を利用することで、Dartパッケージにネイティブコードを直接組み込み、ビルド時に自動的にコンパイルおよびリンクできるようになります。
導入の経緯を紐解くと、Dart 3.2にてexperimentalな機能として導入されました。その後、Dart 3.10で安定版となり、Flutter 3.38からデフォルトで有効化されました。このためか、Hooks(Native Assets)のリリースに関するアナウンスは(まだ)ないようです。最初期にはNative Assetsという名称でしたが、3.10以降はHooksと呼ばれています。記事内では、Flutter文脈におけるhooksとの違いを明確にするため、Hooks(Native Assets)と表記します。
筆者の理解では、大きく3つのメリットがあります。
- CocoaPodsやSwift Package Managerなどが(ケースによっては)不要になる
- ネイティブコードの知識は必要だが、ネイティブコードを書かなくて良くなる
- FFIの導入が容易になる
多くのケースで嬉しいのは、1つ目のケースではないでしょうか? iOSやmacOSのAPIをちょっと利用したいだけなのに、CocoaPodsやSwift Package Managerの設定に苦心していた人も多いはずです。pigeonやMethod Channelの導入と比較した際、最も際立つのはこの点になるでしょう。
2点目は、APIの都合上ネイティブの知識は求められますが、実装を全てDartで完結できる点です。これがメリットではなくデメリットであると感じる人もいるかもしれません。筆者としては、"Objective-CのコードをDartのtry-finallyで囲める"というのは、非常に魅力的なポイントです。詳細は後述します。
3点目は、FFIのメリットが(実は)大きいのに、導入のメリットが薄く見えることに起因します。
Flutter 3.29でAndroidとiOSに、3.35でmacOSとWindowsにてThread mergeが導入されました。ちなみに、Linuxは3.38の次のリリースで導入予定です。[2]
Thread Mergeにより、FFIを利用したネイティブコードの呼び出しが、(処理速度によっては)同期的に実行できるようになります。Hooks(Native Assets)や関連パッケージの整備により、結果的にFFIが導入しやすくなります。今後、ネイティブAPIに関連する処理が、Futureではなくなるケースが増えるかもしれません。
実装事例: platform_image_converter
筆者はHooksを試してみたかったので、platform_image_converterを作成しました。このパッケージは、iOSの実装においてHooksを利用したobjective_cを採用しています。
なお、パッケージの趣旨は画像のフォーマットやサイズを変換するものです。Flutterではimageパッケージが有名ですが、純Dartで実装されています。これに対し、AndroidやiOS、WebのAPIを利用した高速な画像変換を目的としています。画像変換には、通常16ms以上かかるためIsolateで実行することが大半だと思いますが、小さい画像などではメインスレッドで実行しても問題ないかもしれません。その場合、最速の変換を実現できる見込みです。
iOS: objective_c
iOSの実装では、ffiとobjective_cパッケージを利用しています。
objective_cは、DartからObjective-Cのコードを呼び出すためのパッケージです。v9.2.0未満ではbindingコードを.mファイルに記述する必要がありましたが、HooksによりDartパッケージに直接Objective-Cコードを組み込めるようになりました。
最も有名なobjective_cの利用例は、cupertino_httpです。dartチームが開発している、iOSやmacOSのネイティブHTTPクライアントを利用するためのパッケージになります。ただ、この記事を書いている2025年12月14日時点では、Flutterのサポートバージョンの関係でHooksは利用されていません。それ以前の.mファイルを利用した実装となっています。
導入にあたっては、公式ドキュメントを参照しましょう。ffiを定義するためのファイルが、yamlからdartファイルに変わっているなど、アップデートがあります。
platform_image_converterでは、以下のようにObjective-CコードをDartパッケージに直接組み込んでいます。
ライブラリの実装にあたり、追加が必要なclassやfunction、enumなどを列挙します。この列挙した内容に基づき、bindingコードが生成される仕組みです。仕組み上、一度Objective-Cでコードを記述し、それをDartパッケージに転記する形です。[3]
platform_image_converterでは、iOSやmacOSのImage I/Oフレームワークを利用して、画像の変換を実装しています。Image I/Oは、Appleが提供する画像処理用のフレームワークで、多くの画像フォーマットに対応しています。
ただImage I/Oを利用するためには、CGImageやCFDataなど、Core GraphicsやCore Foundationの型も利用する必要があります。これらの型をffigen.dartに追加する必要があります。そして、これらの型は適切にreleaseしないとメモリリークの原因となるため、Dartコードでtry-finallyを利用して解放処理を記述しています。
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
ResizeMode resizeMode = const OriginalResizeMode(),
}) async {
Pointer<Uint8>? inputPtr;
CFDataRef? cfData;
try {
inputPtr = calloc<Uint8>(inputData.length);
inputPtr.asTypedList(inputData.length).setAll(0, inputData);
cfData = CFDataCreate(
kCFAllocatorDefault,
inputPtr.cast(),
inputData.length,
);
if (cfData == nullptr) {
throw const ImageConversionException(
'Failed to create CFData from input data.',
);
}
// 省略
} finally {
if (inputPtr != null) calloc.free(inputPtr);
if (cfData != null) CFRelease(cfData);
}
}
上のコードは、典型的なtry-finallyによるリソース解放の例です。CFDataCreateで生成したCFDataRefを、finallyブロックで確実に解放しています。独特の記述スタイルになりますが、網羅的にリソースを解放できる点は非常に魅力的です。
Dartのコードであるため、ExceptionもDartで記述できます。例えば、次のように変換可能なフォーマットをenumからImage I/OのUTIに変換するコードは次のようになります。サポートしていないフォーマットの場合、DartのUnsupportedErrorをthrowできます。非常に明快です。
final utiStr = switch (format) {
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypejpeg
OutputFormat.jpeg => 'public.jpeg',
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypepng
OutputFormat.png => 'public.png',
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypeheic
OutputFormat.heic => 'public.heic',
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttypewebp
OutputFormat.webp => throw UnsupportedError(
'WebP output format is not supported on iOS/macOS via ImageIO.',
),
};
Android: jnigen
Androidの実装では、jniとjnigenパッケージを利用しています。
jnigenについては、2年前に記事を書いています。バージョンアップによる変更もあるのですが、基本的な利用方法は変わっていません。
最も有名なjnigenの利用例は、これまたcronet_httpです。dartチームが開発している、AndroidにおいてネイティブのHTTPクライアント(Cronet HTTP)を利用するためのパッケージになります。
導入にあたっては、こちらも公式ドキュメントを参照しましょう。というのも、jniの対象となるapkを作成する必要があるため、手順を確認せずに進めると確実に詰まるためです。
platform_image_converterでは、以下のようにJavaコードをDartパッケージに直接組み込んでいます。
Androidの実装経験がある方であれば、列挙されているByteArrayOutputStreamやBitmapFactory、Bitmapなどを見ることで、おおよその処理内容が想像できると思います。
Androidにおいても、Javaのオブジェクトを作成した場合、そのリリースをDartで記述する必要があります。こちらもtry-finallyを利用して、確実に解放処理を記述しています。
Future<Uint8List> convert({
required Uint8List inputData,
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
ResizeMode resizeMode = const OriginalResizeMode(),
}) async {
JByteArray? inputJBytes;
Bitmap? originalBitmap;
try {
inputJBytes = JByteArray.from(inputData);
originalBitmap = BitmapFactory.decodeByteArray(
inputJBytes,
0,
inputData.length,
);
if (originalBitmap == null) {
throw const ImageDecodingException('Invalid image data.');
}
// 省略
} finally {
inputJBytes?.release();
originalBitmap?.recycle();
originalBitmap?.release();
}
}
上のコードは、典型的なtry-finallyによるリソース解放の例です。JByteArrayやBitmapを、finallyブロックで確実に解放しています。iOS/macOSの実装と同様に、網羅的にリソースを解放できる点は非常に魅力的です。またDartは?を利用できるため、null安全に解放処理を記述できる点も嬉しいポイントです。
ネイティブコードをDartで書くメリット
Javaの処理をDartで書くことは、確かに違和感があります。しかしネイティブの処理をDartのasync/awaitやtry-finallyを駆使して記述できる点は、プラットフォームごとの細かな困りごとを解消するパッケージでは非常に有効です。また、Dartで記述することで、Flutterに親しみはあるがネイティブのコードに詳しくない開発者でも、ある程度「何が起きているのか」が推測しやすくなります。パッケージの中身を、利用者が確認できることは重要です。
なにより、FFIを利用するハードルが下がることで、パッケージに頼らず軽微な実装であればアプリケーション側で実装できる可能性が高まります。例えばiOSのPush通知のcountを変更する処理などは、Hooksを利用すれば、ライブラリなしで実現しても良いかもしれません。端末の生体認証を利用するシーンなどでは、アプリケーションの要件に合わせた実装が求められることも多いと思います。そのような時に、Hooks(Native Assets)は強力な選択肢となるでしょう。
Flutterの利点であり弱点であった、"ネイティブのAPIを利用しにくい"という点が、Hooks(Native Assets)により大きく改善されたと言えます。
まとめ
Flutter 3.38から、Hooks(Native Assets)がデフォルトで有効化されました。これにより、Dartパッケージにネイティブコードを組み込み、ビルド時に自動的にコンパイルおよびリンクできるようになります。CocoaPodsやSwift Package Managerの設定が不要になるなど、多くのメリットがあります。
サポートするFlutter SDKのバージョンが厳しいため、まだ広く利用されているとは言えません。しかし、今後Flutterのバージョンが進むにつれて、Hooks(Native Assets)を利用したパッケージが増えていくことが予想されます。また、Hooks(Native Assets)を利用することで、既存パッケージの置き換えが進むかもしれません。
Discussion