SDK 55 の Expo UI:React Native アプリで Jetpack Compose が使えるようになった
SDK 55 の Expo UI は Jetpack Compose を beta に引き上げ、SwiftUI API を Apple の慣習に合わせた。ネイティブフレームワークで身につけた知識をそのまま持ち込める。
日本語
コピー

Expo UI は SwiftUI と Jetpack Compose を React Native アプリにそのまま持ち込む。ネイティブコンポーネントを JavaScript で再実装するのではなく、本物をそのまま公開している——ネイティブ開発者が使うものと同じコンポーネント、同じ挙動、同じプラットフォームの作り込みだ。
SDK 54 で Expo UI を発表し、SwiftUI をサポート、Jetpack Compose は早期 alpha 版を提供した。以降、開発者はこれを使ってアプリを構築しており、私たち自身も expo-widgets の基盤として Expo UI を採用した。実運用に耐えることがこれで裏付けられた。ただし Jetpack Compose サポートは依然 alpha で、アプリ全体でテストしたことはまだなかった。
SDK 55 はこの両方に決着をつける。Jetpack Compose は beta になり、アプリを丸ごと作れるだけの Material Design 3 コンポーネントが揃った。SwiftUI API も再構築して Apple の設計に寄せたので、どちらのフレームワークについても既存の知識がそのまま活きる。
なぜ重要か
Apple と Google は SwiftUI と Jetpack Compose に多大なリソースを注ぎ続けており、最新の API やコンポーネントの多くはまずこの2つのフレームワークに登場する。JavaScript でこれらを再実装するのは膨大な作業で、追いつくのも難しい。
Expo UI は別の道を行く。ネイティブフレームワークをそのまま公開するのだ。プラットフォームが新機能を出せばすぐ使える。しかも API の表面は SwiftUI と Jetpack Compose の慣習に従っているので、これらのフレームワークに馴染んだ開発者(あるいは AI)は新たに学ぶことなく Expo UI のコードを書ける。
Jetpack Compose:alpha から beta へ
Compose API が実用に足るか確かめるため、Expo UI のコンポーネントだけでアプリを1本作り上げた。WikiReader(@nsh07 によるオリジナルのクローン)だ。何が足りないか、どこが不十分かを洗い出すのが狙いだった。

このアプリは全編を通して Material Design 3 のコンポーネントを使っている——Card、LazyColumn、ListItem、DockedSearchBar、ModalBottomSheet などなど。コンポーネントはアプリの実際の必要に応じて1つずつ追加した。アプリ中の現実の問題を解決できるものだけを取り込んでいる。
Expo UI と Jetpack Compose で設定画面がどうなるかはこちら:
import { useState } from "react";
import { useColorScheme } from "react-native";
import { Host, Switch, Icon, ListItem, LazyColumn } from "@expo/ui/jetpack-compose";
import { clip, Shapes } from "@expo/ui/jetpack-compose/modifiers";
export default function SettingsScreen() {
const colorScheme = useColorScheme() ?? "light";
const [wifi, setWifi] = useState(true);
const [bluetooth, setBluetooth] = useState(false);
const [darkMode, setDarkMode] = useState(false);
return (
<Host style={{ flex: 1 }} colorScheme={colorScheme}>
<LazyColumn
verticalArrangement={{ spacedBy: 2 }}
contentPadding={{ start: 16, end: 16, top: 8, bottom: 16 }}
>
<ListItem headline="Wi-Fi" supportingText="Connected" modifiers={[clip(Shapes.RoundedCorner({ topStart: 12, topEnd: 12 }))]}>
<ListItem.Leading>
<Icon source={require("../../assets/icons/wifi.xml")} tintColor="#1d1b20" />
</ListItem.Leading>
<ListItem.Trailing>
<Switch value={wifi} onValueChange={setWifi} />
</ListItem.Trailing>
</ListItem>
<ListItem headline="Bluetooth" supportingText="Off">
<ListItem.Leading>
<Icon source={require("../../assets/icons/bluetooth.xml")} tintColor="#1d1b20" />
</ListItem.Leading>
<ListItem.Trailing>
<Switch value={bluetooth} onValueChange={setBluetooth} />
</ListItem.Trailing>
</ListItem>
<ListItem headline="Dark mode" modifiers={[clip(Shapes.RoundedCorner({ bottomStart: 12, bottomEnd: 12 }))]}>
<ListItem.Leading>
<Icon source={require("../../assets/icons/dark_mode.xml")} tintColor="#1d1b20" />
</ListItem.Leading>
<ListItem.Trailing>
<Switch value={darkMode} onValueChange={setDarkMode} />
</ListItem.Trailing>
</ListItem>
</LazyColumn>
</Host>
);
}
Jetpack Compose を使ったことがあれば、コンポーネント名も構造も見覚えがあるはずだ——arrangement と padding を伴う LazyColumn、modifier チェーン、leading と trailing の内容を持つ ListItem。Compose ではこれらを composable のラムダ引数として渡す。Expo UI では React の複合コンポーネントパターン(ListItem.Leading、ListItem.Trailing)で同じレイアウトを JSX に表現する。
modifier システムも揃えた。Jetpack Compose には独自の modifier チェーンがあり、それを JSX に持ち込んだ。@expo/ui が SwiftUI に対してやったのと同じ流儀で、こうして modifier の挙動が両プラットフォームで統一される:
<Box modifiers={[size(100, 100), background(Color.android.dynamic.primaryContainer)]}>
<Text modifiers={[padding(16)]}>Hello from Compose</Text>
</Box>
スコープ付き modifier も含む。Row と Column にはそれぞれ専用のオプションがあり、たとえば weight や matchParentSize で、Compose と同じだ。modifier の詳細はこちら。
Icon コンポーネントも追加した。Material Symbols の XML vector drawable をネイティブにレンダリングする。Jetpack Compose の開発では Material Symbols を XML drawable として使うのが一般的だ。Material Symbols からアイコンをダウンロードすると Android drawable の XML ファイルが手に入るので、それをそのまま参照すればいい:
<Icon source={require('./path/to/symbol.xml')} />
XML ファイルを1つずつダウンロードするのが面倒なら、AI agent に生成させればいい。私たちのケースではこれがよく効いた——AI が Expo UI のコードとアイコンの XML ファイルをまとめて書いてくれた。
Material Design 3 のコンポーネントを片っ端からラップするつもりはない。WikiReader のクローンに必要なものから追加し、あとは要望に応じて広げていく。コンポーネントの追加に協力してくれた外部コントリビューターに感謝する。
@expo/ui/jetpack-compose のドキュメントで詳しくはこちらから。
SwiftUI:今度こそ本当に SwiftUI らしく
SDK 55 では SwiftUI コンポーネントの名前を変更し、再構築して SwiftUI の対応するコンポーネントと揃えた。DateTimePicker は DatePicker に、Switch は Toggle に、CircularProgress は ProgressView になった。SwiftUI を使ったことがあれば、コンポーネント名も modifier 名も全体の構造も見覚えがあるはずだ。Apple のドキュメントで調べた内容がそのまま Expo UI に対応する。Apple のドキュメントを見ながら直接書けて、翻訳の層を学ぶ必要はない。AI ツールも既存の学習データから正しいコードを生成できる。
すべての SwiftUI ビューとモディファイアを網羅することはできないため、Expo UI は拡張可能な設計になっています。まだ対応していない部分については、独自のカスタム SwiftUI ビューとモディファイアを作成できます。
hot-chocolate サンプルアプリは SDK 55 と最新 API に更新済みです。Expo で SwiftUI を使った構築の完全な例を見たいときにどうぞ。
AI で構築する
ネイティブフレームワークに合わせることの実利的な利点のひとつは、AI との相性が非常に良いことです。
Expo UI は SwiftUI と Jetpack Compose の規約に従っているため、AI ツールはすでにこれらのパターンを深く理解しています。そこで expo/skills にスキルを追加して AI アシスタントに Expo UI の具体的な詳細を把握させ、より緊密な統合のために expo-mcp も構築しました。
ワークフローはこうです。スクリーンショットをアップロードするか、実現したい内容を説明すれば、AI がすでに慣れ親しんだパターンを使って SwiftUI と Jetpack Compose 向けの Expo UI コードを生成します。このプロンプトを今すぐ試せます:
@expo/ui/jetpack-compose コンポーネント(Column、Row、Switch、Slider)で Android の設定風の画面を構築し、行のアイコンには Material Symbols を使い、グループ化されたセクション(ネットワーク、ディスプレイ、サウンド、プライバシー)にスイッチと明るさスライダーを配置する。
次のステップ
私たちは Expo UI のユニバーサルコンポーネントに取り組んでいます。SwiftUI と Jetpack Compose(将来的には web でも)で共通して使える共有コンポーネント API です。
ただ今は、SDK 55 についてのフィードバックをぜひ聞きたいです。Expo UI でアプリをひとつ作ってみてください——@expo/ui/swift-ui でも @expo/ui/jetpack-compose でも——そして感想を教えてください。足りないコンポーネントは何か?しっくりこないところはどこか? いただいた意見は今後の開発方針に直接反映されます。