Counts the Clouds

FlutterのAutocompleteをCupertinoスタイルにする
2021.09.06

Flutter

austin-schmid-JCOlqpfxsKo-unsplash.jpg

FlutterのAutocompleteをCupertinoスタイルにする

全体のDartPadはこちら。

The user’s text input is received in a field built with the fieldViewBuilder parameter.

AutocompleteクラスのfieldViewBuilderでカスタマイズできそうなので、何を指定すればいいか見てみる。

fieldViewBuilderAutocompleteFieldViewBuilderクラスで、Widgetを返す関数のようだ。

typedef AutocompleteFieldViewBuilder = Widget Function(
  BuildContext context,
  TextEditingController textEditingController,
  FocusNode focusNode,
  VoidCallback onFieldSubmitted,
);

See also:

RawAutocompleteのサンプルを見てみると、TextFormFieldcontrollerfocusNodeあたりを渡して返してあげれば良さそう。

// ...
class AutocompleteBasicExample extends StatelessWidget {
  const AutocompleteBasicExample({Key? key}) : super(key: key);

  static const List<String> _options = <String>[
    'aardvark',
    'bobcat',
    'chameleon',
  ];

  @override
  Widget build(BuildContext context) { 
    return RawAutocomplete<String>(
      // ...
      fieldViewBuilder: (BuildContext context,
          TextEditingController textEditingController,
          FocusNode focusNode,
          VoidCallback onFieldSubmitted) {
        return TextFormField(
          controller: textEditingController,
          focusNode: focusNode,
          onFieldSubmitted: (String value) {
            onFieldSubmitted();
          },
        );
      },
      // ...
    );
  }
}

AutocompleteのサンプルでTextFormFieldCupertinoTextFieldに置き換える。

  // ...
  class AutocompleteBasicExample extends StatelessWidget {
    const AutocompleteBasicExample({Key? key}) : super(key: key);

    static const List<String> _kOptions = <String>[
      'aardvark',
      'bobcat',
      'chameleon',
    ];

    @override
    Widget build(BuildContext context) {
      return Autocomplete<String>(
+       fieldViewBuilder: (
+         BuildContext context,
+         TextEditingController textEditingController,
+         FocusNode focusNode,
+         VoidCallback onFieldSubmitted,
+       ) {
+         return CupertinoTextField(
+           controller: textEditingController,
+           focusNode: focusNode,
+         );
+       },
        optionsBuilder: (TextEditingValue textEditingValue) {
          if (textEditingValue.text == '') {
            return const Iterable<String>.empty();
          }
          return _kOptions.where((String option) {
            return option.contains(textEditingValue.text.toLowerCase());
          });
        },
        onSelected: (String selection) {
          print('You just selected $selection');
        },
      );
    }
  }

これで、TextFormFieldCupertinoTextFieldに置き換わり、入力内容に反応させることもできる。

MacのキーボードでSimulatorのTextFormFieldに入力するとStack Overflow

本題から外れるが、Simulatorの“I/O”→“Input”→“Send Keyboard Event to Device”、もしくは“I/O”→“Keyboard”→“Connect Hardware Keyboard”オプションを有効にすると、TextFormFieldへの入力がStack Overflowを吐くようになった。

flutter cleanやXCodeのアップデートを行ってみたが、改善しなかった。

══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
The following StackOverflowError was thrown during a platform message callback:
Stack Overflow

When the exception was thrown, this was the stack:
#0      Duration.toString (dart:core/duration.dart:259:3)
#1      _StringBase._interpolate (dart:core-patch/string_patch.dart:846:19)
#2      Duration.toString (dart:core/duration.dart:275:25)
#3      _StringBase._interpolate (dart:core-patch/string_patch.dart:846:19)
...
...
#5308   ChannelBuffers.push (dart:ui/channel_buffers.dart:329:17)
#5309   PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:544:22)
#5310   _dispatchPlatformMessage (dart:ui/hooks.dart:92:31)
(elided 2 frames from dart:async)
════════════════════════════════════════════════════════════════════════════════════════════════════

Another exception was thrown: Stack Overflow

一時的な対応として、上記の2つのオプションは無効にしてソフトウェアキーボードを使うようにした。

Autocompleteのオプションにモデルを使用する

Autocompleteのオプションがテキストであることは実用上はあまりなくて、だいたいオブジェクト的なものになるので、Autocompleteクラスの2番目のサンプルを参考に、モデルを渡せるようにする。まずモデルの定義。

class User {
  const User({
    required this.id,
    required this.name,
    required this.email,
  });

  final int id;
  final String name;
  final String email;

  @override
  String toString() {
    return '$name, $email';
  }
}

次に、StringクラスだったところをUserクラスに置き換える。displayStringForOptionを使うと、表示や返り値を制御できる。

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

  static const List<User> users = <User>[
    User(id: 1, name: 'Alpha', email: 'alpha@example.com'),
    User(id: 2, name: 'Bravo', email: 'bravo@example.com'),
    User(id: 3, name: 'Charlie', email: 'charlie@example.com'),
  ];

  @override
  Widget build(BuildContext context) {
    return Autocomplete<User>(
      displayStringForOption: (User option) => option.name,
      fieldViewBuilder: (
        BuildContext context,
        TextEditingController textEditingController,
        FocusNode focusNode,
        VoidCallback onFieldSubmitted,
      ) {
        return CupertinoTextField(
          controller: textEditingController,
          focusNode: focusNode,
        );
      },
      optionsBuilder: (TextEditingValue textEditingValue) {
        if (textEditingValue.text == '') {
          return const Iterable<User>.empty();
        }
        return users.where((User option) {
          return option
              .toString()
              .contains(textEditingValue.text.toLowerCase());
        });
      },
      onSelected: (User selection) {
        print('You just selected $selection.name');
      },
    );
  }
}

AutocompleteをCupertinoスタイルにすることができた。