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
[FlutterKaigi2024]ステートマシンで実現する高品質なFlutterアプリ開発
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
teamLab
PRO
November 20, 2024
1.9k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
[FlutterKaigi2024]ステートマシンで実現する高品質なFlutterアプリ開発
teamLab
PRO
November 20, 2024
More Decks by teamLab
See All by teamLab
「ボタンだけどリンクにしたい…」をスマートに解決するPolymorphicコンポーネントの話
teamlab
PRO
1
32
border-radiusだけじゃ足りない: Squircleがつくる自然なUI
teamlab
PRO
0
23
僕はただドキュメントを HTML で見たいだけなんだ
teamlab
PRO
0
25
アクセシビリティに配慮したアニメーション
teamlab
PRO
0
22
TSKaigi 2026 - 10秒のビルドを1秒へ:tsdownが切り拓く2026年のTypeScriptライブラリ開発
teamlab
PRO
2
340
TSKaigi 2026 - enumよ、さようなら
teamlab
PRO
3
660
TSKaigi 2026 - 型プラグインシステムの実装に使われるテクニック
teamlab
PRO
2
490
TSKaigi 2026 - Auth.jsからBetter Authへの 移行に見る「型とランタイム」の 設計思想の変化
teamlab
PRO
1
300
TSKaigi Hokuriku - TypeScriptによる静的データガバナンス
teamlab
PRO
0
160
Featured
See All Featured
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.7k
How to Think Like a Performance Engineer
csswizardry
28
2.7k
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
62
54k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Making Projects Easy
brettharned
120
6.7k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
How Software Deployment tools have changed in the past 20 years
geshan
0
34k
It's Worth the Effort
3n
188
29k
Unsuck your backbone
ammeep
672
58k
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
240
4 Signs Your Business is Dying
shpigford
187
22k
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Transcript
ステートマシンで実現する 高品質なFlutterアプリ開発 チームラボ株式会社 Smartphone Team Engineer そた
そた • 23卒高専出身 • FlutterとKMPを書いてます • カメラにハマってます • Flutter Kaigi運営
自己紹介 𝕏: @_sotaatos
会社紹介 • 会社名: チームラボ株式会社 • 主な事業内容 ◦ アート ◦ ソリューション
実績 • りそなグループアプリ • ネスカフェ ドルチェ グストアプリ • スミセイ・デジタルコンシェルジュ •
三井ショッピングパークアプリ • それ以外にも多数!
こんなことに遭遇したことありませんか?
APIとの通信が完了したのに ローディングの表示が消えない
異常系の実装が漏れていて表示が崩れた
ある程度アプリ開発の経験がある方なら 一度や二度じゃないはずです。
このようなバグを減らし高品質なアプリを 開発するにはどうしたら良いでしょうか?
優れた状態管理の設計
BLoC Hooks Riverpod setState Provider GetX ChangeNotifier Signals Redux
弊社がメインで使用しているのは このどれでもありません
ステートマシン
ステートマシンとは
ステートマシンとは • オートマトン(状態機械) • 計算理論における概念 • 有限オートマトン ◦ 有限個の状態と遷移と動作の組み合わせからなる数学的に 抽象化された「ふるまいのモデル」である (Wikipedia)
• 100円と50円のみ使用可能 • 途中で返金はできない • 150円を超えたら自動でジュースが出てくる ステートマシンとは
ステートマシンとは
アプリの持つ状態も ステートマシンで表せる
APIを叩いて情報を表示する画面
RiverpodのAsyncValue
@riverpod Future<Configuration> configurations(Ref ref) async { final uri = Uri.parse('configs.json');
final rawJson = await File.fromUri(uri).readAsString(); return Configuration.fromJson(json.decode(rawJson)); } final configs = ref.watch(configurationsProvider); return switch (configs) { AsyncData(:final value) => Text('data: ${value.host}'), AsyncError(:final error) => Text('error: $error'), AsyncLoading() => const CircularProgressIndicator(), }; RiverpodのAsyncValue
RiverpodのAsyncValueの構造
RiverpodのAsyncValueも ステートマシン!
Riverpodを使いましょう! ご清聴ありがとうございました!
…とはならないです!
独自にステートマシンを設計することに どのような優位性が?
ページングのある画面の処理 • 画面表示時に一度APIを叩く • APIはコンテンツと共に追加読み込み可能かどうかを返却 • 追加読み込み可能の際は下までスクロールしたら追加読み込み • 追加読み込み不可の場合はそこでAPIの呼び出しは終了
ページングのある画面のステートマシン
ステートマシンを独自に設計することで 複雑なビジネスロジックの表現が可能!
ここで一旦本題に戻りましょう
私たちが追い求めていたもの
高品質なアプリを開発するための 優れた状態管理の設計
ステートマシンはどのような点が 優れている?
ステートマシンは 設計と実装にメリットをもたらし 保守性を向上させます
設計と実装におけるメリット
画面の取りうる状態を 実装前に全て考えられる
ロード 中 コンテンツ 表示 エラー 表示
設計をそのまま実装に 落とし込むことが可能
不要なnullableを排除
class SamplePageState { SamplePageState({ required this.isLoading, required this.data, required this.errorMessage,
}); final bool isLoading; final String? data; final String? errorMessage; } 不要なnullableを排除
sealed class SamplePageState {} class LoadingState extends SamplePageState {} class
LoadedState extends SamplePageState { LoadedState(this.data); final String data; } class ErrorState extends SamplePageState { ErrorState(this.errorMessage); final String errorMessage; } 不要なnullableを排除
想定していない状態が起こらない
class SamplePageState { SamplePageState({ required this.isLoading, required this.data, required this.errorMessage,
}); final bool isLoading; final String? data; final String? errorMessage; } 想定していない状態が起こらない final state1 = SamplePageState( isLoading: false, data: null, errorMessage: null, ); final state2 = SamplePageState( isLoading: true, data: 'data', errorMessage: 'error', );
final loadingState = LoadingState(); final loadedState = LoadedState('data'); final errorState
= ErrorState('error'); 想定していない状態が起こらない
保守性の向上
実装から設計が読み取りやすい
網羅的なテストの記述が簡単
final loadingState = LoadingState(); final loadedState = LoadedState('data'); final errorState
= ErrorState('error'); 網羅的なテストの記述が簡単
拡張が容易
拡張が容易
拡張が容易
ステートマシンを用いた状態管理には 多くの利点
Dartで実装してみましょう
APIを叩いて情報を表示する画面
sealed class SampleState {} class InitialState extends SampleState {} class
LoadingState extends SampleState {} class LoadedState extends SampleState { LoadedState({required this.data}); final String data; } class ErrorState extends SampleState { ErrorState({required this.message}); final String message; } Stateの定義
@freezed sealed class SampleState with _$SampleState { const factory SampleState.initial()
= InitialState; const factory SampleState.loading() = LoadingState; const factory SampleState.loaded({required String data}) = LoadedState; const factory SampleState.error({required String message}) = ErrorState; } Stateの定義
@freezed sealed class SampleAction with _$SampleAction { const factory SampleAction.fetch()
= FetchAction; const factory SampleAction.success({required String newData}) = SuccessAction; const factory SampleAction.fail({required String newMessage}) = FailAction; } Actionの定義
class StateMachine { StateMachine(this.api); final ApiClient api; SampleState state =
const SampleState.initial(); void dispatch(SampleAction action){} Future<void> _load() async {} } StateMachineの定義
Future<void> _load() async { try { final data = await
api.getData(); dispatch(SuccessAction(newData: data)); } catch (e) { dispatch(FailAction(newMessage: e.toString())); } } _loadメソッドの実装
void dispatch(SampleAction action) { state = switch (state) { InitialState()
=> switch (action) { FetchAction() => () { _load(); return const LoadingState(); }(), _ => state, }, LoadingState() => switch (action) { SuccessAction(:final newData) => LoadedState(data: newData), FailAction(:final newMessage) => ErrorState(message: newMessage), _ => state, }, ErrorState() || LoadedState() => state, }; } dispatchの定義
これでステートマシンが作れました!
これを画面で使用してみる
class StateMachine { StateMachine(this.api) { _stateStreamController.add(state); } final ApiClient api;
// プライベート変数の宣言 final _stateStreamController = StreamController<SampleState>.broadcast(); SampleState _state = const SampleState.initial(); // ゲッターの宣言 Stream<SampleState> get stateStream => _stateStreamController.stream; SampleState get state => _state; // セッターの宣言、stateの値を変更した際にStreamにも値を流す set state(SampleState value) { _state = value; _stateStreamController.sink.add(value); } StateをStreamに
return StreamBuilder<SampleState>( stream: stateMachine.stateStream, builder: (context, snapshot) { final state
= snapshot.data ?? stateMachine.state return switch (state) { InitialState() => ElevatedButton( onPressed: () => stateMachine.dispatch(const FetchAction()), child: const Text('Fetch'), ), LoadingState() => const CircularProgressIndicator(), LoadedState(:final data) => Text(data), ErrorState(:final message) => Text(message), }; }, ); StateMachineを利用したWidget
ではこれを実際に動かしてみましょう!
デモ
ステートマシンを用いた 状態管理ができました!
複雑なステートマシンを作る
ページングのある画面のステートマシン
デモ
ステートマシンで 複雑なロジックも実装できました!
ステートマシンのデメリット
ステートマシンのデメリット • ステートマシンの設計には経験が必要 • 実装量が多くなる ◦ 小規模なアプリだとオーバーエンジニアリングに ◦ コード量が多いので開発者によって実装が変わる
弊社ではステートマシンを簡単に 実現するパッケージを開発しました!
dart_fsm
dart_fsmの思想 • 副作用の混じらない純粋なステートマシンの記述の強制 • 副作用の記述の方法を強制 • テスタビリティの高いステートマシンの実現
副作用の混じらない 純粋なステートマシンの記述の強制
void dispatch(SampleAction action) { state = switch (state) { InitialState()
=> switch (action) { FetchAction() => () { _load(); return const LoadingState(); }(), _ => state, }, LoadingState() => switch (action) { SuccessAction(:final newData) => LoadedState(data: newData), FailAction(:final newMessage) => ErrorState(message: newMessage), _ => state, }, ErrorState() || LoadedState() => state, }; } 純粋なステートマシンの記述の強制
final sampleStateGraph = GraphBuilder<SampleState, SampleAction>() ..state<InitialState>( (b) => b ..on<FetchAction>(
(state, action) => b.transitionTo(const LoadingState()), ), ) ..state<LoadingState>( (b) => b ..on<SuccessAction>( (state, action) => b.transitionTo(LoadedState(data: action.newData)), ) ..on<FailAction>( (state, action) => b.transitionTo(ErrorState(message: action.newMessage)), ), ); 純粋なステートマシンの記述の強制
副作用の記述の方法を強制
副作用の記述の方法を強制 • SideEffect ◦ 副作用本体 ◦ APIの呼び出しなどを行う • SideEffectCreator ◦
StateMachineの状態遷移を受けてSideEffectの実行を決定
class SampleSideEffect implements AfterSideEffect<SampleState, SampleAction> { SampleSideEffect(this.api); final ApiClient api;
@override Future<void> execute( StateMachine<SampleState, SampleAction> stateMachine, ) async { try { final data = await api.getData(); stateMachine.dispatch(SuccessAction(newData: data)); } catch (e) { stateMachine.dispatch(FailAction(newMessage: e.toString())); } } } 副作用の記述の方法を強制
class SampleSideEffectCreator implements AfterSideEffectCreator<SampleState, SampleAction, SampleSideEffect> { SampleSideEffectCreator(this.api); final ApiClient
api; @override SampleSideEffect? create(SampleState prevState, SampleAction action) { return switch(action) { FetchAction() => SampleSideEffect(api), _ => null, }; } } 副作用の記述の方法を強制
テスタビリティの高い ステートマシンの実現
dart_fsmを用いて実装
APIを叩いて情報を表示する画面
@freezed sealed class SampleState with _$SampleState { const factory SampleState.initial()
= InitialState; const factory SampleState.loading() = LoadingState; const factory SampleState.loaded({required String data}) = LoadedState; const factory SampleState.error({required String message}) = ErrorState; } Stateの定義
@freezed sealed class SampleAction with _$SampleAction { const factory SampleAction.fetch()
= FetchAction; const factory SampleAction.success({required String newData}) = SuccessAction; const factory SampleAction.fail({required String newMessage}) = FailAction; } Actionの定義
final sampleStateGraph = GraphBuilder<SampleState, SampleAction>() ..state<InitialState>( (b) => b ..on<FetchAction>(
(state, action) => b.transitionTo(const LoadingState()), ), ) ..state<LoadingState>( (b) => b ..on<SuccessAction>( (state, action) => b.transitionTo(LoadedState(data: action.newData)), ) ..on<FailAction>( (state, action) => b.transitionTo(ErrorState(message: action.newMessage)), ), ); Graphの定義
class SampleSideEffect implements AfterSideEffect<SampleState, SampleAction> { SampleSideEffect(this.api); final ApiClient api;
@override Future<void> execute( StateMachine<SampleState, SampleAction> stateMachine, ) async { try { final data = await api.getData(); stateMachine.dispatch(SuccessAction(newData: data)); } catch (e) { stateMachine.dispatch(FailAction(newMessage: e.toString())); } } } SideEffectの作成
class SampleSideEffectCreator implements AfterSideEffectCreator<SampleState, SampleAction, SampleSideEffect> { SampleSideEffectCreator(this.api); final ApiClient
api; @override SampleSideEffect? create(SampleState prevState, SampleAction action) { return switch(action) { FetchAction() => SampleSideEffect(api), _ => null, }; } } SideEffectCreatorの作成
final sampleStateMachine = createStateMachine( graphBuilder: sampleStateGraph, initialState: const InitialState(), sideEffectCreators:
[SampleSideEffectCreator(ApiClient())], ); StateMachineの作成
ステートマシンを利用する効果的な アーキテクチャについて
StateMachineが管理する状態
UIの状態とビジネスロジックの状態
ステートマシンは ビジネスロジックの状態管理
UIの状態とビジネスロジックの状態を 同時にステートマシンで管理するのは難しい
UIの変更に追従しやすい
UIの状態はどこで管理?
ステートマシンを用いるアーキテクチャ
Viewとステートマシンの間に ViewModelのような中間層を用意
ステートマシンを用いるアーキテクチャ
最適なアーキテクチャは アプリの特性によって変わる
final class SamplePageViewModel extends ChangeNotifier { SamplePageViewModel(this._stateMachine) { _stateMachine.stateStream.listen((_) =>
notifyListeners()); } final SampleStateMachine _stateMachine; // isLoadingはstateを参照するcomputed property bool get isLoading => _stateMachine.state is LoadingState; void fetch() { _stateMachine.dispatch(const FetchAction()); } } ステートマシンを用いたViewModel
まとめ
まとめ • ステートマシンを用いる状態管理を採用すると、設計と実装に おいて様々なメリットが得られ、保守性も向上します! • 弊社ではdart_fsmというパッケージを開発し、ステートマシン を簡単に実装できるようにしました! • ステートマシンの利用はビジネスロジックの状態管理に留め、 Viewとの間にViewModelを配置するのがおすすめです!
最後に
チームラボでは一緒にアプリを開発する 仲間を通年で募集しています
ステートマシンのような技術を用いて 高品質なアプリを開発したい方は是非!