😇

[Flutter] 並び替えできて動的にTextFieldを追加, 削除できるUI

2021/07/05に公開

[Flutter]動的にTextFieldを追加, 削除できるUI](https://zenn.dev/soraef/articles/5f1bfb747a2414)の続き

動的にTextFieldを追加削除できて、更に並び替えできるようにしたくなった。
削除についてはTextFieldをスライドして出てくる削除ボタンから削除できるようにするためにflutter_slidableというライブラリを使用した。

Stateクラス

class TextFieldState {
  final String id;
  final TextEditingController controller;

  TextFieldState(this.id, this.controller);
}

WidgetでするStateのクラスを定義する。 前回の記事ではid, TextEditingController, textの3つのインスタンス変数を持っていた。
しかしTextEditingController自体がtextを保持しているので, 今回はtextは持たないようにした。

Controllerクラス

ReorderableMultiTextFieldControllerはWidgetにわたすコントローラー。
TextFieldを追加するadd, 削除するremoveメソッドに加えて, 並べ替えるreorderメソッドを定義した。

class ReorderableMultiTextFieldController
    extends ValueNotifier<List<TextFieldState>> {
  ReorderableMultiTextFieldController(List<TextFieldState> value)
      : super(value);

  void add(text) {
    final state = TextFieldState(
      Uuid().v4(),
      TextEditingController(text: text),
    );

    value = [...value, state];
  }

  void remove(String id) {
    final removedText = value.where((element) => element.id == id);
    if (removedText.isEmpty) {
      throw "Textがありません";
    }

    value = value.where((element) => element.id != id).toList();

    Future.delayed(Duration(seconds: 1)).then(
      (value) => removedText.first.controller.dispose(),
    );
  }

  void reorder(int oldIndex, int newIndex) {
    if (oldIndex < newIndex) {
      newIndex -= 1;
    }
    final item = value.removeAt(oldIndex);
    value = [...value..insert(newIndex, item)];
  }

  
  void dispose() {
    value.forEach((element) {
      element.controller.dispose();
    });
    super.dispose();
  }
}

Widgetクラス

WidgetはReorderableListViewとSlidableの組み合わせで書いた。
TextFieldの右にIcon(Icons.drag_handle)を表示させていて、ここで並び替えができる。
Slidableを設定していてスライドすると削除ボタンが出てくる。

class ReorderableMultiTextField extends StatelessWidget {
  final ReorderableMultiTextFieldController controller;
  const ReorderableMultiTextField({
    Key? key,
    required this.controller,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return ValueListenableBuilder<List<TextFieldState>>(
      valueListenable: controller,
      builder: (context, state, _) {
        return ReorderableListView(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          children: state
              .map(
                (textFieldState) => Slidable(
                  key: ValueKey(textFieldState.id),
                  actionPane: SlidableDrawerActionPane(),
                  secondaryActions: [
                    IconSlideAction(
                      color: Colors.red,
                      iconWidget: Text("delete"),
                      onTap: () => controller.remove(textFieldState.id),
                    ),
                  ],
                  child: Row(
                    children: [
                      Expanded(
                        child: TextField(
                          controller: textFieldState.controller,
                          decoration: InputDecoration.collapsed(hintText: ""),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                          bottom: 8.0,
                          top: 8.0,
                          left: 16,
                        ),
                        child: Icon(Icons.drag_handle),
                      ),
                    ],
                  ),
                ),
              )
              .toList(),
          onReorder: (oldIndex, newIndex) => controller.reorder(
            oldIndex,
            newIndex,
          ),
        );
      },
    );
  }
}

使い方

ReorderableMultiTextFieldControllerを初期化して, ReorderableMultiTextFieldに渡してあげるだけ。
追加ボタンはReorderableMultiTextFieldに含まれていないので、適当に追加してあげる。

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

  
  _SamplePageState createState() => _SamplePageState();
}

class _SamplePageState extends State<SamplePage> {
  late ReorderableMultiTextFieldController controller;

  
  void initState() {
    controller = ReorderableMultiTextFieldController([]);
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            Container(
              padding: EdgeInsets.all(32),
              child: ReorderableMultiTextField(
                controller: controller,
              ),
            ),
            TextButton(
              onPressed: () {
                controller.add("");
              },
              child: Text("追加"),
            )
          ],
        ),
      ),
    );
  }
}

Widget名長すぎ...

Discussion