FlutterとAWSで始めるサービス開発 (4)AWS Cognitoへの新規アカウント登録


# はじめに
前回の[「(3)AWS Cognito連携」](https://qiita.com/makotomi/items/cb6ccb8568eacf8772dd)では、Cognitoユーザープールを準備し、Flutterアプリで使うための準備を行いました。今回は、それを使って、新規アカウント登録機能を実装したいと思います。

# 新規アカウント登録の流れ

1. メールアドレスとパスワードをユーザープールに送信
2. アカウントが、ステータス「__UNCONFIRMED__」、E メール確認済み「__false__」で作成される
3. 入力したメールアドレス宛に確認コードが送信される
4. メールアドレスと確認コードをユーザープールに送信
5. アカウントのステータスが「__CONFIRMED__」、E メール確認済みが「__true_」_に変化しアカウントが使えるようになる。

# 画面遷移

エラー処理等々を考えると問題があるのだがフローを追いやすくするため、以下のような単純な画面遷移とします。ログイン画面('/')から新規登録画面(RegisterUSer'')へ遷移し、レジスターコード入力画面('ConfirmRegistration')へ遷移し、処理が完了したらログイン画面へ戻るという流れになります。

![画面遷移 Step1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/af1d836b-640e-db89-a5b0-d7edc0129757.png)


# コード

今回の実装したlib/main.dartファイルをすべて掲載します。cognitoのプールIDやクライアントIDは伏字にしていますので実際に動かす場合にはそこを変更してください。

```dart
import 'package:flutter/material.dart';
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

final userPool = new CognitoUserPool(
    'ap-northeast-1_XoqXlafQm', '1e7ri3io3j0ds336077gelhhub');

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'login sample app',
      routes: <String, WidgetBuilder>{
        '/': (_) => new MyHomePage(),
        '/RegisterUser': (_) => new RegisterUserPage(),
        '/ConfirmRegistration': (_) => new ConfirmRegistration(null),
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ログイン'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: Text('新しいアカウントの作成'),
              color: Colors.indigo,
              textColor: Colors.white,
              shape: StadiumBorder(),
              onPressed: () => Navigator.of(context).pushNamed('/RegisterUser'),
            ),
          ],
        ),
      ),
    );
  }
}

class RegisterUserPage extends StatelessWidget {
  final _mailAddressController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('アカウント作成'),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: TextField(
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    hintText: 'test@examle.com',
                    labelText: 'メールアドレス',
                  ),
                  controller: _mailAddressController,
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: TextField(
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    hintText: 'password',
                    labelText: 'パスワード',
                  ),
                  obscureText: true,
                  controller: _passwordController,
                ),
              ),
              Container(
                alignment: Alignment.centerRight,
                padding: const EdgeInsets.all(8.0),
                child: RaisedButton(
                  child: Text('登録'),
                  color: Colors.indigo,
                  shape: StadiumBorder(),
                  textColor: Colors.white,
                  onPressed: () => _signUp(context),
                ),
              ),
            ]),
      ),
    );
  }

  void _signUp(BuildContext context) async {
    try {
      CognitoUserPoolData userPoolData = await userPool.signUp(
          _mailAddressController.text, _passwordController.text);
      Navigator.push(
        context,
        new MaterialPageRoute<Null>(
          settings: const RouteSettings(name: "/ConfirmRegistration"),
          builder: (BuildContext context) => ConfirmRegistration(userPoolData),
        ),
      );
    } on CognitoClientException catch (e) {
      await showDialog<int>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('エラー'),
            content: Text(e.message),
            actions: <Widget>[
              FlatButton(
                child: Text('OK'),
                onPressed: () => Navigator.of(context).pop(1),
              ),
            ],
          );
        },
      );
    }
  }
}

class ConfirmRegistration extends StatelessWidget {
  final _registrationController = TextEditingController();
  final CognitoUserPoolData _userPoolData;

  ConfirmRegistration(this._userPoolData);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('レジストレーションキー確認'),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(_userPoolData.user.username),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: TextField(
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    hintText: 'レジストレーションコード',
                    labelText: 'レジストレーションコード',
                  ),
                  obscureText: true,
                  controller: _registrationController,
                ),
              ),
              Container(
                alignment: Alignment.centerRight,
                padding: const EdgeInsets.only(right: 8.0),
                child: RaisedButton(
                  child: Text('確認'),
                  color: Colors.indigo,
                  shape: StadiumBorder(),
                  textColor: Colors.white,
                  onPressed: () => _confirmRegistration(context),
                ),
              ),
            ]),
      ),
    );
  }

  void _confirmRegistration(BuildContext context) async {
    try {
      await _userPoolData.user
          .confirmRegistration(_registrationController.text);
      await showDialog<int>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('登録完了'),
            content: Text('ユーザーの登録が完了しました。'),
            actions: <Widget>[
              FlatButton(
                child: Text('OK'),
                onPressed: () =>
                    Navigator.of(context).popUntil(ModalRoute.withName('/')),
              ),
            ],
        );
        },
      );
    } on CognitoClientException catch (e) {
      await showDialog<int>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('エラー'),
            content: Text(e.message),
            actions: <Widget>[
              FlatButton(
                child: Text('OK'),
                onPressed: () => Navigator.of(context).pop(1),
              ),
            ],
          );
        },
      );
    }
  }
}

```

# MyApp

__MaterialApp__のroutesパラメータで指定しているか気がFlutterでの画面遷移の肝となる実装になります。ここで画面に名前をつけて事前に登録しておきます。この登録した名前をベースに画面の移動を指示することで画面遷移を制御します。本実装では3種類の画面を名前をつけて登録しており、'/'で登録しているのが初期画面、'/RegisterUser'で登録しているのがユーザー新規登録画面、'/ConfirmRegistration'で登録しているのがレジストレーションコードの確認画面になります。

```dart
      routes: <String, WidgetBuilder>{
        '/': (_) => new MyHomePage(),
        '/RegisterUser': (_) => new RegisterUserPage(),
        '/ConfirmRegistration': (_) => new ConfirmRegistration(null),
      },
```

# MyHomePage
初期に表示されるログイン画面です。画面は以下のようになっています。

![画面1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/99ed0726-4122-d058-dec5-8ecbe2ca785e.png)


新しいアカウントの作成を開始するためのボタンを定義しており、ボタンを押下したら、`Navigator.of(context).pushNamed('/RegisterUser'),`を実行します。

```dart
            RaisedButton(
              child: Text('新しいアカウントの作成'),
              color: Colors.indigo,
              textColor: Colors.white,
              shape: StadiumBorder(),
              onPressed: () => Navigator.of(context).pushNamed('/RegisterUser'),
            ),
```

これが画面を遷移するためのコードで、`'/RegisterUser'`と名前の付いた画面を__Push__するという指示です。Flutterの画面管理は__Stack__のモデルになっており、__Stack__の最上位にある画面が表示されるようになっています。つまり、__Stack__に対して__Push__するとStackに積まれ画面が切り替わります。また、__Stack__から__Pop__すると最上位の画面が取り除かれ、ひとつ前の画面に戻るといった動きになります。絵で表すと以下の通りです。

![stack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/56f6c06c-eef7-8d6f-fa4e-dab33b941534.png)



# RegisterUserPage
アカウントの新規作成ページです。画面は以下のようになっています。

![画面2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/877bb1fb-9880-1675-9d9b-d06be42fff90.png)

登録するためのメールアドレスとパスワードを入力するためのテキストフィールドと登録処理を実行するためのボタンを定義しています。
ボタンを押下すると**_signup**メソッドを実行します。このメソッドではまず、ユーザープールに入力されたメールアドレスとパスワードを送りアカウント作成します。

``` dart
      CognitoUserPoolData userPoolData = await userPool.signUp(
          _mailAddressController.text, _passwordController.text);
```
上記API呼び出しが成功したら、APIの戻り値を渡し次の画面へ遷移させます。以下は、引数でデータを渡す場合の定型実装になります。

``` dart
      Navigator.push(
        context,
        new MaterialPageRoute<Null>(
          settings: const RouteSettings(name: "/ConfirmRegistration"),
          builder: (BuildContext context) => ConfirmRegistration(userPoolData),
        ),
      );
```

# ConfirmRegistration
確認コードの入力画面です。画面は以下のようになっています。

![画面3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/37478048-8c65-f4a3-f40a-debaa000a157.png)

前画面で登録したメールアドレスの表示、確認コードを入力するためのテキストフィールドと確認を実行するためのボタンを定義しています。
ボタンを押下すると**_confirmRegistration**メソッドを実行します。このメソッドではまず、ユーザープールに確認コードを送信します。
以下のように、前画面から渡されたアカウント作成結果のオブジェクトの__confirmRegistration__メソッドを使い、入力された確認コードを送信します。
```dart
      await _userPoolData.user
          .confirmRegistration(_registrationController.text);
```

APIの呼び出しが成功したら、以下のダイアログを表示します。

![画面4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/0f880c50-35dd-e648-a63f-e71b23854e77.png)

ダイアログでOKを押下したら`Navigator.of(context).popUntil(ModalRoute.withName('/'))`を実行します。これは画面のStackから名前が'/'の画面までpopするという指示になります。以下のように一気にログイン画面まで戻れます。

![stack popuntill.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/ae6f71af-4111-4e50-c8f3-1b46b94e2175.png)


# まとめ
ユーザープールへのアカウント登録の流れを確認できました。また、Flutterの画面遷移のごくごく基本的なところも見えてきたと思います。
次回「[(5)AWS Cognitoでログイン](https://qiita.com/makotomi/items/a0b05e8819270072780b)」では、今回作ったアカウントを使ってログインする処理を見ていきたいと思います。