Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
三者三様 宣言的UI
Search
Keita Kagurazaka
October 21, 2025
Programming
0
360
三者三様 宣言的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
420
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
910
CQRS Architecture on Android
kkagurazaka
7
3k
suspending functionの裏側
kkagurazaka
3
450
coroutinesで非同期ページネーション
kkagurazaka
1
670
Other Decks in Programming
See All in Programming
Amazon Bedrock Knowledge Bases Hands-on
konny0311
0
120
contribution to astral-sh/uv
shunsock
0
590
オンデバイスAIとXcode
ryodeveloper
0
420
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
580
Webサーバーサイド言語としてのRustについて
kouyuume
1
5.1k
AkarengaLT vol.38
hashimoto_kei
1
140
coconala_slide_pop.pdf
yukihito13
0
260
ドメイン駆動設計のエッセンス
masuda220
PRO
15
7.5k
モテるデスク環境
mozumasu
3
1.4k
Making Angular Apps Smarter with Generative AI: Local and Offline-capable
christianliebel
PRO
0
110
CSC305 Lecture 13
javiergs
PRO
0
390
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
330
Featured
See All Featured
Java REST API Framework Comparison - PWX 2021
mraible
34
9k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
132
19k
Optimising Largest Contentful Paint
csswizardry
37
3.5k
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
Git: the NoSQL Database
bkeepers
PRO
431
66k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
23
1.5k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.7k
VelocityConf: Rendering Performance Case Studies
addyosmani
333
24k
Scaling GitHub
holman
463
140k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
10
920
[RailsConf 2023] Rails as a piece of cake
palkan
57
6k
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
ありがとうございました