小部件的状态问题——从父级更新和从内部更新

Stateful issues with widgets -- update from parent and update from inside

这会太长,但我不知道问题在哪里is/what。

更新:如果我改用 StateLessWidget,这些值会正确跟踪;所以...

目的

At its core, I envisioned my TouchPoint to be a button that if tapped and dragged would pass the drag up to the scrollable container. If LongPressed, it would go into edit mode and handle a few different types of data; if OK is pressed on the dialog, it will call the parent() OnPressed function to pass the new value back out. If Cancel is pressed, then it should not call onpressed or update the label.

问题

When the dialogbox is opened, you can't really interact with it (I have print messages on the controls and I see that it was clicked, but the UI control in the dialog doesn't reflect the "new" state).

You can't tap and drag a button

代码

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'statusword_t.dart';

enum InputType { boolean, text, numeric, outconfig, sword }

class TouchPoint extends StatelessWidget {
  TouchPoint({
    Key? key,
    required this.title,
    required this.icon,
    required this.value,
    required this.enabled,
    required this.type,
    this.onlyButtons = false,
    this.warning,
    this.setter,
  }) : super(key: key);

  final String title;
  final String value;
  final Icon icon;
  final bool enabled;
  final InputType type;
  final bool onlyButtons;
  final Function(String)? setter;
  final String? warning;

  /*@override
  _TouchPointState createState() => _TouchPointState();
  */
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      //This is the screen buton
      style: ButtonStyle(
          backgroundColor: MaterialStateProperty.all<Color>(
              enabled ? Colors.blue : Colors.grey),
          foregroundColor: MaterialStateProperty.all(Colors.white)),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start, //ToDo: center?
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          icon,
          Column(
              //ToDo: Alignment here doesn't seem to do anything...
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                  title,
                  textAlign:
                      TextAlign.center, //ToDo: TextAlign doesn't work either...
                ),
                _uiControl(inEdit: false, type: type, value: value),
              ])
        ],
      ),
      onPressed: null, //ToDo: Pass click-drag to parent...
      onLongPress: !enabled
          ? null
          : () {
              //ToDo: Flyout setter widget here.
              print(
                  "Pressed: " + title + " [" + type.toString() + ": $value ]");

              _inputDialog(context: context, touchPoint: this).then((retval) {
                if (retval != null) {
                  //OK was pressed
                  print("Setter:" + retval);
                  if (setter != null) setter!(retval);
                }
              });
            },
    );
  }
}

TextEditingController _textFieldController = TextEditingController();

Widget _uiControl(
    {required bool inEdit,
    required InputType type,
    required String value,
    Function(String)? onPressed,
    bool? printme}) {
  if (printme != null)
    print("IE $inEdit, val: $value, " +
        ((onPressed == null) ? "nocall" : "wcall"));
  switch (type) {
    case InputType.numeric:
    case InputType.text:
      if (!inEdit)
        return Text(value);
      else {
        Widget textf = TextField(
          autofocus: true,
          keyboardType: //ToDo: Flutter Bug https://github.com/flutter/flutter/issues/58510
              (type == InputType.numeric)
                  ? TextInputType.number
                  : TextInputType.text,
          //ToDo: maxLength: maxlen,
          //ToDo: Min/Max value?
          controller: _textFieldController,
          decoration: InputDecoration(hintText: "zzBogusHint"),
        );

        //Set Initial Values (since it has to be done via the Controller)
        switch (type) {
          case InputType.numeric:
          case InputType.text:
            _textFieldController.text = value;
            _textFieldController.selection = TextSelection.fromPosition(
                TextPosition(offset: _textFieldController.text.length));
            break;
          default: //These aren't keyboards
            break;
        }
        return textf;
      }
    case InputType.boolean:
      return SizedBox(
          height: 16,
          width: 16,
          child: Checkbox(
            value: value == "1",
            onChanged: onPressed == null
                ? null
                : (val) {
                    onPressed(value);
                  },
          ));
    case InputType.outconfig:
      return !inEdit
          ? ToggleButtons(
              //We're gonna look like toggle buttons, but it's just a statement of what we are.
              disabledColor: Colors.white,
              color: Colors.white,
              disabledBorderColor: Colors.white,
              children: [
                int.parse(value) % 2 == 0 ? Text("NO") : Text("NC"),
              ],
              constraints: BoxConstraints(minHeight: 16, minWidth: 32),
              borderRadius: BorderRadius.circular(8.0),
              borderWidth: 1,
              isSelected: [true])
          : Row(
              children: [
                ToggleButtons(
                    children: [Text("NO"), Text("NC")],
                    constraints: BoxConstraints(minHeight: 16, minWidth: 32),
                    borderRadius: BorderRadius.circular(8.0),
                    borderWidth: 1,
                    isSelected: [
                      int.parse(value) % 2 == 0,
                      int.parse(value) % 2 == 1
                    ],
                    onPressed: onPressed == null
                        ? null
                        : (index) => onPressed(index.toString())),
              ],
            );
    case InputType.sword:
      return StatusWordWidget(int.parse(value),
          short: !inEdit, direction: !inEdit ? Axis.horizontal : Axis.vertical);
  }
}

//A flyout dialog for changing values
Future<String?> _inputDialog(
    {required BuildContext context, required TouchPoint touchPoint}) async {
  String myval = touchPoint.value;
  final dlg = showDialog<String>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text(touchPoint.title),
          content: Column(mainAxisSize: MainAxisSize.min, //Keep it small
              children: [
                if (touchPoint.warning != null) Text(touchPoint.warning!),
                if (!touchPoint.onlyButtons)
                  _uiControl(
                    printme: true,
                    inEdit: true,
                    type: touchPoint.type,
                    value: myval,
                    onPressed: (str) {
                      switch (touchPoint.type) {
                        case InputType.boolean:
                          myval = myval == "0" ? "1" : "0";
                          print("BoolUpdate: $myval");
                          break;
                        case InputType.outconfig:
                          int ival = int.parse(myval);
                          print("NoncB4: $str - $ival");

                          ival =
                              ((ival & 0xFF00) + ((ival & 0xFF) == 0 ? 1 : 0));
                          myval = ival.toString();
                          print("NoncAfter: $ival");
                          break;
                        default:
                      }
                    },
                  )
              ]),
          actions: <Widget>[
            FlatButton(
              color: Colors.red,
              textColor: Colors.white,
              child: Text('CANCEL'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
            FlatButton(
              color: Colors.green,
              textColor: Colors.white,
              child: Text('OK'),
              onPressed: () {
                //You have to get the value of the TextField from the Controller
                String retval;
                switch (touchPoint.type) {
                  case InputType.numeric:
                  case InputType.text:
                    retval = _textFieldController.text;
                    break;
                  default:
                    retval = myval;
                }
                Navigator.pop(context, retval);
              },
            ),
          ],
        );
      });

  return dlg;
}

真正的答案是我需要拆分它: 主要小部件是无状态的 - 因此它会自动更新。

“编辑对话框”是一个单独的 StateFulWidget - 因此您可以与其交互,但值是在创建时设置的。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'statusword_t.dart';

enum InputType { boolean, text, numeric, outconfig, sword }

class TouchPoint extends StatelessWidget {
  TouchPoint({
    Key? key,
    required this.title,
    required this.icon,
    required this.value,
    required this.enabled,
    required this.type,
    this.onlyButtons = false,
    this.warning,
    this.setter,
  }) : super(key: key);

  final String title;
  final String value;
  final Icon icon;
  final bool enabled;
  final InputType type;
  final bool onlyButtons;
  final Function(String)? setter;
  final String? warning;

  /*@override
  _TouchPointState createState() => _TouchPointState();
  */
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      //This is the screen buton
      style: ButtonStyle(
          backgroundColor: MaterialStateProperty.all<Color>(
              enabled ? Colors.blue : Colors.grey),
          foregroundColor: MaterialStateProperty.all(Colors.white)),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start, //ToDo: center?
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          icon,
          Column(
              //ToDo: Alignment here doesn't seem to do anything...
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                  title,
                  textAlign:
                      TextAlign.center, //ToDo: TextAlign doesn't work either...
                ),
                _uiControl(inEdit: false, type: type, value: value),
              ])
        ],
      ),
      onPressed: null, //ToDo: Pass click-drag to parent...
      onLongPress: !enabled
          ? null
          : () {
              //ToDo: Flyout setter widget here.

              _inputDialog(context: context, touchPoint: this).then((retval) {
                if (retval != null) {
                  //OK was pressed
                  if (setter != null) setter!(retval);
                }
              });
            },
    );
  }
}

TextEditingController _textFieldController = TextEditingController();

Widget _uiControl(
    {required bool inEdit,
    required InputType type,
    required String value,
    Function(String)? onPressed}) {
  switch (type) {
    case InputType.numeric:
    case InputType.text:
      if (!inEdit)
        return Text(value);
      else {
        Widget textf = TextField(
          autofocus: true,
          keyboardType: //ToDo: Flutter Bug https://github.com/flutter/flutter/issues/58510
              (type == InputType.numeric)
                  ? TextInputType.number
                  : TextInputType.text,
          //ToDo: maxLength: maxlen,
          //ToDo: Min/Max value?
          controller: _textFieldController,
          decoration: InputDecoration(hintText: "zzBogusHint"),
        );

        //Set Initial Values (since it has to be done via the Controller)
        switch (type) {
          case InputType.numeric:
          case InputType.text:
            _textFieldController.text = value;
            _textFieldController.selection = TextSelection.fromPosition(
                TextPosition(offset: _textFieldController.text.length));
            break;
          default: //These aren't keyboards
            break;
        }
        return textf;
      }
    case InputType.boolean:
      return SizedBox(
          height: 16,
          width: 16,
          child: Checkbox(
            value: value == "1",
            onChanged: onPressed == null
                ? null
                : (val) {
                    onPressed(value);
                  },
          ));
    case InputType.outconfig:
      return !inEdit
          ? ToggleButtons(
              //We're gonna look like toggle buttons, but it's just a statement of what we are.
              disabledColor: Colors.white,
              color: Colors.white,
              disabledBorderColor: Colors.white,
              children: [
                int.parse(value) % 2 == 0 ? Text("NO") : Text("NC"),
              ],
              constraints: BoxConstraints(minHeight: 16, minWidth: 32),
              borderRadius: BorderRadius.circular(8.0),
              borderWidth: 1,
              isSelected: [true])
          : Row(
              children: [
                ToggleButtons(
                    children: [Text("NO"), Text("NC")],
                    constraints: BoxConstraints(minHeight: 16, minWidth: 32),
                    borderRadius: BorderRadius.circular(8.0),
                    borderWidth: 1,
                    isSelected: [
                      int.parse(value) % 2 == 0,
                      int.parse(value) % 2 == 1
                    ],
                    onPressed: onPressed == null
                        ? null
                        : (index) => onPressed(index.toString())),
              ],
            );
    case InputType.sword:
      return StatusWordWidget(int.parse(value),
          short: !inEdit, direction: !inEdit ? Axis.horizontal : Axis.vertical);
  }
}

/* The dialog, since it changes the vaues, needs to be a stateful widget */
class _TouchPointDialog extends StatefulWidget {
  //ToDo: Pass in things here
  _TouchPointDialog({Key? key, required this.touchPoint}) : super(key: key);

  final TouchPoint touchPoint;
  @override
  _TouchPointDialogState createState() =>
      _TouchPointDialogState(touchPoint.value);
}

class _TouchPointDialogState extends State<_TouchPointDialog> {
  _TouchPointDialogState(this.myval);
  String myval;

  @override
  Widget build(BuildContext context) {
    final dlg = AlertDialog(
      title: Text(widget.touchPoint.title),
      content: Column(mainAxisSize: MainAxisSize.min, //Keep it small
          children: [
            if (widget.touchPoint.warning != null)
              Text(widget.touchPoint.warning!),
            if (!widget.touchPoint.onlyButtons)
              _uiControl(
                inEdit: true,
                type: widget.touchPoint.type,
                value: myval,
                onPressed: (str) {
                  setState(() {
                    switch (widget.touchPoint.type) {
                      case InputType.boolean:
                        myval = myval == "0" ? "1" : "0";
                        break;
                      case InputType.outconfig:
                        int ival = int.parse(myval);
                        ival = ((ival & 0xFF00) + ((ival & 0xFF) == 0 ? 1 : 0));
                        myval = ival.toString();
                        break;
                      default:
                    }
                  });
                },
              )
          ]),
      actions: <Widget>[
        FlatButton(
          color: Colors.red,
          textColor: Colors.white,
          child: Text('CANCEL'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        FlatButton(
          color: Colors.green,
          textColor: Colors.white,
          child: Text('OK'),
          onPressed: () {
            //You have to get the value of the TextField from the Controller
            String retval;
            switch (widget.touchPoint.type) {
              case InputType.numeric:
              case InputType.text:
                retval = _textFieldController.text;
                break;
              default:
                retval = myval;
            }
            Navigator.pop(context, retval);
          },
        ),
      ],
    );

    return dlg;
  }
}

//A flyout dialog for changing values
Future<String?> _inputDialog(
    {required BuildContext context, required TouchPoint touchPoint}) async {
  return showDialog(
      barrierDismissible: false,
      context: context,
      builder: (BuildContext context) {
        return _TouchPointDialog(touchPoint: touchPoint);
      });
}