【Flutter】シンプルなTwitter風アカウント作成機能を作ってみた（TextFormField、cloud_firestoreなど）

# はじめに
Twitterみたいなアカウント作成を出来るだけ作ってみました！
（とは言っても、名前とユーザー名の登録だけのシンプルなものですので電話番号やメールでの認証は行っていません）


# 今回作成するものについて
出来るだけTwitterに寄せていきたいと思います。


## 条件
Twitterのヘルプを見てみると…
>ユーザー名の長さは15文字までです。名前は4文字以上50文字以下にすることができますが、ユーザー名は使いやすいように短くなっています。
 (引用元：[ユーザー名の登録のヘルプ](https://help.twitter.com/ja/managing-your-account/twitter-username-rules))


>ユーザー名を選択できないはなぜですか？
 ユーザー名の中には、他のアカウントが使用しているため利用できなかったり、その他の理由で利用不可であったりするものがあります。利用可能なユーザー名を選ぶためのヒントをご覧ください。
 (引用元：[ユーザー名に関する一般的な問題のヘルプ](https://help.twitter.com/ja/managing-your-account/help-with-common-username-issues))

とのことなので、これを踏まえています。


## やること
- 名前とユーザー名を登録する
    - 名前は50文字まで
    - ユーザー名は15文字まで
- ユーザー名が既に使用されていたら登録できない


## 今回はやらないこと
- 名前が4文字以上であるかの判定
- 特定のユーザー名の利用を禁止
    - TwitterやAdminという文字を含めることは本当は使用できないが、今回は考慮しない
- 電話番号やメールアドレスの認証
- shared_preferencesの使用
    - 現時点では活用してません

もしかしたら今後追記しているかも…？？

# 作ってみる
出来上がりイメージ↓
![Screenshot_1640416949.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630025/450e9b54-d72a-3eb9-7f61-94fa1908098c.png)
名前に「テスト」、ユーザー名に「username」と記入して新規作成すると、以下のように登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630025/14fc6d12-7556-a294-484d-f63988c53569.png)

ちなみに使用済みのユーザー名の時はこんな感じです。
![Screenshot_1640417114.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630025/53b6de59-51b4-b9de-9ccf-36b6752ea0f4.png)


## 完成形のコード全体
```dart:singup.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twitter/utils/firebase.dart';

class SignUpPage extends StatefulWidget {
  const SignUpPage({Key? key}) : super(key: key);

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

class _SignUpPageState extends State<SignUpPage> {
  final _formKey = GlobalKey<FormState>();
  bool _existsUserName = false;

  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _userNameController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    _userNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('アカウント作成'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            const SizedBox(
              height: 200,
            ),

            Padding(
              padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 20),
              child: Form(
                key: _formKey,
                child: Column(
                  children: [
                    TextFormField(
                      controller: _nameController,
                      decoration: const InputDecoration(
                        hintText: '名前',
                      ),
                      maxLength: 50,
                      textInputAction: TextInputAction.next,
                      validator: (String? value){
                        return value!.isEmpty ? '名前を入力してください' : null;
                      },
                    ),

                    TextFormField(
                      controller: _userNameController,
                      decoration: const InputDecoration(
                        hintText: 'ユーザー名',
                      ),
                      maxLength: 15,
                      textInputAction: TextInputAction.done,
                      onChanged: (value) async{
                        _existsUserName = await Firestore.existsUserName(value);
                        _formKey.currentState!.validate();
                      },
                      validator: (String? value){
                        if(value!.isEmpty) {
                          return 'ユーザー名を入力してください';
                        } else if(_existsUserName) {
                          return 'そのユーザー名は使用されています';
                        } else {
                          return null;
                        }
                      },
                    ),

                    Padding(
                      padding: const EdgeInsets.only(top: 20.0),
                      child: ElevatedButton(
                        onPressed: () async{
                          if(_formKey.currentState!.validate()){
                            if(!_existsUserName) {
                              await Firestore.signUp(_nameController.text, _userNameController.text);
                              // ここでページ遷移させる。今はしていない
                            }
                            print(_nameController.text);
                            print(_userNameController.text);
                          }
                        },
                        child: const Text('新規作成'),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

```dart:firebase.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class Firestore {
  static FirebaseFirestore firestoreInstance = FirebaseFirestore.instance;

  static CollectionReference users = firestoreInstance.collection('users');

  static Future<bool> existsUserName(userName) async{
    bool temp = false;
    await users
        .doc(userName)
        .get()
        .then((DocumentSnapshot documentSnapshot) {
          if (documentSnapshot.exists) {
            temp = true;
          }
        });

    return temp;
  }

  static Future<void> signUp(name, userName) async {
    await users
        .doc(userName)
        .set({
           'name': name,
           'createdAt': Timestamp.now()
        })
        .then((value) => print("signUp"))
        .catchError((error) => print("Failed to singUp: $error"));
  }
}
```

## 補足説明
### existsUserName
ユーザー名が既に登録されているかどうかをチェックする。存在していたらtrue、存在してなかったらfalseが返る。

```dart:firebase.dart
static Future<bool> existsUserName(userName) async{
    bool temp = false;
    await users
        .doc(userName)
        .get()
        .then((DocumentSnapshot documentSnapshot) {
          if (documentSnapshot.exists) {
            temp = true;
          }
        });

    return temp;
  }
```

### signUp
ユーザー名をドキュメント名に使用し、名前と登録日を記録する。

```dart:firebase.dart
static Future<void> signUp(name, userName) async {
    await users
        .doc(userName)
        .set({
          'name': name,
          'createdAt': Timestamp.now()
        })
        .then((value) => print("signUp"))
        .catchError((error) => print("Failed to singUp: $error"));
}
```

### form関連
重要なものを抜粋したつもりです。ユーザー名が使用されているかどうか判断するexistsUserNameを活用してバリデーションを設定するのに苦労しました…。[こちら](https://moimoiblog.com/programing/fluter-textformfield-validator-async/)のサイトを参考にさせていただきました。


```dart:singup.dart
// 抜粋しています
class _SignUpPageState extends State<SignUpPage> {
  final _formKey = GlobalKey<FormState>();
  bool _existsUserName = false;

  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _userNameController = TextEditingController();

  // TextEditingControllerはdisposeする
  @override
  void dispose() {
    _nameController.dispose();
    _userNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...,
      body: SingleChildScrollView(
        ...,
              child: Form(
                key: _formKey, // keyを指定
                child: Column(
                  children: [
                    TextFormField(
                      controller: _nameController, // controllerを指定
                      validator: (String? value){
                        return value!.isEmpty ? '名前を入力してください' : null;
                      },
                    ),

                    TextFormField(
                      controller: _userNameController, // controllerを指定
                      // 非同期処理をさせる
                      onChanged: (value) async{
                        _existsUserName = await Firestore.existsUserName(value);
                        _formKey.currentState!.validate();
                      },
                      validator: (String? value){
                        if(value!.isEmpty) {
                          return 'ユーザー名を入力してください';
                        } else if(_existsUserName) {
                          return 'そのユーザー名は使用されています';
                        } else {
                          return null;
                        }
                      },
                    ),

                   ...,
                      child: ElevatedButton(
                        onPressed: () async{
                          if(_formKey.currentState!.validate()){
                            if(!_existsUserName) {
                              // ユーザー名が使用されていない場合、新規作成する
                              await Firestore.signUp(_nameController.text, _userNameController.text);
                            }
                          }
                        },
                        child: const Text('新規作成'),
                      ),
...

```

# 参考資料
https://firebase.flutter.dev/docs/firestore/usage

https://flutter.ctrnost.com/basic/interactive/form/textfield/

https://api.flutter.dev/flutter/widgets/TextEditingController-class.html

https://moimoiblog.com/programing/fluter-textformfield-validator-async/

# おわりに
ここまで読んでいただきありがとうございました。書き終わって気づいたのですが、パスワードの設定を完全に忘れていました…。また別の記事にして投稿していきたいと思います。

何かありましたらコメントをお願いします。