Slide 1

Slide 1 text

React Native経験者が Flutterを30⽇間触って驚いたこと ©ASSIGN Inc. All Rights Reserved

Slide 2

Slide 2 text

齋藤 稜太 Ryota Saito 株式会社アサイン デジタル戦略室 / エンジニア • ⼤学卒業後、Web制作会社に⼊社し、主に⼤⼿教育企業や⾦融機関向けの コンテンツ制作にかかわるプロジェクトを経験 • 昨年11⽉にアサインに参画し、今年3⽉にReact Nativeアプリをリリース。 現在はFlutterアプリを開発中 ©ASSIGN Inc. All Rights Reserved 2

Slide 3

Slide 3 text

アジェンダ 1. UI構築 ─ なぜWidgetで包むのか? 2. 状態管理 ─ hooksとRiverpod 3. まとめ ©ASSIGN Inc. All Rights Reserved 3

Slide 4

Slide 4 text

余⽩をつけたいだけなのに、なぜ包む? React Nativeではstyleの1⾏。FlutterではWidgetで包む。 React Native Flutter テキスト Padding( padding: EdgeInsets.all(16), child: Text('テキスト'), ) ©ASSIGN Inc. All Rights Reserved 4

Slide 5

Slide 5 text

Widget は、⾃分の位置を⾃分で決められない 制約は下へ、サイズは上へ、位置は親が決める Flutter 親 Widget 使える範囲を渡す 制約は下へ Padding Textの位置を決める サイズは上へ Padding( padding: EdgeInsets.all(16), child: Text('テキスト'), ) Text 自分のサイズを決める ©ASSIGN Inc. All Rights Reserved 5

Slide 6

Slide 6 text

この⾒⽅は、ほかのWidgetにも通⽤するのか 試してみると、どの Widget もひとつの仕事しかしていなかった DecoratedBox( // 装飾をつける decoration: …, child: Padding( // 余白をとる padding: …, child: Column( // 縦に並べる children: [ const Text('テキスト'), // 文字を出す ElevatedButton(...), // 操作を受け付ける ]))) ©ASSIGN Inc. All Rights Reserved 6

Slide 7

Slide 7 text

ネストの正体は、役割の階層だった 仕事は⼤きく3種類に分けることができる Flutter React Native 構造 Text, Image, ElevatedButton レイアウト Padding, Column, Center Text, Image, Pressable Viewのstyle(padding, flex, backgroundColor…) スタイル DecoratedBox, Opacity, Theme ※ 理解のための対⽐で、1対1の対応ではない ©ASSIGN Inc. All Rights Reserved 7

Slide 8

Slide 8 text

同じことをするのに、クラスを2つ書くの!? React NativeのuseStateは、 1⾏だった Flutter class CounterPage extends StatefulWidget { const CounterPage({super.key}); @override React Native State createState() => _CounterPageState(); } const [count, setCount] = useState(0); class _CounterPageState extends State { int _count = 0; @override setCount(count + 1)} Widget build(BuildContext context) { /> return ElevatedButton( onPressed: () => setState(() => _count++), child: Text('$_count'), ); } } ©ASSIGN Inc. All Rights Reserved 8

Slide 9

Slide 9 text

React NativeのuseStateも値は関数の外にあった 関数も Widget も描画のたびに作り直されるので、 値は外に置くしかない React Native Flutter 関数 描画のたびに実行し直す Widget 再ビルドのたびに作り直す 値 React の内部の オブジェクトに残る 値 State のクラスに残る hooks が値の所在を透過的にしていた ©ASSIGN Inc. All Rights Reserved 9

Slide 10

Slide 10 text

Flutterにもhooksがあった useStateという名前まで同じで、React Nativeの感覚のまま書けた React Native const [count, setCount] = useState(0); setCount(count + 1)} /> ©ASSIGN Inc. All Rights Reserved flutter_hooks final count = useState(0); onPressed: () => count.value++, 10

Slide 11

Slide 11 text

状態と通信を、別の道具で持っていた クライアント側で持つデータか、サーバーから来るデータかで 分けていた React Native // 入力途中のデータ(クライアント側の状態) 2つのライブラリを使⽤ const schoolHistory = useCreateDocumentStore((s) => s.schoolHistory); 1. Zustand ⼊⼒途中のデータを保持 // 保存済みデータの取得(サーバー由来の状態) const { data, isLoading, error } = useQuery({ queryKey: queryKeys.userDocument(userId), queryFn: () => getUserDocument(userId), 2. TanStack Query ⼀覧の取得やAPIへの保存 }); ©ASSIGN Inc. All Rights Reserved 11

Slide 12

Slide 12 text

取得も⼊⼒中の値も、同じref.watchで読める Widgetの外に値を置く仕組みが、ひとつにまとまっていた Flutter / riverpod // クライアント側の状態 final isNewGraduate = ref.watch(isNewGraduateControllerProvider); // サーバー由来の状態 final profileAsync = ref.watch(userProfileControllerProvider); ©ASSIGN Inc. All Rights Reserved 12

Slide 13

Slide 13 text

読み込み中も失敗も、⼀つの値として届く ⾮同期の結果は、AsyncValueという型でUIに届く @riverpod Future userProfile(Ref ref) async { ... } // 画面:3つの状態がひとつの型で届く final profileAsync = ref.watch(userProfileProvider); return switch (profileAsync) { AsyncValue(hasError: true) => _ErrorArea(), // 失敗 AsyncValue(:final value?, hasValue: true) => _ProfileBody(profile: value), // 値 _ => const CircularProgressIndicator(), // 読み込み中 }; ©ASSIGN Inc. All Rights Reserved 13

Slide 14

Slide 14 text

まとめ 別のフレームワークを学ぶと、今までの当たり前の理由が⾒えてくる 1. UI構築 React Nativeではstyleの1か所にまとめていたものが、 Flutterでは役割ごとの Widgetに分かれていた 2. 状態管理 React Nativeでは2つのライブラリで扱っていたものが、 Riverpodでは同じ ref.watchで読め、⾮同期はAsyncValueという型で届いた ©ASSIGN Inc. All Rights Reserved 14