0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter で並べ替え可能な ListView にテキストフィールドを入れて作るのに ReorderableListView を使ってちょっと戸惑った話

Posted at

これなに

Flutter で ListView 並べ替えしたいなあと思ってたところ、公式に ReorderableListView があるということで使ってみた。
しかし、実はそのままだと並べ替えた時に値がその場所から変わらなくて困ったので色々勉強したメモ。

遭遇した事象

なんでだよぉ、なんで入れ替えようとしているのに入れ替わらないんだよおおおおお
変わらないよお.gif

完成したコード

とりあえずコードコピペしたい人用にコードだけ置いておきます。

Sample 1: とりあえず並べ替えだけできるようにしたもの

変わったよお.gif

// Ref:
// https://api.flutter.dev/flutter/material/ReorderableListView-class.html
// https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html
// https://stackoverflow.com/questions/72371330/flutter-textfield-does-not-follow-listtiles-within-reorderablelistview
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: ReorderableListViewWithTextField(),
      ),
    );
  }
}

class Item {
  int? item;
  String? text;

  Item(this.item, this.text);
}

class ReorderableListViewWithTextField extends StatefulWidget {
  State<ReorderableListViewWithTextField> createState() =>
      _ReorderableListViewWithTextFieldState();
}

class _ReorderableListViewWithTextFieldState
    extends State<ReorderableListViewWithTextField> {
  final List<Item> _listItems =
      List<Item>.generate(50, (int index) => Item(index, ''));

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

    return ReorderableListView(
      padding: const EdgeInsets.symmetric(horizontal: 40),
      children: <Widget>[
        for (int index = 0; index < _listItems.length; index += 1)
          ListTile(
            key: ObjectKey(_listItems[index]),
            tileColor:
                _listItems[index].item!.isOdd ? oddItemColor : evenItemColor,
            title: Text(
                'Item ${_listItems[index].item}: ${_listItems[index].text}'),
            subtitle: TextFormField(
              initialValue: _listItems[index].text,
              onChanged: (String? value) {
                setState(() {
                  _listItems[index].text = value!;
                });
              },
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
              ),
            ),
          ),
      ],
      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final Item listItem = _listItems.removeAt(oldIndex);
          _listItems.insert(newIndex, listItem);
        });
      },
    );
  }
}

Sample 2: Dragger を好きなアイコンに変えてみたもの

カスタムしたよお.gif

// Ref:
// https://api.flutter.dev/flutter/material/ReorderableListView-class.html
// https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html
// https://stackoverflow.com/questions/72371330/flutter-textfield-does-not-follow-listtiles-within-reorderablelistview
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: ReorderableListViewWithTextFieldAndCustomDragger(),
      ),
    );
  }
}

class Item {
  int? item;
  String? text;

  Item(this.item, this.text);
}

class ReorderableListViewWithTextFieldAndCustomDragger extends StatefulWidget {
  State<ReorderableListViewWithTextFieldAndCustomDragger> createState() =>
      _ReorderableListViewWithTextFieldAndCustomDraggerState();
}

class _ReorderableListViewWithTextFieldAndCustomDraggerState
    extends State<ReorderableListViewWithTextFieldAndCustomDragger> {
  final List<Item> _listItems =
      List<Item>.generate(50, (int index) => Item(index, ''));

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

    return ReorderableListView(
      padding: const EdgeInsets.symmetric(horizontal: 40),
      buildDefaultDragHandles: false,
      children: <Widget>[
        for (int index = 0; index < _listItems.length; index += 1)
          ListTile(
            key: ObjectKey(_listItems[index]),
            tileColor:
                _listItems[index].item!.isOdd ? oddItemColor : evenItemColor,
            title: Text(
                'Item ${_listItems[index].item}: ${_listItems[index].text}'),
            subtitle: Row(children: [
              Expanded(
                  child: TextFormField(
                initialValue: _listItems[index].text,
                onChanged: (String? value) {
                  setState(() {
                    _listItems[index].text = value!;
                  });
                },
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                ),
              )),
              Padding(padding: const EdgeInsets.all(5.0)),
              SizedBox(
                  height: 40,
                  width: 40,
                  child: ReorderableDragStartListener(
                      index: index, child: const Icon(Icons.drag_handle))),
            ]),
          ),
      ],
      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final Item listItem = _listItems.removeAt(oldIndex);
          _listItems.insert(newIndex, listItem);
        });
      },
    );
  }
}

何を書いていたかのメモ

並べ替えには ReorderableListView を使うこと

以下の公式ページで紹介されているコードをコピペで使ってもらえばとりあえずは並べ替えができる ListView が作れます。

import 'dart:ui';

import 'package:flutter/material.dart';

/// Flutter code sample for [ReorderableListView].

void main() => runApp(const ReorderableApp());

class ReorderableApp extends StatelessWidget {
  const ReorderableApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('ReorderableListView Sample')),
        body: const ReorderableExample(),
      ),
    );
  }
}

class ReorderableExample extends StatefulWidget {
  const ReorderableExample({super.key});

  @override
  State<ReorderableExample> createState() => _ReorderableExampleState();
}

class _ReorderableExampleState extends State<ReorderableExample> {
  final List<int> _items = List<int>.generate(50, (int index) => index);

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.secondary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.secondary.withOpacity(0.15);
    final Color draggableItemColor = colorScheme.secondary;

    Widget proxyDecorator(
        Widget child, int index, Animation<double> animation) {
      return AnimatedBuilder(
        animation: animation,
        builder: (BuildContext context, Widget? child) {
          final double animValue = Curves.easeInOut.transform(animation.value);
          final double elevation = lerpDouble(0, 6, animValue)!;
          return Material(
            elevation: elevation,
            color: draggableItemColor,
            shadowColor: draggableItemColor,
            child: child,
          );
        },
        child: child,
      );
    }

    return ReorderableListView(
      padding: const EdgeInsets.symmetric(horizontal: 40),
      proxyDecorator: proxyDecorator,
      children: <Widget>[
        for (int index = 0; index < _items.length; index += 1)
          ListTile(
            key: Key('$index'),
            tileColor: _items[index].isOdd ? oddItemColor : evenItemColor,
            title: Text('Item ${_items[index]}'),
          ),
      ],
      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final int item = _items.removeAt(oldIndex);
          _items.insert(newIndex, item);
        });
      },
    );
  }
}

Sample 2 の buildDefaultDragHandles: falseReorderableDragStartListener

buildDefaultDragHandles に false を指定することでデフォルトのドラッグハンドルの機能を無効化しています。
代わりにアイコンを設定して、ReorderableDragStartListener で並べ替えできるようにしています。
buildDefaultDragHandles については公式ページでも紹介されています。

onReorder >> setState

ここでは並べ替えの処理を行っています。
配列になっている _items に対して動きがあった添字の列に対して .removeAt して、入れ替え先のところの添字に .insert しているというのがこのロジックのようです。

      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final int item = _items.removeAt(oldIndex);
          _items.insert(newIndex, item);
        });
      },

newIndex -= 1 で何が起きているか

以下のようになっています。ここ何が起きているか図で確認します。

          if (oldIndex < newIndex) {
            newIndex -= 1;
          }

例えば D を B と C の間に移動するだけなら、添字をいじる必要はないことは図で見るとわかりやすいかもです。
image.png

しかし、たとえば D を F と G に移動すると、3の添え字を持っていた列がいなくなったことで配列の添え字の番号が1つずつ減るという形になります。
それゆえ、もし oldIndex より newIndex が大きい場合、添え字を1つ減らすという作業が発生するということになります。
image.png

Item class

大きい Item という単位でしかステートを保持していなかったものを丸めて TextFormField の値と一緒に持っていけるようにまとめてくれている。以下のページがとても参考になった。

class Item {
  int? item;
  String? text;

  Item(this.item, this.text);
}

最後に

この ReorderableListView との戦いで多分3日間くらい使った気がする…
結構大変でした

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?