Best way to pass widgets to child widget in flutter

2k Views Asked by At

I'm new to flutter but I have a widget that wraps a custom painter. I am trying to get it to work so I can supply a Widget to this child widget's constructor and then use that widget as the child of the custom painter.

For example:

class MyPainterWrapper extends StatefulWidget {

  Widget _childWidget;

  SceneRender([this._childWidget]);

  @override
  State<StatefulWidget> createState() {
    return new MyPainterWrapperState(_childWidget);
  }
}

class MyPainterWrapperState extends State<SceneRender> {
Widget _childWidget;

MyPainterWrapperState(this._childWidget);

@override
  Widget build(BuildContext context) {
    return Column(
      children: [
          CustomPaint(painter: MyPainter(), child: _childWidget)
      ],
    );
  }
}

And in another widget (called testWidget):

  bool _answerCorrect = false;
  bool _answerInputted = false;
  var _msgController = TextEditingController();
  FocusNode _answerFieldFocus = new FocusNode();
  DictionaryEntry _currentEntry;

void _checkIfCorrect(String answerGiven) {
    setState(() {
      _answerCorrect = false;
      if (_currentEntry.Full_Word == answerGiven)
        _answerCorrect = true;
      else if (_currentEntry.dictReadings.isNotEmpty) {
        for (AlternateDictionaryEntryReading entryReading in _currentEntry
            .dictReadings) {
          if (entryReading.Alternate_Reading == answerGiven) {
            _answerCorrect = true;
            break;
          }
        }
      }
      _answerInputted = true;
      _msgController.clear();
    });

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('test'),
      ),
      body: MyPainterWrapper(Center(Container(Column(children: <Widget>[
              if (_answerCorrect && _answerInputted) Text('CORRECT!'),
              if (!_answerCorrect && _answerInputted) Text('WRONG:'),
              if (_answerInputted)
                  Text(_currentEntry.Full_Word),
              if (_answerInputted)
                  for(AlternateDictionaryEntryReading reading in _currentEntry.dictReadings)
                    Text(reading.Alternate_Reading),
              Container(
                constraints: BoxConstraints.expand(
                    height: 100,
                    width: 1000
                ),
                child: SingleChildScrollView(
                  child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    for (DictionaryTranslation translation in _currentEntry.dictTranslations)
                      Text(translation.Translation),
                  ],
                )
                ),
              ),
              Text('Enter Answer:',),
              TextField(
                controller: _msgController,
                focusNode: _answerFieldFocus,
                onSubmitted: (String value) {
                  _checkIfCorrect(value);
                  _answerFieldFocus.requestFocus();
                },
              )

This works to render the first time correctly, but any setState calls from checkIfCorrect from testWidget do not force the child widget to rebuild. I've tried testing it this way and it works, so that leads me to believe that I'm passing the widget incorrectly to have it redrawn via setState

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('test'),
      ),
      body: CustomPaint(painter: TestPainter(), child: Center(
          child: Container(...))


1

There are 1 best solutions below

0
Curt Eckhart On

Your MyPainterWrapperState class reads like you are creating a new _childWidget property inside your state (which has a default value of null). You are then using it to initialize a new instance of MyPainterWrapperState, then throwing away the instance of MyPainterWrapper() that you just created.

You're not actually using the stateful part of your stateful widget at all; You're just calling a method that returns something once.

That said, your approach is basically right, but the implementation is off a little.

My advice:

First, you can use named properties to supply constructor arguments to your class. I've made that change in the code snippet shown below.

The State class of a stateful widget supplies a widget property that should be used to reference the properties of the widget that created it. The State widget should also have a solitary initializer that accepts no arguments.

Also good to know is that the State class provides an initState() method that you can override to initialize any class local variables that you declare. This should be used to give a value to your _childWidget property.

Finally, anything you expect to be rebuilt should be inside the MyPainterWrapperState() class. Your SceneRender() method doesn't appear in your code, but you might want to move it into MyPainterWrapperState() if you expect the scene to change based on the value of the child.

I suggest these changes.

  • Pass arguments to MyPainterWrapper via named arguments.
  • Remove the argument to MyPainterWrapperState() and reference the child through the widget property supplied to the state.
  • Initialize _childWidget by overriding initState()
  • If SceneRender() does anything that depends on the value of _childWidget, move it to the build() method of MyPainterWrapperState().

The Flutter docs are great, and the Flutter team has created a ton of helpful YouTube videos that explain in a couple of minutes examples of how to use dozens of them. For a better understanding of StatefulWidget, you can read about it here.

If you make these changes, your solution would look something like the code snippet below.

Presuming you make those changes, you would alter your call to MyPainterWrapper() to use named properties.

Change this line

 body: MyPainterWrapper(Center(Container(Column(children: <Widget>[

To this

 body: MyPainterWrapper(child: Center(Container(Column(children: <Widget>[

This won't get you to done, but it will get you closer. I also haven't run this through a compiler, so there are probably errors in the snippet, but this should serve to illustrate the approach.

class MyPainterWrapper extends StatefulWidget {

  MyPainterWrapper(
    { 
      @required child: this.childWidget,
    }
  );
  final Widget childWidget;

  // Not sure what this does, but I'm pretty sure that it doesn't
  // provide anything into the widget tree.
  // If it mutates its arguments, then you might still need it.
  // SceneRender([this._childWidget]);

  @override
  State<StatefulWidget> createState() {
    // Note no arguments.
    return new MyPainterWrapperState();
  }
}

class MyPainterWrapperState extends State<MyPainterWrapper> {

// This is an uninitialized variable inside this class.
Widget _childWidget;

// MyPainterWrapperState(this._childWidget);

// Initialize state variables here.
@override initState() {
  // Assigns the widget class initializer to your state variable.
  _childWidget = widget.childWidget;
}


@override
  Widget build(BuildContext context) {
    return Column(
      children: [
          CustomPaint(painter: MyPainter(), child: _childWidget)
      ],
    );
  }
}```