Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
三者三様 宣言的UI
Search
Keita Kagurazaka
October 21, 2025
Programming
0
410
三者三様 宣言的UI
React / Jetpack Compose / Flutterの3つの宣言的フレームワークの共通点・相違点を実践的なトピックに絞って概説します。
Keita Kagurazaka
October 21, 2025
Tweet
Share
More Decks by Keita Kagurazaka
See All by Keita Kagurazaka
SELECT FOR UPDATEの話
kkagurazaka
0
430
Mobileアプリのアーキテクチャ設計法
kkagurazaka
2
1.4k
原理から完全理解するDagger Hilt Migration
kkagurazaka
1
1.9k
今後のJetpackでAndroid開発はこう変わる!
kkagurazaka
16
6.2k
外部SDKのViewにマスク処理をする方法と罠
kkagurazaka
0
1k
AWAのフルリニューアルを支えたアーキテクチャ
kkagurazaka
1
920
CQRS Architecture on Android
kkagurazaka
7
3k
suspending functionの裏側
kkagurazaka
3
450
coroutinesで非同期ページネーション
kkagurazaka
1
670
Other Decks in Programming
See All in Programming
生成AIを利用するだけでなく、投資できる組織へ
pospome
0
240
How Software Deployment tools have changed in the past 20 years
geshan
0
28k
ソフトウェア設計の課題・原則・実践技法
masuda220
PRO
26
22k
STYLE
koic
0
150
Rediscover the Console - SymfonyCon Amsterdam 2025
chalasr
2
160
まだ間に合う!Claude Code元年をふりかえる
nogu66
3
400
Microservices rules: What good looks like
cer
PRO
0
1.1k
AIコードレビューがチームの"文脈"を 読めるようになるまで
marutaku
0
350
Go コードベースの構成と AI コンテキスト定義
andpad
0
120
WebRTC、 綺麗に見るか滑らかに見るか
sublimer
1
160
sbt 2
xuwei_k
0
260
認証・認可の基本を学ぼう前編
kouyuume
0
190
Featured
See All Featured
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
359
30k
Designing for Performance
lara
610
69k
Raft: Consensus for Rubyists
vanstee
141
7.2k
YesSQL, Process and Tooling at Scale
rocio
174
15k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
The World Runs on Bad Software
bkeepers
PRO
72
12k
Visualization
eitanlees
150
16k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
15k
Art, The Web, and Tiny UX
lynnandtonic
303
21k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
10
720
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
The Invisible Side of Design
smashingmag
302
51k
Transcript
三者三様 宣言的UI React / Compose / Flutter を見比べて Keita Kagurazaka
@ ANDPAD Inc.
今日のテーマ 状態管理と再レンダリング 3つの宣言的UIフレームワークの共通点や相違点を概説 React (Web): 2013年初版公開 Jetpack Compose (Android): 2019年preview版公開
Flutter (iOS/Android): 2017年alpha版公開 2
アプリの状態管理
アプリの2つの状態について アプリの状態とは UIを(再)構築する際に必要となる、あらゆるデータ 宣言的UIは アプリの状態 を引数に UI を出力する関数とみなせる 大きく分けて2種類ある Ephemeral
State Ephemeralは 一時的な という意味 1つのUI要素に閉じた状態 他のUIツリーからは依存されず、独立している App State アプリ内の様々な箇所から参照される状態 画面単位だったり、アプリが立ち上がってる間ずっと保持する状態 https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app 4
Ephemeral Stateの扱い方
Ephemeral Stateの扱い方 各フレームワークにおける代表的な方法 React: useState など Compose: remember x mutableStateOf
Flutter: StatefulWidget 6
Reactの場合 - Hooks React Hooksでシンプルな状態をget/setするパターン function Counter() { const [count,
setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ); } 7
Composeの例 (1) - remember recompositionを超えて MutableState を維持させるため remember を利用 Kotlinのdelegation記法
by を使って、mutableな変数を書き換える形で書ける @Composable fun Counter() { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Count: $count") } } 8
Composeの例 (2) - remember Kotlinの分解宣言を用いて書くこともできる こうするとReactのuseStateとそっくりなことがわかる setterを関数としてそのまま引数に渡すみたいなケースで便利 @Composable fun Counter()
{ val (count, setCount) = remember { mutableStateOf(0) } Button(onClick = { setCount(count + 1) }) { Text("Count: $count") } } 9
Flutterの例 (1) - StatefulWidget class Counter extends StatefulWidget { @override
_CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int count = 0; @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => setState(() { count++; }), child: Text('Count: $count'), ); } } 10
Flutterの例 (2) - flutter_hooks React Hooksライクな書き方ができる外部パッケージ 中身のコードは本質的にStatufulWidgetと等価なので、個人的にオススメ class Counter extends
HookWidget { @override Widget build(BuildContext context) { final count = useState(0); return ElevatedButton( onPressed: () => count.value++, child: Text('Count: ${count.value}'), ); } } 11
Ephemeral Stateの扱い方 - まとめ いずれもReact Hooksスタイルで書ける React: useState など Compose:
remember x mutableStateOf Flutter: useState など by 3rd-party lib 12
暗黙的な状態伝達
暗黙的な状態伝達 UIツリーの部分木で状態を共有する仕組み React: Context API Compose: CompositionLocal Flutter: InheritedWidget 14
Reactの例 - Context API Providerで提供した値を、任意のComponent内で useContext を使って取得 Providerをネストすると直近の祖先の値を優先して使う const ThemeContext
= React.createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Screen /> </ThemeContext.Provider> ); } function Screen() { const theme = useContext(ThemeContext); return <div>Theme: {theme}</div>; } 15
Composeの例 - CompositionLocal Providerで提供した値を、任意のComposable内で .current を使って取得 Providerをネストすると直近の祖先の値を優先して使う val LocalTheme =
compositionLocalOf { LightTheme } @Composable fun App() { CompositionLocalProvider(LocalTheme provides DarkTheme) { Screen() } } @Composable fun Screen() { val theme = LocalTheme.current Text("Theme: $theme") } 16
Flutterの例 (1) - InheritedWidget class ThemeData extends InheritedWidget { final
Color primaryColor; // ThemeDataは、この状態を保持するUIなしWidget final Widget child; const ThemeData({ required this.primaryColor, required this.child, }) : super(child: child); @override bool updateShouldNotify(ThemeData old) => primaryColor != old.primaryColor; static ThemeData of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ThemeData>()!; } } 17
Flutterの例 (2) - InheritedWidget class App extends StatelessWidget { @override
Widget build(BuildContext context) { return ThemeData( // 提供したい値を引数にInheritedWidgetでwrap primaryColor: Colors.blue, child: Screen(), ); } } class Screen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = ThemeData.of(context); // .ofで提供された値を取得 return Text('Theme: ${theme.primaryColor}'); } } 18
暗黙的な状態伝達 - まとめ 書き方にはそれぞれ個性があるが、同じ機能は提供されている ThemeのようにUIツリーのありとあらゆる箇所から参照しつつ、動的に変わりうる ものだけに使うのが一般的 非常にシンプルなアプリの場合、App State管理としても使える UIツリー全体をroot nodeを頂点とする部分木としてみる
19
App Stateの扱い方
App Stateの扱い方 アプリ全体で共有する状態管理 React: Zustand など Compose: ViewModel + StateFlow
Flutter: Riverpod など 21
Reactの例 - Zustand selectorで状態を部分watchできる import create from 'zustand'; const useStore
= create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })) })); function Counter() { const increment = useStore((state) => state.increment); return <button onClick={increment}><CountDisplay /></button>; } function CountDisplay() { const count = useStore((state) => state.count); // 状態を部分的にwatch return <span>Count: {count}</span>; } 22
Composeの例 (1) - ViewModel + StateFlow 状態とその操作を定義 data class CounterUiState(val
count: Int = 0) class CounterViewModel : ViewModel() { private val _uiState = MutableStateFlow(CounterUiState()) val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow() fun increment() { _uiState.update { it.copy(count = it.count + 1) } } } 23
Composeの例 (2) - ViewModel + StateFlow Jetpack Composeにはselectorはなし @Composable fun
Counter(viewModel: CounterViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState() Button(onClick = { viewModel.increment() }) { CountDisplay(count = uiState.count) } } @Composable fun CountDisplay(count: Int) { Text("Count: $count") } 24
Flutterの例 (1) - Riverpod 状態とその操作を定義 class CounterState { final int
count; CounterState(this.count); } final counterProvider = StateNotifierProvider<CounterNotifier, CounterState>( (ref) => CounterNotifier() ); class CounterNotifier extends StateNotifier<CounterState> { CounterNotifier() : super(CounterState(0)); void increment() => state = CounterState(state.count + 1); } 25
Flutterの例 (2) - Riverpod selectorで状態を部分watchできる class Counter extends ConsumerWidget {
@override Widget build(BuildContext context, WidgetRef ref) { return ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: CountDisplay(), ); } } class CountDisplay extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider.select((state) => state.count)); return Text('Count: $count'); } } 26
App Stateの扱い方 - まとめ 採用するライブラリによって千差万別 実現したいこと 状態の定義や操作をUIとは別のクラス・コンポーネントに切り出す パフォーマンスを落とさない 暗黙的な状態伝達も同じライブラリで実現可能なことも ZustandやRiverpodはUIツリーの特定の部分木で状態を上書きできる
大は小を兼ねる そもそも全ての状態をApp Stateとして管理することは理屈としては可能 適宜使い分けると、より保守しやすいコードに 27
再レンダリングの思想
Reactの場合 再レンダリングは制御しよう派 コンポーネント設計を工夫する 状態をなるべくツリー末端に移動 propsでchildren / componentを受け取る useMemo , useCallback
, React.memo などのメモ化を駆使する デフォルトでは、あるコンポーネントを再レンダリングすると、その子孫すべて が再レンダリングされる つい先日、React Compiler v1.0がリリースされたので、今後は不要になりそう 状態管理ライブラリにもパフォーマンスを維持する機能が搭載されがち selectorによる部分watchなど 29
Composeの場合 State Hoistingすればコンパイラがなんとかする派 Composableはなるべくstatelessにし、必要なimmutable stateを引数として受け取る いわゆるバケツリレー推奨の設計 原則として Composableが引数を比較して必要な部分だけ再composition Stable うんぬん話は今回は省略
strong skippingでラムダも自動でメモ化 後発なUIフレームワークなだけあって開発者フレンドリー https://developer.android.com/develop/ui/compose/performance/stability/strongskipping 30
Flutterの場合 再buildが軽量ならば何も考えなくていいよね派 Reactと同様、あるWidgetの再buildは子孫のWidget全てを再buildする const で生成されたWidgetを除く Flutterの開発陣いわく、再buildは十分に高速なので、むやみに避ける必要はない Widgetツリーの再構築とRenderObjectの差分更新が分離しているため 現実問題どうなのかは周りの人に聞いてみよう RiverpodやInheritedWidgetを活用しているチームが多いと思われる 31
まとめ
まとめ 宣言的UIの本質は共通 パラダイムが実現したいことは一緒なので、目的ベースで機能を理解しよう 書き方の差はAIが埋めてくれる時代に感謝 それぞれに独自の思想がある 「なぜこういう書き方になるのか?」を掘っていくと理解が深まる これらを踏まえたうえで、1つ理解すれば他も学びやすい 33
ありがとうございました