Flutterやってみたよ part4 （状態管理導入 Redux編 ）

# はじめに
[前回]()に引き続きFlutterアプリの製造を行っていきます。
[前回までのソース](https://github.com/ikuo-yamashita/flutter_firebase_sample/tree/develop)は状態管理なしでゴリゴリに書いたやつです。。

# 導入前にアプリの動きを決める
せっかく状態管理導入するのでそれらしい動きを見せるため、サンプルアプリの仕様を決めます。

画面遷移はこんな感じでサインイン・サインアップ・サインアウトを行うだけのシンプルな構成にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325497/99f3cdc5-bdd0-f1b6-213f-84a63dbdc0ae.png)



# AppState
まずは状態を用意します。
今回はシンプルにFirebase Authenticationの情報を状態とします。

```dart
@immutable
class AppState {

  final Authentication authentication;

  const AppState(
      {
        this.authentication = const Authentication(AuthStatus.initState, null, "", "", null),
      });

  factory AppState.initState() => const AppState();

  AppState copyWith({
    AuthStatus authStatus,
  }) {
    return new AppState(
      authentication: authentication ?? this.authentication,
    );
  }

  @override
  int get hashCode => authentication.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
          other is AppState && runtimeType == other.runtimeType &&
              authentication == other.authentication;

  @override
  String toString() {
    return 'AppState{authentication: $authentication}';
  }

}


class Authentication {
  final AuthStatus authStatus;
  final FirebaseUser firebaseUser;
  final String errorCode;
  final String errorMessage;
  final Exception e;
  const Authentication(this.authStatus, this.firebaseUser, this.errorCode, this.errorMessage, this.e);
}
```

# Anction、Reducer
次に状態を更新するのに必要なActionとReducerを用意します。

Reducer

```dart

AppState appReducer(AppState state, action) {
  return new AppState(
    // 各アクションで変更された状態で新しい状態を作成する
    authentication: authStateReducer(state.authentication, action),
  );
}

final authStateReducer = combineReducers<Authentication>([
  TypedReducer<Authentication, CheckAuthStatusCompleteAction>(_setAuthStatusCheckAction),
  TypedReducer<Authentication, SignInCompleteAction>(_setAuthStatusSignInAction),
  TypedReducer<Authentication, SignInFailedAction>(_setAuthStatusSignInFailedAction),
  TypedReducer<Authentication, SignUpCompleteAction>(_setAuthStatusSignUpAction),
  TypedReducer<Authentication, SignUpFailedAction>(_setAuthStatusSignUpFailedAction),
  TypedReducer<Authentication, SignOutCompleteAction>(_setAuthStatusSignOutAction),
]);

// 状態の確認を行うアクションでの状態更新
Authentication _setAuthStatusCheckAction(Authentication state, CheckAuthStatusCompleteAction action) {
  return action.authentication;
}
// サインインアクションでの状態更新
Authentication _setAuthStatusSignInAction(Authentication state, SignInCompleteAction action) {
  return action.authentication;
}
// サインイン失敗した時のアクションでの状態更新
Authentication _setAuthStatusSignInFailedAction(Authentication state, SignInFailedAction action) {
  return action.authentication;
}
// サインアップアクションでの状態更新
Authentication _setAuthStatusSignUpAction(Authentication state, SignUpCompleteAction action) {
  return action.authentication;
}
// サインアップ失敗した時のアクションでの状態更新
Authentication _setAuthStatusSignUpFailedAction(Authentication state, SignUpFailedAction action) {
  return action.authentication;
}
// サインアウトした時のアクションでの状態更新
Authentication _setAuthStatusSignOutAction(Authentication state, SignOutCompleteAction action) {
  return action.authentication;
}

```

Action

```dart

// CheckAuthStatus
class CheckAuthStatusAction {}
class CheckAuthStatusCompleteAction {
  final Authentication authentication;
  const CheckAuthStatusCompleteAction(this.authentication);
}
class CheckAuthStatusFailedAction {
  final Authentication authentication;
  const CheckAuthStatusFailedAction(this.authentication);
}

// SignIn
class SignInAction {
  final String email;
  final String password;
  const SignInAction(this.email, this.password);
}
class SignInCompleteAction {
  final Authentication authentication;
  const SignInCompleteAction(this.authentication);
}
class SignInFailedAction {
  final Authentication authentication;
  const SignInFailedAction(this.authentication);
}

// SignUp
class SignUpAction {
  final String displayName;
  final String email;
  final String password;
  const SignUpAction(this.displayName, this.email, this.password);
}
class SignUpCompleteAction {
  final Authentication authentication;
  const SignUpCompleteAction(this.authentication);
}
class SignUpFailedAction {
  final Authentication authentication;
  const SignUpFailedAction(this.authentication);
}

// SignOut
class SignOutAction {}
class SignOutCompleteAction {
  final Authentication authentication;
  const SignOutCompleteAction(this.authentication);
}
class SignOutFailedAction {
  final Authentication authentication;
  const SignOutFailedAction(this.authentication);
}

```

# Middleware
アクションを検知してそのアクションに応じた処理を実装します。
今回はサインインの状態確認・サインイン・サインアップ・サインアウトです。
Middlewareは全てのアクションで通るのでどのアクションで動くのか最初に判定しています。
長くなるので詳細は割愛してます。

```dart
void checkAuthStatusMiddleware(Store<AppState> store, action, NextDispatcher next) {
  if (action is CheckAuthStatusAction) {
    // 状態確認処理
    next(次のアクション);
  }
  next(action);
}

Future<void> signInMiddleware(Store<AppState> store, action, NextDispatcher next) async {
  if (action is SignInAction) {
    // サインイン処理
    next(次のアクション);
  }
  next(action);
}

Future<void> signUpMiddleware(Store<AppState> store, action, NextDispatcher next) async {
  if (action is SignUpAction) {
    // サインアップ処理
    next(次のアクション);
  }
  next(action);
}

Future<void> signOutMiddleware(Store<AppState> store, action, NextDispatcher next) async {
  if (action is SignOutAction) {
    // サインアウト処理
    next(次のアクション);
  }
  next(action);
}


```
ここまでくれば状態の準備は完了です。

# 実際に使ってみる
では実際に状態をどのように使うのかサインイン画面でみていきます。
全部ではないですがコードを載せました。

ポイントは**「widgetの全体をStoreProviderで囲み、状態の監視が必要なwidgetのみStoreConnectorで囲む」**です。
状態の変更はaction経由じゃないとダメなのでstore.dispatchで状態変更させます。

```dart
class SignInScreen extends StatefulWidget {
  SignInScreen({Key key}) : super(key: key);

  static final formKey = new GlobalKey<FormState>();

  @override
  _SignInScreenState createState() => _SignInScreenState();
}

class _SignInScreenState extends State<SignInScreen> {

  @override
  Widget build(BuildContext context) {
    Store<AppState> store = Store<AppState>(
      appReducer,
      initialState: AppState.initState(),
      // ここで走らせるmiddlewareを定義する
      middleware: [checkAuthStatusMiddleware, signInMiddleware],
    );

    // 先頭にStoreProviderを定義してsoreを渡す
    return new StoreProvider(
      store: store,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Login Screen"),
        ),
        // ・・・長いので色々省略・・・
        body: new Center(
          child: new Form(
            key: SignInScreen.formKey,
            child: new Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  emailText(),
                  passwordText(),
                  signInButton(store, progress),
                  signUpButton(),
                ]
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget emailText() {
    return new TextFormField(
      key: new Key("email"),
      decoration: new InputDecoration(labelText: "Email"),
      autocorrect: false,
      validator: (val) => val.isEmpty ? "emailを入力してください" : null,
      onSaved: (val) => {
        // 画面の入力なので状態管理せずviewModelの変数に突っ込む
        _viewModel.email = val
      },
    );
  }

  Widget passwordText() {
    return new TextFormField(
      key: new Key("password"),
      decoration: new InputDecoration(labelText: "Password"),
      obscureText: true,
      autocorrect: false,
      validator: (val) => val.isEmpty ? "パスワードを入力してください" : null,
      onSaved: (val) => {
        // 画面の入力なので状態管理せずviewModelの変数に突っ込む
        _viewModel.password = val
      },
    );
  }

  Widget signInButton(Store<AppState> store, ProgressDialog progress) {
    // 状態管理が必要なwidgetの場合はStoreConnectorで囲む。
    // StoreConnectorで囲まれた配下のwidgetがリビルド対象になるので囲むのは必要最低限にすること。
    return StoreConnector<AppState, Authentication>(
      distinct: true,
      // converterで参照したい値に変換してあげる。必要ないならそのままstoreが参照できる
      converter: (store) => store.state.authentication,
      // converterで変換かけた値が右側の変数authentication
      builder: (context, authentication) {
        return new PrimaryButton(
            key: new Key("signIn"),
            text: "サインイン",
            height: 44.0,
            onPressed: () {
              if (!validateAndSave() || _viewModel.isEmpty()) {
                return;
              }
              progress.show();
              // store.dispatch（アクション）で状態変更アクションを投げることができる
              // 投げるとmiddleware→reducer→stateの流れでstateが更新される
              store.dispatch(SignInAction(_viewModel.email, _viewModel.password));
            }
        );
      },
      // 状態が変わった後に呼ばれるとこ
      onDidChange: (authentication) {
        progress.hide();
        if (authentication.errorCode.isNotEmpty) {
          return;
        }
        if (authentication.authStatus == AuthStatus.signedIn) {
          Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomeScreen()));
          return;
        }
      },
    );
  }

  Widget signUpButton() {
    return new FlatButton(
        key: new Key("need-account"),
        textColor: Colors.green,
        child: new Text(
            "初めて利用する方\n（サインアップ）",
            textAlign: TextAlign.center
        ),
        onPressed: () {
          Navigator.push(context, MaterialPageRoute(builder: (context) => SignUpScreen()));
        }
    );
}
```
これで未ログイン状態からログイン状態のstateに変更することができました。

# 終わりに

実際に導入してみた感想としては、コード量が圧倒的に増えて少し面倒だなあと思いました。
それに設計思想はシンプルだと思ったんですが、実際の実装は少しわかりずらいと感じました。
ただ状態を更新するのにお決まりのコードが必要なので状態が壊れたり（意図せず変更されたり）するのは少ないのかなと思います。

完成したサンプルコードは[こちら](https://github.com/190yamayama/flutter_redux_sample)です。