LoginSignup
7
9

More than 5 years have passed since last update.

StatefullWidgetに値を渡す

Posted at

Flutterで本格的なアプリを開発していると自作のStatefullWidgetに値を渡したいことが出てきます。
ある程度Flutterに慣れていると次のような実装方法にしようと考えるのではないでしょうか。

  1. StatefullWidgetに直接値は渡せないぽいなぁ
  2. そうだStatefullWidgetをインスタンス化しているbuildメソッドを呼ぶために、親でsetStateを呼び出してbuildメソッドをコールしてもらおう
  3. そうすればbuildメソッド内でStatefullWidgetのコンストラクタが呼ばれて再生成されるから設定したい値が反映されるはずだ

そう考えて実装したのが次のコードです。
EmployeeEditorというのが、今回取り上げる値を設定可能にしたいWidgetです。

// 従業員Entity
class Employee {
  int id;
  String name;

  Employee(this.id, this.name);
}

// Scaffold
class SampleScaffoldWidget extends StatefulWidget {
  @override
  State createState() => SampleScaffoldWidgetState();
}

// ScaffoldのState
class SampleScaffoldWidgetState extends State<SampleScaffoldWidget> {
  List<Employee> employees = List();
  Employee selectedEmployee;

  @override
  Widget build(BuildContext context) {
    print("SampleScaffoldWidgetState#build");
    return Scaffold(
        appBar: AppBar(
          title: Text("Employee Editor"),
        ),
        body: Column(
          children: <Widget>[
            Flexible(
              child: ListView.builder(
                  itemCount: employees.length,
                  itemBuilder: (context, i) {
                    Employee employee = employees[i];
                    return ListTile(
                      title: Text(employee.name),
                      onTap: () {
                        setState(() {
                          print("select:${employee.name}");
                          selectedEmployee = employee;
                        });
                      },
                    );
                  }),
            ),
            EmployeeEditor(
              employee: selectedEmployee,
              onSubmit: (Employee _employee) {
                print("onSubmit:${_employee.name}");
                employees.asMap().forEach((i, employee) {
                  if(_employee.id == employee.id){
                    setState(() {
                      employees[i] = _employee;
                    });
                  }
                });
              },
            )
          ],
        ));
  }

  @override
  void initState() {
    employees.add(Employee(1, "Alice"));
    employees.add(Employee(2, "Bob"));
    selectedEmployee = employees[0];
  }
}

// 従業員名を編集するためのWidget
// コンストラクタで編集対象のEmployeeを渡す
class EmployeeEditor extends StatefulWidget {
  Employee employee;
  EditEmployeeCallback onSubmit;

  EmployeeEditor({this.employee, this.onSubmit}) {
    print("EmployeeEditor#constructor");
  }

  @override
  State createState() {
    print("EmployeeEditor#createState");
    return EmployeeEditorState(employee: this.employee);
  }
}

// 従業員名を編集完了されたときのCallback
typedef void EditEmployeeCallback(Employee employee);

// 従業員名を編集するためのWidgetのState
class EmployeeEditorState extends State<EmployeeEditor> {
  Employee employee;

  EmployeeEditorState({this.employee}) {
    print("EmployeeEditorState#constructor");
    _controller = TextEditingController(text: employee.name);
  }

  TextEditingController _controller;

  @override
  Widget build(BuildContext context) {
    print("EmployeeEditorState#build");
    return Row(
      children: <Widget>[
        new Flexible(
          child: Container(
            margin: EdgeInsets.only(left: 8.0, right: 8.0),
            child: TextField(
              autofocus: true,
              controller: _controller,
              decoration: new InputDecoration.collapsed(hintText: "Name"),
            ),
          ),
        ),
        Container(
          child: FlatButton(
              onPressed: () {
                employee.name = _controller.text;
                widget.onSubmit(employee);
              },
              child: Text("Done")),
        )
      ],
    );
  }
}

ListTileをクリックすると、クリックされたEmployeeがselectedEmployeeにセットされるので、SampleScaffoldWidgetのbuildが実行されEmployeeEditorのインスタンが再生成されるので、
エディターに選択されたEmployeeがセットされると考えたわけです。

しかしこのプログラムを実行すると次のような動作になりました。
AliceをAlice2に編集すると意図したとおり、リストの名前も変更されます。
次にBobをクリックしたのですが、TextFieldに名前が反映されないという状況です。

ログです。
Bobをクリックしたあとは予想していたとおりEmployeeEditorのコンストラクタが呼び出され、EmployeeEditorState#buildも実行されています。
しかしどうやらEmployeeEditorStateが保持しているemployeeはAliceのインスタンスのようです。

Performing hot restart...
Restarted app in 1,657ms.
I/flutter ( 9936): SampleScaffoldWidgetState#build
I/flutter ( 9936): EmployeeEditor#constructor
I/flutter ( 9936): EmployeeEditor#createState
I/flutter ( 9936): EmployeeEditorState#constructor
I/flutter ( 9936): EmployeeEditorState#build
I/flutter ( 9936): onSubmit:Alice2
I/flutter ( 9936): SampleScaffoldWidgetState#build
I/flutter ( 9936): EmployeeEditor#constructor
I/flutter ( 9936): EmployeeEditorState#build
I/flutter ( 9936): select:Bob
I/flutter ( 9936): SampleScaffoldWidgetState#build
I/flutter ( 9936): EmployeeEditor#constructor
I/flutter ( 9936): EmployeeEditorState#build

いろいろググってみましたが、ピンとくる方法が見つかりませんでした。
ふとTextFieldのテキストの値を変更するのにTextEditingControllerとかを経由してたなと思いました。
少しおってみるとTextEditingControllerはValueNotifierを継承していました。
ValueNotifierのコードはこんなでした。

/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);

  /// The current value stored in this notifier.
  ///
  /// When the value is replaced, this class notifies its listeners.
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

値を一つ保持するChangeNotifierです。
値が置換されると、このクラスはリスナーにそれを通知する。
お、なんだかそれっぽいぞ。

つまりValueNotifierを経由して値の変更をEmployeeEditorStateに通知すればよいということか。
というわけで修正したのが次のコードです。

// 従業員Entity
class Employee {
  int id;
  String name;

  Employee(this.id, this.name);
}

// Scaffold
class SampleScaffoldWidget extends StatefulWidget {
  @override
  State createState() => SampleScaffoldWidgetState();
}

// ScaffoldのState
class SampleScaffoldWidgetState extends State<SampleScaffoldWidget> {
  List<Employee> employees = [
    Employee(1, "Alice"),
    Employee(2, "Bob"),
  ];
  ValueNotifier<Employee> employeeChangeNotifier;

  SampleScaffoldWidgetState() {
    employeeChangeNotifier = ValueNotifier<Employee>(employees[0]);
  }

  @override
  Widget build(BuildContext context) {
    print("SampleScaffoldWidgetState#build");
    return Scaffold(
        appBar: AppBar(
          title: Text("Employee Editor"),
        ),
        body: Column(
          children: <Widget>[
            Flexible(
              child: ListView.builder(
                  itemCount: employees.length,
                  itemBuilder: (context, i) {
                    Employee employee = employees[i];
                    return ListTile(
                      title: Text(employee.name),
                      onTap: () {
                        // setStateは不要
                        // notifierを経由してEmployeeEditStateに値の変更を通知
                        employeeChangeNotifier.value = employee;
                      },
                    );
                  }),
            ),
            EmployeeEditor(
              notifier: employeeChangeNotifier,
              onSubmit: (Employee _employee) {
                print("onSubmit:${_employee.name}");
                employees.asMap().forEach((i, employee) {
                  if (_employee.id == employee.id) {
                    setState(() {
                      employees[i] = _employee;
                    });
                  }
                });
              },
            )
          ],
        ));
  }
}

// 従業員名を編集するためのWidget
// コンストラクタで編集対象のEmployeeを渡す
class EmployeeEditor extends StatefulWidget {
  EditEmployeeCallback onSubmit;
  ValueNotifier<Employee> notifier;

  EmployeeEditor({this.onSubmit, this.notifier}) {
    print("EmployeeEditor#constructor");
  }

  @override
  State createState() {
    print("EmployeeEditor#createState");
    return EmployeeEditorState(notifier: notifier);
  }
}

// 従業員名を編集完了されたときのCallback
typedef void EditEmployeeCallback(Employee employee);

// 従業員名を編集するためのWidgetのState
class EmployeeEditorState extends State<EmployeeEditor> {
  Employee employee;
  ValueNotifier<Employee> notifier;

  EmployeeEditorState({this.notifier}) {
    print("EmployeeEditorState#constructor");

    notifier.addListener(() {
      // notifierを経由して値が変更されたらビューを更新する
      setState(() {
        this.employee = notifier.value;
        _controller.text = employee.name;
      });
    });

    // notifierから初期値を取得
    employee = this.notifier.value;
    _controller = TextEditingController(text: this.notifier.value.name);
  }

  TextEditingController _controller;

  @override
  Widget build(BuildContext context) {
    print("EmployeeEditorState#build");
    return Row(
      children: <Widget>[
        new Flexible(
          child: Container(
            margin: EdgeInsets.only(left: 8.0, right: 8.0),
            child: TextField(
              autofocus: true,
              controller: _controller,
              decoration: new InputDecoration.collapsed(hintText: "Name"),
            ),
          ),
        ),
        Container(
          child: FlatButton(
              onPressed: () {
                employee.name = _controller.text;
                widget.onSubmit(employee);
              },
              child: Text("Done")),
        )
      ],
    );
  }
}

動作は以下のようになりました。
意図したとおり、ListTileをタップすると選択されたEmployeeの名前がエディタに設定されて、DONEをクリックするとnameが更新されてリストにも変更が反映されるようになりました。

他にも方法があるかもしれませんが、今回実現できたのはこのような方法だったので紹介しました。
お作法的にもそんなに悪くないような気もします。

7
9
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
7
9