Android - TECH PLAY - TECH PLAY

TECH PLAY

Android

イベント

マガジン

技術ブログ

はじめに この度、株式会社エブリーは、2026年9月11日(金)〜13日(日)に開催される「iOSDC Japan 2026」に、ゴールドスポンサーとして協賛することになりました! 弊社はこれまで Go Conference や TSKaigi などに協賛してきましたが、今年は9月頭の DroidKaigi 2026 に続いて、iOSDC Japan にも初めて協賛します。初めての参加ということで、ブースでどんな方々とお話しできるのか、今から楽しみにしています。 本記事では、開催概要と弊社のブース企画、iOSDCチャレンジのトークン紹介、そしてアフターパーティーのご案内をお届けします。 iOSDC Japan 2026 とは? iosdc.jp iOSDC Japan は、iOS 関連技術をコアテーマとしたソフトウェア技術者のためのカンファレンスです。会場でのトークセッションやスポンサーブースに加えてオンライン配信もあり、iOS エンジニアが年に一度集まる大規模なイベントです。 今年の開催概要は以下のとおりです。 開催日時 Day 0: 2026年9月11日(金) Day 1: 2026年9月12日(土) Day 2: 2026年9月13日(日) 開催場所 有明セントラルタワーホール&カンファレンス 開催形態 オフライン / オンライン(ニコニコ生放送) ブース出展日程 2026年9月11日(金)〜9月13日(日)の3日間 ブース企画 弊社のブースでは、次の2つの企画を用意しています。開発本部のメンバーが現地に参加しますので、ぜひお気軽にお立ち寄りください! アンケートボード「iOSアプリ開発、AIにどこまで任せていますか?」 「iOSアプリ開発、AIにどこまで任せていますか?」をテーマにした参加型のアンケートボードを設置します。 実装からレビュー、テストまで、日々の iOS 開発のどこまでを AI に任せているのか、エンジニア歴とあわせて皆さんのリアルな声をお聞かせください。 集計結果は、イベント後の事後レポート記事で公開する予定です。シールを1枚貼るだけで参加できますので、セッションの合間にぜひ立ち寄ってみてください! 新しくなったデリッシュAI をブースで触れます! また、iOS版でリリースしたばかりのデリッシュキッチンの新しい「デリッシュAI」を、実機でデモ展示します! 新しいデリッシュAI は、チャットで相談するとレシピを提案してくれる機能です。たとえば「子供に人気のレシピ教えて」と聞くと、まず「ハンバーグ / カレー / から揚げ …」と選択肢で聞き返してくれて、「ハンバーグ」を選ぶと、子供向けのハンバーグレシピをすぐに提案してくれます。その下には「もっと簡単」「野菜入り」のような次の絞り込みや、「こんな質問もできます」の候補が並ぶので、文字を打たなくてもタップだけで会話が進みます! 技術的には、サーバー側の AI エージェントが「どの UI をどう並べるか」まで生成し、iOS アプリがそれを SwiftUI で描画する、いわゆる Generative UI の作りになっています。 「LLM に UI を任せてデザインは崩れないの?」「サーバーが返した UI を SwiftUI でどう描画してるの?」と気になった方は、ぜひブースへ! 実機を触りながら、iOS エンジニアが設計の中身までお話しします。 デリッシュAI は最新版のアプリでご利用いただけます。iOSDC までにぜひ一度、「今日の夕飯、何がいいかな」と聞いてみてください! iOSDCチャレンジのトークン紹介 iOSDC Japan では、会場をはじめ様々なところに散りばめられた「iOSDCトークン」を探し、見つけた数に応じて抽選券を獲得できる「iOSDCチャレンジ」が開催されます。集めた抽選券は、会場の抽選カウンターでノベルティが当たる抽選に使えます。 弊社もこの企画に参加しており、トークンは全部で3つ用意しました。トークンは「#」から始まるスペースを含まない文字列です。この記事ではそのうち2つを見出しとして掲載し、残りの1つはブースで公開します。 #AIファースト・カンパニー 弊社は「AI ファースト・カンパニー」を掲げ、AI を前提に組織や仕事の進め方を組み替えている最中です。 Claude の全社導入で個人の AI 活用は一気に進みました。ここからは、そこで生まれたツールやプロンプト、スキルを個人に閉じたままにせず共有資産へ育てる「個人から組織へ」を進めきることと、AI に任せられる範囲が広がるなかで人間の役割を意思決定と設計へ寄せていくことが、次のテーマです。 #誰でも簡単においしく作れる デリッシュキッチンが大切にしているのは、「誰でも簡単においしく作れる」レシピ動画を届けることです。 料理が得意な人だけでなく、料理に苦手意識のある人、忙しい日の夕飯に悩んでいる人、料理を始めたばかりの人にも「今日はこれ作ってみよう」と思ってもらえるように、レシピの選び方から作り方の見せ方まで、サービスの体験全体をこの言葉を軸に作り込んでいます。 残りの1つはブースで 3つ目のトークンは、弊社のブースに掲示しています。 アンケートボードやデリッシュAI の展示を見に来ていただいたついでに、ぜひ探してみてください。 アフターパーティーのご案内 DroidKaigi 2026 の協賛記事でもご案内しましたが、ゆめみ、フェンリル、Yappli、WealthNavi、セーフィー、エブリーの6社合同で「DroidKaigi & iOSDC After Talks Night 2026」を開催します! DroidKaigi 2026 と iOSDC Japan 2026 の合同アフターパーティーですので、iOS エンジニアと Android エンジニアが同じ場に集まり、モバイルエンジニア全体で技術交流や LT などを楽しめる会になる予定です。 開催日時 2026年10月2日(金)19:00〜21:00 開催場所 住友不動産麻布十番ビル アクセンチュア・イノベーション・ハブ 東京 開催形態 オフライン / オンライン コンテンツ ・各社の Android & iOS に関するセッション ・懇親会 詳細・お申し込みは以下からご確認ください。皆さんのご参加をお待ちしています! yumemi.connpass.com おわりに 初めての iOSDC Japan への協賛ということで、チーム一同、当日をとても楽しみにしています。 ブースでは、デリッシュキッチンの iOS 開発の裏側や技術スタック、AI 時代のアプリ開発についてのご質問や雑談も大歓迎です。「トークンを探しに来た」「新しいデリッシュAI の技術的な中身を聞いてみたい」「ちょっとエンジニアと話してみたい」くらいの軽い気持ちで構いませんので、ぜひ弊社のブースに足をお運びください。 有明の会場、そして10月のアフターパーティーで、皆さんとお会いできることを楽しみにしています! 最後までお読みいただき、ありがとうございました!
この記事でわかること Compose Multiplatform(CMP)の動画再生は、Android では AndroidView + Media3 PlayerView 、iOS では UIKitView + AVPlayerLayer 、Web では canvas の裏に置いた <video> 要素と、プラットフォームごとにまったく異なる Interop 機構で動いている 「どのフレームワークがネイティブか」は見せかけの問いで、デコードと撮像の層は Flutter も React Native も CMP もすべて OS の同じネイティブ API を使っている。本当の分岐点は「ネイティブが描いた 1 フレームを、フレームワーク自身の UI とどう合成するか」にある Android の SurfaceView / TextureView を画面ごとに選べる自由は CMP(と React Native)にはあり、Flutter の video_player には長らくなかった。一方、実際のプロジェクトでは角丸プレビューのために、あえて Flutter と同じテクスチャコピー経路( TextureView )を選んだ CameraK 0.4 やコミュニティ製の動画プレイヤーといった「若いエコシステム」の現実と、その不足を expect/actual で各プラットフォームのネイティブ API に直接下りて補うという対処法を、実プロジェクトのコードで示す はじめに こんにちは、モバイルアプリ開発部の Yao です。 私は以前から Kotlin Multiplatform(KMP)/ Compose Multiplatform を継続的に研究していて、このテーマで何本か記事を書いてきました。興味のある方は 過去の記事一覧 もご覧ください。本記事では、 CMP の弱点として真っ先に名前が挙がる「動画再生」と「カメラ」を、Android・iOS・Web の 3 プラットフォームでどう実装したか を書きます。 題材は、社内で開発した 動画アバター生成アプリ です。自分の顔を動画で録画してデジタル分身(アバター)を作り、テキストを入力するとその分身が自分の声で話す動画を生成する、というアプリです。録画・録音・再生というメディア機能がプロダクトの根幹にあり、「CMP でメディアをやるとどうなるか」を検証するには格好の題材でした。 生成された動画を確認・承認する VideoDetailScreen (本記事のアプリ画面はいずれもデザインモックで、人物はすべてダミーです) CMP のエコシステムはまだ若いです。この記事では、その若さがどこでどう痛むのか、そしてその痛みが expect/actual というアーキテクチャでどこまで緩和できるのかを、実際の行数・バージョン番号・コードで示します。形容詞ではなく数字で語ることを目標にします。 このアプリのアーキテクチャはどうなっているか 本アプリは 1 つの Kotlin コードベースから Android / iOS / Web(wasmJs)の 3 プラットフォームに配信しています。モジュール構成は次のとおりです。 androidApp ┐ iosApp ├─→ sharedUI ─→ sharedLogic ─→ apiClient webApp ┘ モジュール Kotlin ファイル数 役割 sharedLogic 113 Compose 依存ゼロ の純 Kotlin。Ktor / Serialization / Coroutines / Koin-core sharedUI 154 モバイル向け共有 CMP 画面 + 3 プラットフォームの actual 実装 webApp 35 モバイルのレイアウトは再利用せず、デスクトップ向けに再構築 apiClient 191 OpenAPI Generator 7.22.0 で生成しリポジトリにコミットした自社バックエンドのクライアント androidApp / iosApp 6 / 4(Swift) エントリポイントのみ 設計は Clean Architecture + MVI です。UseCase が 32 個(1 クラス 1 操作)、Repository が 7 個、ViewModel が 12 個あり、各 ViewModel は MutableStateFlow<XxxUiState> と不変 data class による単一の状態ソース(Single Source of Truth)を持ちます。プラットフォーム能力は 20 個の expect 宣言 に集約しています。 技術スタックはかなり新しめです。Kotlin 2.3.10 / CMP 1.10.1 / AGP 9.0.1 / Ktor 3.4.0 / Koin 4.1.1 / Navigation 3 1.0.0-alpha06 / Media3 1.9.0 / Coil3 3.3.0 / CameraK 0.4 / FileKit 0.13.0、そして wasmJs ターゲット。エコシステムの最前線に立つと何が起きるかは、後半で具体的に書きます。 プロジェクトの特徴を 1 つだけ挙げると、 ロジックは 100% 共有、UI は形態ごとに分岐 という方針です。Web はモバイルのレイアウトを再利用せず 33 個の独立した Web*Screen をデスクトップ向けに構築していますが、ViewModel の状態機械はモバイルとまったく同じものを使います。この「ロジックとプレゼンテーションの分離」が、後述するメディア実装の 3 プラットフォーム分岐を支える土台になっています。 なぜ Compose Multiplatform を選んだのか Flutter や React Native ではなく CMP を選んだ理由は 3 つあります。 1 つ目は、ロジックの複雑さが UI ではなく状態機械にあったこと。 本アプリのビジネスロジックには、本人確認(consent)のブラウザ連携の状態遷移、アバター生成状態の 3 値導出( status × generationRetryable × retryCount → GENERATING / RETRYABLE / TERMINAL )、動画生成とアバター訓練それぞれの 60 秒間隔ポーリングといった、型で守りたい遷移が多くあります。この種のロジックは、sealed interface と data class を持つ Kotlin で書くのが Dart や TypeScript より安全だと判断しました。 2 つ目は、チームのスキルセット。 メンバーは Kotlin / Android 出身で、UseCase 32 個 + StateFlow の MVI という構成は学習コストゼロで書き始められました。 3 つ目が最も重要で、「漸進的にネイティブへ戻れる」こと。 sharedLogic は Compose 依存ゼロの純 Kotlin なので、 Swift から直接呼べます 。実際 iOS はすでに SwiftUI のネイティブシェル + CMP コンテンツ画面というハイブリッド構成です。将来 UI をすべて SwiftUI / Jetpack Compose に置き換えることになっても、ロジック側は 1 行も変わりません。Flutter や React Native では、UI フレームワークを捨てる = ほぼ全部書き直しですが、KMP ではロジック資産がそのまま残ります。この退路の存在が、以前の記事 「Kotlin Multiplatform ハイブリッドモード」 で書いたハイブリッドモードの実利です。 「動画再生」と「カメラ」——若いエコシステムで最初にぶつかる壁 CMP でアプリを作ると宣言したとき、最初に聞かれるのが「動画とカメラはどうするの?」です。もっともな質問です。Flutter には flutter.dev 公式チームが保守する video_player と camera があり、React Native には無数のプロダクションで数年動いている react-native-video と vision-camera があります。CMP にはそれに相当する「公式の定番」がまだありません。 エコシステムの現実を数字で見る 本アプリの動画再生は io.github.kdroidfilter.composemediaplayer (ComposeMediaPlayer)というコミュニティ製ライブラリをベースにしています。Flutter の video_player のような公式チーム保守のプラグインではないので、挙動の細部を確かめたいときはライブラリの公開ソースを読み解くことになります。この記事の Interop 解説も、そのソースリーディングの成果です。 カメラは CameraK を使いました。バージョンは 0.4 です。Flutter の camera が公式チームの保守下にあり、 vision-camera が成熟していることと比べると、この数字がエコシステムの若さを端的に表しています。 それでもこの構成で 3 プラットフォームのアプリは出荷できました。なぜ成立したのかを説明するために、まず前提となる誤解を 1 つ解きます。 そもそも「ネイティブかどうか」は論点ではない クロスプラットフォームの比較記事では「Flutter は独自レンダリングだから」「RN はネイティブだから」という議論をよく見ますが、動画とカメラに関しては、 デコードと撮像の層は 3 者とも完全に同じ です。自前で H.264 デコーダやカメラドライバを書いているフレームワークは 1 つもありません。 フレームワーク / ライブラリ 再生のデコード カメラの撮像 Flutter: video_player / camera (いずれも flutter.dev 公式プラグイン) ExoPlayer / AVPlayer CameraX・Camera2 / AVCaptureSession React Native: react-native-video / vision-camera ExoPlayer / AVPlayer CameraX / AVCaptureSession 本アプリ(CMP) Media3 ExoPlayer / AVPlayer / <video> CameraK 経由の CameraX / AVCaptureSession、Web は getUserMedia を直接 どのフレームワークを選んでも、動画をデコードするのは ExoPlayer と AVPlayer で、カメラを制御するのは CameraX と AVCaptureSession です。つまり「画質」「デコード性能」「対応コーデック」でフレームワーク間に差は出ません。 では何が違うのか。 ネイティブ API がデコードした 1 フレームを、フレームワーク自身が描いた UI(ボタン、テキスト、角丸カード)とどう合成するか 。ここがすべての分岐点です。 本当の分岐点:ネイティブのフレームをどう合成するか 合成戦略は大きく 2 路線に分かれます。 路線 A:テクスチャ搬送。 ネイティブプレイヤーの出力を GPU テクスチャとしてフレームワーク側にコピーし、フレームワークが自分のシーングラフの中で 1 枚の画像として描く。Flutter の video_player は従来この方式が既定でした。 路線 B:ネイティブ view の埋め込み。 ネイティブの view( SurfaceView / UIView / DOM 要素)をそのまま画面に重ね、フレームワークの描画面と OS のコンポジタで合成する。React Native と、CMP の Interop( AndroidView / UIKitView )はこちらです。 路線 A は毎フレームのコピーと引き換えに自由な変換を、路線 B はゼロコピー直結と引き換えに手動のレイヤー管理を受け入れる 両者の得失は明確に非対称です。 観点 路線 A:テクスチャ搬送(Flutter) 路線 B:ネイティブ view 埋め込み(RN / CMP) 角丸・クリップ・回転・拡縮 ✅ 動画はただの画像なので自由に変換できる ⚠️ ネイティブ層は親の clip を受けない( SurfaceView の場合。 TextureView なら可) 他 UI との z 順の重なり ✅ 常に正しい ⚠️ 手動管理。Web ではほぼ不可能 リスト内スクロール ✅ フレーム同期が自然に取れる ⚠️ ネイティブ層がずれて「浮く」ことがある GPU 負荷 / 遅延 ⚠️ コピーと同期で約 1 フレーム分のコスト ✅ ゼロコピーでハードウェア合成に直結( SurfaceView の場合) PiP(iOS)/ AirPlay / ロック画面操作 / ネイティブ字幕 ⚠️ 失われる。追加プラグインが必要 ✅ ネイティブプレイヤーの機能がそのまま使える DRM のセキュアパス(Widevine L1) ❌ テクスチャ経路では取得できない ✅ 可能( SurfaceView が必要) この表を頭に入れたうえで、CMP が 3 プラットフォームでそれぞれどう実装しているかを見ていきます。本アプリでは ComposeMediaPlayer の上に 272 行のラッパー VideoPlayer.kt を被せ、成果物動画の再生( VideoDetailScreen )、アバターのプレビュー再生( AvatarDetailScreen )、録画素材の見直し( AvatarCreateVideoReviewScreen )とその Web 版、計 6 画面で使っています。 3 つのプラットフォーム、3 つの Interop Android:AndroidView と、SurfaceView / TextureView の選択権 Android 実装は AndroidView で Media3 の PlayerView を Compose ツリーに埋め込みます。興味深いのは、このライブラリが 動画を描く面(surface)を 2 種類用意していて、XML レイアウトで切り替えられる ことです。 <androidx.media3.ui.PlayerView android:layout_width="match_parent" android:layout_height="match_parent" app:surface_type="surface_view" app:use_controller="false" /> <androidx.media3.ui.PlayerView android:layout_width="match_parent" android:layout_height="match_parent" app:surface_type="texture_view" app:use_controller="false" /> SurfaceView は OS のハードウェアコンポジタに直結した独立レイヤーで、ゼロコピーかつ低遅延、DRM のセキュアパスもここを通ります。ただし 独立レイヤーであるがゆえに、親 View のクリップに参加しません 。一方 TextureView は動画フレームを GPU テクスチャとして View ヒエラルキーの描画に合流させるため、1 コピー分のコストと引き換えに、普通の View と同じように角丸クリップや変換が効きます。 本アプリのデフォルトは TextureView です。 @Composable actual fun VideoPlayerSurface( playerState: VideoPlayerState, modifier: Modifier, contentScale: ContentScale, overlay: @Composable () -> Unit ) { VideoPlayerSurfaceInternal( playerState = playerState, modifier = modifier, contentScale = contentScale, overlay = overlay, surfaceType = SurfaceType.TextureView ) } 理由は具体的です。アバター詳細画面( AvatarDetailScreen )のプレビューは 350×600 の角丸カードで、 SurfaceView を使うと角丸の内側から四角い動画がはみ出して、カードの角を突き破ります。デザインを守るには TextureView しかありませんでした。 問題の角丸プレビューカード。この 4 つの角を守るために TextureView を選んだ :::message ここに直感に反する事実があります。前節の表のとおり、 TextureView の「GPU テクスチャとしてコピーする」経路は、 まさに Flutter の video_player が従来常に通ってきた路線 A そのもの です。つまり本アプリは Android では、角丸というデザイン要件のために、Flutter と同じ性能特性を自ら受け入れています。 ::: ただし Flutter との違いは「選べること」にあります。 video_player も現在は VideoViewType.platformView で platform view を選べますが、この選択肢が入ったのは登場から長い年月が経ってからでした。CMP の路線 B では、 surfaceType 引数 1 つで 画面ごとに SurfaceView (性能・DRM 優先)と TextureView (クリップ・変換優先)を選び分けられます。角丸カードには TextureView 、全画面再生には SurfaceView 、という使い分けが同じアプリの中でできます。この選択権こそが路線 B の実利で、本アプリの角丸プレビューはそれを行使した実例です。 iOS:UIKitView と layerClass = AVPlayerLayer iOS 実装は 3 プラットフォームの中で最も素直です。CMP の UIKitView でネイティブ UIView を埋め込みますが、その UIView の backing layer 自体を AVPlayerLayer に差し替える という UIKit の古典的なテクニックを使っています。 private class PlayerUIView : UIView { companion object : UIViewMeta() { override fun layerClass(): ObjCClass = AVPlayerLayer } @OverrideInit constructor(frame: CValue<CGRect>) : super(frame) var player: AVPlayer? get() = (layer as? AVPlayerLayer)?.player set(value) { (layer as? AVPlayerLayer)?.player = value } } Objective-C の +layerClass オーバーライドを Kotlin/Native の UIViewMeta でそのまま書けている点に注目してください。view の layer が最初から AVPlayerLayer なので、余計なサブレイヤー管理が不要で、AVPlayer のデコード結果は Core Animation のコンポジタにゼロコピーで渡ります。あとは UIKitView(factory = { PlayerUIView(frame = cValue<CGRect>()) }, ...) で Compose に埋め込むだけです。 きれいな構図ですが、路線 B の代償はここにもあります。CMP の UIKitView はネイティブ view を Compose のシーングラフの中に描くのではなく、 Compose が描画している MTKView の兄弟としてその上に重ねます 。つまり動画 view は Compose の描画世界の外にいて、タッチイベントの透過、レイヤーの前後関係、クリップはすべて Interop の境界で個別に面倒を見ることになります。ComposeMediaPlayer がこの境界を吸収するために、再生コントロールなどを載せるための overlay スロットを API として用意しているのは、その裏返しです。 Web:canvas の裏の video 要素と、BlendMode.Clear で開ける穴 Web(wasmJs)実装は、この記事でいちばん劇的な部分です。CMP の Web ターゲットは画面全体を 1 枚の canvas(Skia)に描画します。では <video> 要素はどこに置くのか。答えは「 canvas の裏 」です。 internal fun HTMLVideoElement.applyInteropBehindCanvas() { val wrapper = parentElement as? HTMLElement ?: return wrapper.style.apply { zIndex = "-1" // <video> を Compose の canvas の背後へ setProperty("pointer-events", "none") backgroundColor = "transparent" } } z-index: -1 で動画をページの最背面に沈めます。当然このままでは canvas に隠れて見えません。そこでアプリ側のコードで、Compose の canvas に 文字どおり穴を開けます 。 // BlendMode.Clear zeroes every pixel (including alpha) in the Compose canvas // over this area, punching a transparent hole so the video element behind // the canvas shows through. Box(modifier = Modifier.fillMaxSize().drawBehind { drawRect(color = Color.Transparent, size = size, blendMode = BlendMode.Clear) }) BlendMode.Clear はその領域のピクセルをアルファ値ごとゼロにするので、canvas のその部分が完全に透明になり、背後の <video> が透けて見える、という仕掛けです。カメラのライブプレビューも同じ機構で、 getUserMedia() で取得した MediaStream を WebElementView 経由で HTMLVideoElement に接続し、canvas の穴から覗かせています。 動画は常に最背面。見えるのは canvas に開けた穴の領域だけで、UI は必ず動画より上に載る :::message これは 2 つ目の、直感に反する事実につながります。この方式の帰結として、 Web では動画は構造的に、すべての Compose コンテンツより下にしか存在できません 。「動画の上に UI を重ねる」ことは(canvas 側に描く限り)可能ですが、「UI の下に動画、そのさらに下に別の UI」のような自由な z 順は原理的に組めません。録画中のタイマー表示や停止ボタンは、この制約を前提に「穴の上に Compose で描く」設計になっています。これはバグや実装の手抜きではなく、canvas レンダリング + DOM 動画という構成の構造的な限界です。 ::: 公平を期すために書いておくと、 Web の動画は 3 フレームワークとも体面を保てていません 。Flutter Web の video_player も HtmlElementView で DOM の <video> を重ねる同類の方式で、canvas と DOM の合成に相応の制約とコストを抱えます。React Native には公式の動画プレイヤーがそもそもなく、 react-native-video の Web 対応もまだ発展途上です。ブラウザでは動画デコードとページ合成の主導権がブラウザ自身にあるため、どのフレームワークも <video> 要素と「同居」する方法を探すしかないのです。 カメラと録音:expect/actual が第一級市民である強み 動画再生はコミュニティ製ライブラリで戦いましたが、カメラと録音はもっと直接的に、 expect/actual で各プラットフォームのネイティブ API に下りて実装しました。まず実装量の実態を見てください。 能力 Android iOS Web 動画録画画面 AvatarCreateVideoRecordingScreen.mobile.kt 467 行 (Android / iOS 共有) 同左 AvatarCreateVideoRecordingScreen.wasmJs.kt 425 行 カメラ実装 CameraK 0.4(CameraX ベース) CameraK 0.4(AVCaptureSession ベース) getUserMedia() + MediaRecorder + WebElementView AudioRecorder 83 行( MediaRecorder ) 108 行( AVAudioRecorder ) 11 行の stub MicrophoneDeviceProvider 70 行 17 行 12 行の stub AudioPlayer 53 行 63 行 57 行( HTMLAudioElement ) モバイル側の録画は CameraK に任せました。フロントカメラ固定・HD 画質・録画プラグインという構成が、Android / iOS 共通の 1 ファイルで書けます。 val cameraConfig = remember { CameraConfiguration( flashMode = FlashMode.OFF, cameraLens = CameraLens.FRONT, ) } val cameraState = rememberCameraKState( config = cameraConfig, setupPlugins = { holder -> holder.attachPlugin(plugin) }, // VideoRecorderPlugin ) CameraKScreen( modifier = Modifier.fillMaxSize(), cameraState = cameraState.value, content = { _ -> RecordingOverlay(/* 録画 UI */) }, ) 録画中の画面。カメラプレビュー(ネイティブ層)の上に、タイマー・スクリプト・停止ボタンを Compose の overlay として重ねている Web はライブラリなしで、 getUserMedia() と MediaRecorder を wasmJs から直接呼びます。mimeType のネゴシエーション(ブラウザごとに対応コーデックが違う)もブラウザ API の流儀のまま書きました。 表の中の「11 行の stub」は隠さず書いておきます。Web 版の AudioRecorder と MicrophoneDeviceProvider は中身のない空実装で、音声クローン用の録音フローはモバイル専用と割り切りました。3 プラットフォーム対応とは「全機能 × 全プラットフォーム」ではなく、プラットフォームごとに提供範囲を選ぶことでもあります。 expect/actual はこの割り切りも型として明示できます。 同じ状態機械、違うトリガー:ConsentReturnFromBrowserEffect expect/actual の使いどころとして、私が最も気に入っている例を挙げます。アバター訓練には本人確認(consent)があり、外部ブラウザで確認を完了してからアプリに戻ると、アプリ側が「戻ってきた」ことを検知してステータスの確認を始めます。この「外部ブラウザから戻ってきた」というイベントは、プラットフォームによって観測手段がまったく違います。 @Composable expect fun ConsentReturnFromBrowserEffect(onReturned: () -> Unit) Android / iOS (mobileMain・36 行):Compose の lifecycle を観測し、 ON_PAUSE → ON_RESUME の順で来たときだけ発火。ブラウザを開けば必ずアプリが pause するので、初回 composition や無関係な resume を誤検知しません Web (wasmJs・36 行): window の blur → focus を監視。wasmJs では lifecycle の ON_RESUME が確実には発火しないため、専用の経路を用意しました consent の状態機械そのもの(待機 → 確認中 → 完了)は ViewModel にあり、3 プラットフォームで完全に同一です。 違うのはトリガーの観測方法だけで、そこだけが actual として差し替わる 。状態機械のテスト・修正・拡張は 1 か所で済みます。 :::message ここが 3 つ目の、そしてこの記事で最も伝えたい、直感に反する洞察です。「CMP はエコシステムが若く、公式プラグインがない」という弱点は事実ですが、そのダメージは KMP のアーキテクチャによって 構造的にかなりの部分まで相殺されています 。Flutter で MethodChannel や PlatformView を書くのは「プラグインがないときの例外的な脱出路」という位置づけですが、KMP の actual は日常の文法そのものです。同じ AudioRecorder クラスを Android 83 行 / iOS 108 行 / Web 11 行で書き分ける作業は、型安全で、IDE 補完が効き、ビジネスコードと同じモジュールの中で完結します。「ライブラリがなければネイティブ API を直接呼べばいい」が、儀式なしで成立するのです。 ::: Flutter / React Native と比べたときの CMP の強みは何か 先に明確にしておきます。 「CMP の動画プレイヤーやカメラは Flutter より優れている」という主張は成立しません。 成熟度では公式保守の video_player / camera や実績豊富な vision-camera に及びません。そのうえで、このプロジェクトを通して実感した CMP 固有の強みは次の 3 点です。 1. Interop 路線の選択権がある。 CMP は路線 B(ネイティブ view 埋め込み)ですが、Android では SurfaceView / TextureView を画面単位で選べます。角丸が要る画面は TextureView 、性能や DRM が要る画面は SurfaceView 。Flutter の video_player には、このスイッチに相当する選択肢が長らく存在しませんでした。 AvatarDetailScreen の角丸プレビューカードは、この選択権の行使例です。 2. 「ネイティブに下りる」ことが第一級市民である。 前節のとおり、 expect/actual は例外処理ではなく日常の文法です。エコシステムに欠けている部品は、待つのではなく 83 行書けば埋まります。 3. ネイティブ API サーフェス全体に直接触れられる。 本アプリが使っているのは、Media3 PlayerView の app:surface_type 、UIView の layerClass 差し替え、 MediaRecorder の mimeType ネゴシエーションといった、各プラットフォームの 素の API です。プラグイン作者が切り出した「最大公約数的な抽象」を経由しないので、プラグインが公開していない機能のために上流(upstream)のリリースを待ったり PR を出したりする必要がありません。 引き換えに何を受け入れたか 強みだけ書いて終わる記事は信用できないので、CMP を選んだ引き換えに受け入れたデメリットも正直に書きます。 動画再生をコミュニティ製ライブラリ 1 本に依存している。 公式チーム保守という後ろ盾がなく、挙動の細部を確かめたり問題を切り分けたりするたびに、ライブラリ内部の Interop 実装まで読み解く理解コストがかかる カメラライブラリのバージョンは 0.4。 公式チームが保守する Flutter camera や、長年プロダクションで検証されてきた vision-camera と比べ、実績の厚みが違う Web の録音まわりは stub。 AudioRecorder 11 行、 MicrophoneDeviceProvider 12 行の空実装で、録音フローはモバイル専用に割り切った Web の動画は構造的に最背面。 BlendMode.Clear の穴あけ方式である限り、動画と Compose UI の z 順は「動画が常に下」で固定される これらを踏まえた私の判断はこうです。 プロダクトの重心がメディア能力そのものにあるなら、 vision-camera を持つ React Native がおそらく最も成熟した実用的な選択肢でしょう。 一方、重心が複雑なビジネス状態機械にあり、長期的にネイティブ UI へ回帰する可能性を残したいなら、CMP は正しい選択です。メディア層で余分に書いたコードは、「 sharedLogic を Swift から直接呼べる」「いつでもネイティブ UI に戻れる」という自由への入場料だったと総括しています。 まとめ 動画・カメラのデコードと撮像の層は、Flutter / React Native / CMP のどれを選んでも同じネイティブ API に行き着く。差が出るのは「ネイティブのフレームと自前 UI の合成方法」で、テクスチャ搬送(路線 A)とネイティブ view 埋め込み(路線 B)は得失が非対称 CMP は路線 B に立ちつつ、Android では SurfaceView / TextureView を画面ごとに選べる。本アプリは角丸プレビューのために、あえて Flutter と同じテクスチャ経路( TextureView )をデフォルトにした iOS は UIKitView + layerClass = AVPlayerLayer でゼロコピー再生。Web は canvas 背面の <video> に BlendMode.Clear で穴を開けて見せる方式で、動画が常に UI の下という構造的制約を受け入れた エコシステムの若さ(コミュニティ製の動画プレイヤー、CameraK 0.4、Web 録音の stub)は実在するコストである。同時に、 expect/actual によって「ネイティブ API に直接下りる」ことが日常の文法になっているため、そのダメージはアーキテクチャで大きく緩和できる CMP のメディアエコシステムは、あと数年は「自分の手を動かして埋める」フェーズが続くと思います。それでも、埋めるための道具立て—— AndroidView / UIKitView / WebElementView 、そして expect/actual ——は既に揃っていて、実プロダクトを 3 プラットフォームに出荷できる水準にあります。この記事の数字とコードが、同じ判断を迫られている方の材料になれば幸いです。 最後まで読んでいただき、ありがとうございました!
こんにちは、ネットコムブログはかなり久しぶりになります、クラウド部の西村蝶海です! 普段はモバイルアプリ開発をしています。 Androidエンジニアのためのコミュニティイベント DroidKaigi 2026の2日目にあなたのANRはどこから? — 発生する仕組みを診断し、症状別に処方する というセッションで登壇しました! セッション内容は、主要なANR(Application Not Responding、アプリが固まること)が引き起こされる原因を紐解き、その調査方法や修正方針を解説することで、ANRを減らすための勘所を持ち帰ってもらおう!というものです。詳細は公式ページからどうぞ。 登壇の…

動画

書籍