Flutterのテキストフィールドに数字のみを受け付ける。

今回やっていくのは数字のみを受け付けるテキストフィールドの実装です。
簡単そうで意外とめんどくさいので記事にしてみます。

何がめんどくさいのかを書けるように紆余曲折を全部流れで書いて行こうと思います。
実装だけ知りたい方は[ここからGithubレポジトリ](https://github.com/miyaji555/number_text_field/tree/master)が見れます。

#目次
[1.最初の準備](#1-最初の準備)
[2.テキストフィールドの作成](#2-テキストフィールドの作成)
[3.テキストフィールドの数字対応](#3-テキストフィールドの数字対応)
[4.数字対応したテキストフィールドを閉じる](#4-数字対応したテキストフィールドを閉じる)


#1. 最初の準備

自分はもっぱらProviderパッケージを用いたMVVMでアプリを開発しているので、まずサンプルのアプリをMVVMで表示していきます。

```main.dart
import 'package:flutter/material.dart';
import 'package:number_text_field/main_model.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel(),
      child: Consumer<MainModel>(builder: (context, model, child) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Flutter Demo Home Page'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  model.counter.toString(),
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              model.incrementCounter();
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }),
    );
  }
}

```
```main_model.dart
import 'package:flutter/cupertino.dart';

class MainModel extends ChangeNotifier {
  int counter = 0;

  void incrementCounter() {
    counter++;
    notifyListeners();
  }
}

```

上記のようにサンプルのアプリを書き換えることで、サンプルアプリと同様の機能のアプリがMVVMで実装できました。
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/935513/d2ec2027-0cbd-14db-ce48-5f2783fc1dfc.gif" width = 200px>


#2. テキストフィールドの作成

次に適当なテキストフィールドを作成します。

テキストフィールドだけ作っても味気ないので、右下のボタンを押すとテキストフィールドの中身が上に表示されるという機能にしてみます。

```main.dart
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'When you push the button, this text is updated:',
                ),
                Text(
                  model.text,
                  style: Theme.of(context).textTheme.headline4,
                ),
                Container(
                  padding: EdgeInsets.all(8),
                  child: TextField(
                    controller: model.textController,
                    decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
              ],
            ),
          ),
```

```main_model.dart
class MainModel extends ChangeNotifier {
  String text = 'initial';
  final textController = TextEditingController();

  void incrementCounter() {
    text = textController.text;
    notifyListeners();
  }
}
```
サンプルアプリでは数字を数えていた部分をtextに変えてみました。
テキストフィールドのdecorationは[この記事](https://qiita.com/arrow/items/e9a8c9aac98a1181b92d)を参考にしました。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/935513/86d2b089-3c84-d25a-b243-f8086af184bf.gif" width = 200px>

#3. テキストフィールドの数字対応

こんな感じでテキストフィールドは実装できました。が、数字の入力だけ受け付けたいのでここからが本番です。

調べてみたらわかったこととして、
・キーボードを数字のみ表示させること
・テキストフィールドに数字のみ入力できるバリデーションを書けること(これがないとコピペとかで数字以外も入れれちゃう)

上記の二つが必要とのことでした。なんだ、簡単そう。

flutter textfield numberとかで検索したらたくさん出てきました。
以下の2行をTextFieldに入れてあげるだけで行けるみたいです。

```main.dart
                    keyboardType: TextInputType.number,
                    inputFormatters: <TextInputFormatter>[
                      FilteringTextInputFormatter.digitsOnly
                    ],
```

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/935513/b7078622-86ec-5dcc-c83d-7b8e35fe2a4f.gif" width = 200px>

いけた！！！
いけた！??
いけてない...


数字のキーボードも出てきたし、コピペでも数字以外の入力は受け付けないようにできたのですが、キーボードが閉じれません。。。
かなり不便


そう、Flutterでkeyboardタイプをnumberで指定すると完了ボタンがないのです。
調べてみたところ、こういうもんらしい。
流石に使い勝手が悪いので、いろいろ調べてみると[このパッケージ](https://pub.dev/packages/keyboard_actions)を使うとテキストフィールドをいろいろカスタマイズできるみたいです。こちらの力をお借りしましょう！

#4. 数字対応したテキストフィールドを閉じる

それで、パッケージのusageを見てたんですけど、例のごとくStatefulWidgetで書かれているので、ちょっとわかりづらかったけど、StatelessWidgetでも対応できるように実装していく。

```main.dart
class MyHomePage extends StatelessWidget {
  final FocusNode nodeText = FocusNode();
```
まず MyHomePageでFocusNodeをインスタンス化する。
サンプルではテキストフィールドの種類分だけ用意しているが、今回は数字を受け付けるテキストフィールドのみの実装なので、一つだけ宣言する。

```main.dart
                Container(
                  height: 200,
                  width: 200,
                  padding: EdgeInsets.all(8),
                  child: KeyboardActions(
                    config: buildConfig(context),
                    child: Center(
                      child: TextField(
                        controller: model.textController,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                        ),
                        keyboardType: TextInputType.number,
                        inputFormatters: <TextInputFormatter>[
                          FilteringTextInputFormatter.digitsOnly
                        ],
                        focusNode: nodeText,
                      ),
                    ),
                  ),
                ),
```
次にテキストフィールドをKeyboardActionsでラップする。
この時KeyboardActionsの親Widgetの大きさが決まってないといけないみたいなので、heightとwidthを明示的に指定。

最後にKeyboardActionsConfigを設定する。

```main.dart
  KeyboardActionsConfig buildConfig(BuildContext context) {
    return KeyboardActionsConfig(
      keyboardActionsPlatform: KeyboardActionsPlatform.ALL,
      keyboardBarColor: Colors.grey[200],
      nextFocus: true,
      actions: [
        KeyboardActionsItem(focusNode: nodeText, toolbarButtons: [
          (node) {
            return GestureDetector(
              onTap: () => node.unfocus(),
              child: Padding(
                padding: EdgeInsets.all(8.0),
                child: Row(
                  children: [
                    Text(
                      'Done',
                      style: TextStyle(
                          fontWeight: FontWeight.bold, color: Colors.blue),
                    ),
                    SizedBox(
                      width: 10,
                    )
                  ],
                ),
              ),
            );
          }
        ]),
      ],
    );
  }
```
サンプルでは閉じるボタンが×アイコンだったが、'Done'に書き換えている。

これで実装ができたので、挙動を確かめていく。

<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/935513/e26f32d0-245e-5f7f-47cd-6cce061a1c87.gif" width = 200px>


実装できました！！
今回使ったパッケージのkeyborad_actionsを使うと数字の他にも色々カスタマイズできるみたいなので、テキストフィールドを使いたい時は使うようにしてみてもいいかもしれませんね。

最後まで読んでいただきありがとうございます。本記事が皆様のお助けになれば幸いです。
それでは皆様良きFlutterLifeを！！