Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[FlutterKaigi2024]ステートマシンで実現する高品質なFlutterアプリ開発

teamLab
November 20, 2024
660

 [FlutterKaigi2024]ステートマシンで実現する高品質なFlutterアプリ開発

teamLab

November 20, 2024
Tweet

Transcript

  1. @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
  2. class SamplePageState { SamplePageState({ required this.isLoading, required this.data, required this.errorMessage,

    }); final bool isLoading; final String? data; final String? errorMessage; } 不要なnullableを排除
  3. 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を排除
  4. 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', );
  5. final loadingState = LoadingState(); final loadedState = LoadedState('data'); final errorState

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

    = ErrorState('error'); 網羅的なテストの記述が簡単
  7. 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の定義
  8. @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の定義
  9. @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の定義
  10. class StateMachine { StateMachine(this.api); final ApiClient api; SampleState state =

    const SampleState.initial(); void dispatch(SampleAction action){} Future<void> _load() async {} } StateMachineの定義
  11. Future<void> _load() async { try { final data = await

    api.getData(); dispatch(SuccessAction(newData: data)); } catch (e) { dispatch(FailAction(newMessage: e.toString())); } } _loadメソッドの実装
  12. 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の定義
  13. 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に
  14. 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
  15. 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, }; } 純粋なステートマシンの記述の強制
  16. 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)), ), ); 純粋なステートマシンの記述の強制
  17. 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())); } } } 副作用の記述の方法を強制
  18. 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, }; } } 副作用の記述の方法を強制
  19. @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の定義
  20. @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の定義
  21. 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の定義
  22. 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の作成
  23. 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の作成
  24. 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