パルカワ2

最近はFlutterをやっています

FlutterのStatefulWidgetの課題と解決方法

FlutterでStateをもたせるには色々あるが、基本はStatefulWidgetを利用する。

StatefulWidgetは以下のようにStateを持ち、Stateの中で値を持ったりsetしたりできる。

StatefulWidget class - widgets library - Dart API

class Bird extends StatefulWidget {
  const Bird({
    Key? key,
    this.color = const Color(0xFFFFE306),
    this.child,
  }) : super(key: key);

  final Color color;
  final Widget? child;

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

class _BirdState extends State<Bird> {
  double _size = 1.0;

  void grow() {
    setState(() { _size += 0.1; });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: widget.color,
      transform: Matrix4.diagonal3Values(_size, _size, 1.0),
      child: widget.child,
    );
  }
}

他にもinitStateやdisposeなどWidgetが生成されるときや破棄されるタイミングに呼び出されるCallbackなどがある。

詳しくはドキュメントを読んでもらうとしてそんなのがある。

ちなみに逆にStatelessWidgetはStateを持たずinitStateなどもない。

StatefulWidgetの課題とは

initStateなどがあると言ったが、例えばテキストを入力できる画面を作りたいときにTextEditingControllerをinitStateで作って、disposeで破棄する処理を書いたりする。

TextEditingController class - widgets library - Dart API

/// Flutter code sample for TextEditingController

// This example creates a [TextField] with a [TextEditingController] whose
// change listener forces the entered text to be lower case and keeps the
// cursor at the end of the input.

import 'package:flutter/material.dart';

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

/// This is the main application widget.
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final TextEditingController _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      final String text = _controller.text.toLowerCase();
      _controller.value = _controller.value.copyWith(
        text: text,
        selection:
            TextSelection(baseOffset: text.length, extentOffset: text.length),
        composing: TextRange.empty,
      );
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        padding: const EdgeInsets.all(6),
        child: TextFormField(
          controller: _controller,
          decoration: const InputDecoration(border: OutlineInputBorder()),
        ),
      ),
    );
  }
}

他にもScrollControllerだったりなんとかControllerがよくある。

そのたびに生成して破棄する処理を書く必要がある。

つまりTextEditingControllerに関する処理をinitStateやdisposeに書く必要があり関連するコードが一箇所にまとまらず分散してしまったり、何度も同じコードを書く必要がある。

解決方法1: mixinする

mixinする方法がある。

ただしmixinはTextEditingControllerが複数ある場合に対応出来ない。

解決方法2: Builderを用意する

TextEditingControllerBuilderみたいなWidgetを作って利用すればmixinの問題は解決するが、この方法はBuilderを使うのでbuilder外でControllerを使うことが難しくなる。

解決方法3: flutter_hooks

このように use*Controller すればいいだけになる。解決方法1, 2で起きるような問題は起きない。

flutter_hooks | Flutter Package

class Example extends HookWidget {
  const Example({Key key, required this.duration})
      : super(key: key);

  final Duration duration;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: duration);
    return Container();
  }
}

ただし、flutter_hooksはそれなりにデメリットもあると感じている。

  • HookWidgetを継承する形になり一般的なStatelessWidgetを継承する形ではない
  • useEffectなどを多用するとbuildメソッドが大きくなってしまう
  • use* は if の中で使う事はできないが今の所機械的に発見しにくい
  • Flutter公式が提供している機能ではない

イシューにはなっていないのか

イシューはある。

Static Metaprogrammingがあればいいみたいな話になっているがどうか

Reusing state logic is either too verbose or too difficult · Issue #51752 · flutter/flutter · GitHub

static metaprogramming自体導入されるかはまだ未定

language/intro.md at master · dart-lang/language · GitHub

hooksはFlutterに入らないのか

このイシューで色々と議論されたようですが今の所はなさそう。

Widget hooks · Issue #25280 · flutter/flutter · GitHub

まとめ

flutterh_ooksを使うのは良さげではあるのだが使う判断はまだ出来ておらずどうしたものかなーと思いながらStatefulWidgetを作っている。