Slide 1

Slide 1 text

ステートマシンで実現する 高品質なFlutterアプリ開発 チームラボ株式会社 Smartphone Team Engineer そた

Slide 2

Slide 2 text

そた ● 23卒高専出身 ● FlutterとKMPを書いてます ● カメラにハマってます ● Flutter Kaigi運営 自己紹介 𝕏: @_sotaatos

Slide 3

Slide 3 text

会社紹介 ● 会社名: チームラボ株式会社 ● 主な事業内容 ○ アート ○ ソリューション

Slide 4

Slide 4 text

実績 ● りそなグループアプリ ● ネスカフェ ドルチェ グストアプリ ● スミセイ・デジタルコンシェルジュ ● 三井ショッピングパークアプリ ● それ以外にも多数!

Slide 5

Slide 5 text

こんなことに遭遇したことありませんか?

Slide 6

Slide 6 text

APIとの通信が完了したのに ローディングの表示が消えない

Slide 7

Slide 7 text

異常系の実装が漏れていて表示が崩れた

Slide 8

Slide 8 text

ある程度アプリ開発の経験がある方なら 一度や二度じゃないはずです。

Slide 9

Slide 9 text

このようなバグを減らし高品質なアプリを 開発するにはどうしたら良いでしょうか?

Slide 10

Slide 10 text

優れた状態管理の設計

Slide 11

Slide 11 text

BLoC Hooks Riverpod setState Provider GetX ChangeNotifier Signals Redux

Slide 12

Slide 12 text

弊社がメインで使用しているのは このどれでもありません

Slide 13

Slide 13 text

ステートマシン

Slide 14

Slide 14 text

ステートマシンとは

Slide 15

Slide 15 text

ステートマシンとは ● オートマトン(状態機械) ● 計算理論における概念 ● 有限オートマトン ○ 有限個の状態と遷移と動作の組み合わせからなる数学的に 抽象化された「ふるまいのモデル」である (Wikipedia)

Slide 16

Slide 16 text

● 100円と50円のみ使用可能 ● 途中で返金はできない ● 150円を超えたら自動でジュースが出てくる ステートマシンとは

Slide 17

Slide 17 text

ステートマシンとは

Slide 18

Slide 18 text

アプリの持つ状態も ステートマシンで表せる

Slide 19

Slide 19 text

APIを叩いて情報を表示する画面

Slide 20

Slide 20 text

RiverpodのAsyncValue

Slide 21

Slide 21 text

@riverpod Future 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

Slide 22

Slide 22 text

RiverpodのAsyncValueの構造

Slide 23

Slide 23 text

RiverpodのAsyncValueも ステートマシン!

Slide 24

Slide 24 text

Riverpodを使いましょう! ご清聴ありがとうございました!

Slide 25

Slide 25 text

…とはならないです!

Slide 26

Slide 26 text

独自にステートマシンを設計することに どのような優位性が?

Slide 27

Slide 27 text

ページングのある画面の処理 ● 画面表示時に一度APIを叩く ● APIはコンテンツと共に追加読み込み可能かどうかを返却 ● 追加読み込み可能の際は下までスクロールしたら追加読み込み ● 追加読み込み不可の場合はそこでAPIの呼び出しは終了

Slide 28

Slide 28 text

ページングのある画面のステートマシン

Slide 29

Slide 29 text

ステートマシンを独自に設計することで 複雑なビジネスロジックの表現が可能!

Slide 30

Slide 30 text

ここで一旦本題に戻りましょう

Slide 31

Slide 31 text

私たちが追い求めていたもの

Slide 32

Slide 32 text

高品質なアプリを開発するための 優れた状態管理の設計

Slide 33

Slide 33 text

ステートマシンはどのような点が 優れている?

Slide 34

Slide 34 text

ステートマシンは 設計と実装にメリットをもたらし 保守性を向上させます

Slide 35

Slide 35 text

設計と実装におけるメリット

Slide 36

Slide 36 text

画面の取りうる状態を 実装前に全て考えられる

Slide 37

Slide 37 text

ロード 中 コンテンツ 表示 エラー 表示

Slide 38

Slide 38 text

設計をそのまま実装に 落とし込むことが可能

Slide 39

Slide 39 text

不要なnullableを排除

Slide 40

Slide 40 text

class SamplePageState { SamplePageState({ required this.isLoading, required this.data, required this.errorMessage, }); final bool isLoading; final String? data; final String? errorMessage; } 不要なnullableを排除

Slide 41

Slide 41 text

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を排除

Slide 42

Slide 42 text

想定していない状態が起こらない

Slide 43

Slide 43 text

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', );

Slide 44

Slide 44 text

final loadingState = LoadingState(); final loadedState = LoadedState('data'); final errorState = ErrorState('error'); 想定していない状態が起こらない

Slide 45

Slide 45 text

保守性の向上

Slide 46

Slide 46 text

実装から設計が読み取りやすい

Slide 47

Slide 47 text

網羅的なテストの記述が簡単

Slide 48

Slide 48 text

final loadingState = LoadingState(); final loadedState = LoadedState('data'); final errorState = ErrorState('error'); 網羅的なテストの記述が簡単

Slide 49

Slide 49 text

拡張が容易

Slide 50

Slide 50 text

拡張が容易

Slide 51

Slide 51 text

拡張が容易

Slide 52

Slide 52 text

ステートマシンを用いた状態管理には 多くの利点

Slide 53

Slide 53 text

Dartで実装してみましょう

Slide 54

Slide 54 text

APIを叩いて情報を表示する画面

Slide 55

Slide 55 text

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の定義

Slide 56

Slide 56 text

@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の定義

Slide 57

Slide 57 text

@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の定義

Slide 58

Slide 58 text

class StateMachine { StateMachine(this.api); final ApiClient api; SampleState state = const SampleState.initial(); void dispatch(SampleAction action){} Future _load() async {} } StateMachineの定義

Slide 59

Slide 59 text

Future _load() async { try { final data = await api.getData(); dispatch(SuccessAction(newData: data)); } catch (e) { dispatch(FailAction(newMessage: e.toString())); } } _loadメソッドの実装

Slide 60

Slide 60 text

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の定義

Slide 61

Slide 61 text

これでステートマシンが作れました!

Slide 62

Slide 62 text

これを画面で使用してみる

Slide 63

Slide 63 text

class StateMachine { StateMachine(this.api) { _stateStreamController.add(state); } final ApiClient api; // プライベート変数の宣言 final _stateStreamController = StreamController.broadcast(); SampleState _state = const SampleState.initial(); // ゲッターの宣言 Stream get stateStream => _stateStreamController.stream; SampleState get state => _state; // セッターの宣言、stateの値を変更した際にStreamにも値を流す set state(SampleState value) { _state = value; _stateStreamController.sink.add(value); } StateをStreamに

Slide 64

Slide 64 text

return StreamBuilder( 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

Slide 65

Slide 65 text

ではこれを実際に動かしてみましょう!

Slide 66

Slide 66 text

デモ

Slide 67

Slide 67 text

ステートマシンを用いた 状態管理ができました!

Slide 68

Slide 68 text

複雑なステートマシンを作る

Slide 69

Slide 69 text

ページングのある画面のステートマシン

Slide 70

Slide 70 text

デモ

Slide 71

Slide 71 text

ステートマシンで 複雑なロジックも実装できました!

Slide 72

Slide 72 text

ステートマシンのデメリット

Slide 73

Slide 73 text

ステートマシンのデメリット ● ステートマシンの設計には経験が必要 ● 実装量が多くなる ○ 小規模なアプリだとオーバーエンジニアリングに ○ コード量が多いので開発者によって実装が変わる

Slide 74

Slide 74 text

弊社ではステートマシンを簡単に 実現するパッケージを開発しました!

Slide 75

Slide 75 text

dart_fsm

Slide 76

Slide 76 text

dart_fsmの思想 ● 副作用の混じらない純粋なステートマシンの記述の強制 ● 副作用の記述の方法を強制 ● テスタビリティの高いステートマシンの実現

Slide 77

Slide 77 text

副作用の混じらない 純粋なステートマシンの記述の強制

Slide 78

Slide 78 text

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, }; } 純粋なステートマシンの記述の強制

Slide 79

Slide 79 text

final sampleStateGraph = GraphBuilder() ..state( (b) => b ..on( (state, action) => b.transitionTo(const LoadingState()), ), ) ..state( (b) => b ..on( (state, action) => b.transitionTo(LoadedState(data: action.newData)), ) ..on( (state, action) => b.transitionTo(ErrorState(message: action.newMessage)), ), ); 純粋なステートマシンの記述の強制

Slide 80

Slide 80 text

副作用の記述の方法を強制

Slide 81

Slide 81 text

副作用の記述の方法を強制 ● SideEffect ○ 副作用本体 ○ APIの呼び出しなどを行う ● SideEffectCreator ○ StateMachineの状態遷移を受けてSideEffectの実行を決定

Slide 82

Slide 82 text

class SampleSideEffect implements AfterSideEffect { SampleSideEffect(this.api); final ApiClient api; @override Future execute( StateMachine stateMachine, ) async { try { final data = await api.getData(); stateMachine.dispatch(SuccessAction(newData: data)); } catch (e) { stateMachine.dispatch(FailAction(newMessage: e.toString())); } } } 副作用の記述の方法を強制

Slide 83

Slide 83 text

class SampleSideEffectCreator implements AfterSideEffectCreator { SampleSideEffectCreator(this.api); final ApiClient api; @override SampleSideEffect? create(SampleState prevState, SampleAction action) { return switch(action) { FetchAction() => SampleSideEffect(api), _ => null, }; } } 副作用の記述の方法を強制

Slide 84

Slide 84 text

テスタビリティの高い ステートマシンの実現

Slide 85

Slide 85 text

dart_fsmを用いて実装

Slide 86

Slide 86 text

APIを叩いて情報を表示する画面

Slide 87

Slide 87 text

@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の定義

Slide 88

Slide 88 text

@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の定義

Slide 89

Slide 89 text

final sampleStateGraph = GraphBuilder() ..state( (b) => b ..on( (state, action) => b.transitionTo(const LoadingState()), ), ) ..state( (b) => b ..on( (state, action) => b.transitionTo(LoadedState(data: action.newData)), ) ..on( (state, action) => b.transitionTo(ErrorState(message: action.newMessage)), ), ); Graphの定義

Slide 90

Slide 90 text

class SampleSideEffect implements AfterSideEffect { SampleSideEffect(this.api); final ApiClient api; @override Future execute( StateMachine stateMachine, ) async { try { final data = await api.getData(); stateMachine.dispatch(SuccessAction(newData: data)); } catch (e) { stateMachine.dispatch(FailAction(newMessage: e.toString())); } } } SideEffectの作成

Slide 91

Slide 91 text

class SampleSideEffectCreator implements AfterSideEffectCreator { SampleSideEffectCreator(this.api); final ApiClient api; @override SampleSideEffect? create(SampleState prevState, SampleAction action) { return switch(action) { FetchAction() => SampleSideEffect(api), _ => null, }; } } SideEffectCreatorの作成

Slide 92

Slide 92 text

final sampleStateMachine = createStateMachine( graphBuilder: sampleStateGraph, initialState: const InitialState(), sideEffectCreators: [SampleSideEffectCreator(ApiClient())], ); StateMachineの作成

Slide 93

Slide 93 text

ステートマシンを利用する効果的な アーキテクチャについて

Slide 94

Slide 94 text

StateMachineが管理する状態

Slide 95

Slide 95 text

UIの状態とビジネスロジックの状態

Slide 96

Slide 96 text

ステートマシンは ビジネスロジックの状態管理

Slide 97

Slide 97 text

UIの状態とビジネスロジックの状態を 同時にステートマシンで管理するのは難しい

Slide 98

Slide 98 text

UIの変更に追従しやすい

Slide 99

Slide 99 text

UIの状態はどこで管理?

Slide 100

Slide 100 text

ステートマシンを用いるアーキテクチャ

Slide 101

Slide 101 text

Viewとステートマシンの間に ViewModelのような中間層を用意

Slide 102

Slide 102 text

ステートマシンを用いるアーキテクチャ

Slide 103

Slide 103 text

最適なアーキテクチャは アプリの特性によって変わる

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

まとめ

Slide 106

Slide 106 text

まとめ ● ステートマシンを用いる状態管理を採用すると、設計と実装に おいて様々なメリットが得られ、保守性も向上します! ● 弊社ではdart_fsmというパッケージを開発し、ステートマシン を簡単に実装できるようにしました! ● ステートマシンの利用はビジネスロジックの状態管理に留め、 Viewとの間にViewModelを配置するのがおすすめです!

Slide 107

Slide 107 text

最後に

Slide 108

Slide 108 text

チームラボでは一緒にアプリを開発する 仲間を通年で募集しています

Slide 109

Slide 109 text

ステートマシンのような技術を用いて 高品質なアプリを開発したい方は是非!