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
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
77
border-radiusだけじゃ足りない: Squircleがつくる自然なUI
teamlab
PRO
0
43
僕はただドキュメントを HTML で見たいだけなんだ
teamlab
PRO
0
49
アクセシビリティに配慮したアニメーション
teamlab
PRO
0
41
TSKaigi 2026 - 10秒のビルドを1秒へ:tsdownが切り拓く2026年のTypeScriptライブラリ開発
teamlab
PRO
2
380
TSKaigi 2026 - enumよ、さようなら
teamlab
PRO
3
700
TSKaigi 2026 - 型プラグインシステムの実装に使われるテクニック
teamlab
PRO
2
510
TSKaigi 2026 - Auth.jsからBetter Authへの 移行に見る「型とランタイム」の 設計思想の変化
teamlab
PRO
1
320
TSKaigi Hokuriku - TypeScriptによる静的データガバナンス
teamlab
PRO
0
170
Featured
See All Featured
The SEO Collaboration Effect
kristinabergwall1
1
500
jQuery: Nuts, Bolts and Bling
dougneiner
66
8.5k
The Curse of the Amulet
leimatthew05
2
13k
Between Models and Reality
mayunak
4
360
Side Projects
sachag
455
43k
Everyday Curiosity
cassininazir
0
250
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
340
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
260
Building Flexible Design Systems
yeseniaperezcruz
330
40k
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
62
44k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
460
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を配置するのがおすすめです!
最後に
チームラボでは一緒にアプリを開発する 仲間を通年で募集しています
ステートマシンのような技術を用いて 高品質なアプリを開発したい方は是非!