1

I'm new to Flutter and I've hit my first roadblock while trying to make a custom switch. My switch should functionally work the same like the actual Switch from the material library, the only difference is the UI.

I'm using ValueNotifier and ValueListenableBuilder to update the switch value from another widget. Here's the relevant parts of my code:

Containing Widget

class ParentWidget extends StatefulWidget {
    @override
    _ParentWidget createState() => _ParentWidget();
}

class _ParentWidgetState extends State<ParentWidget> {
    ValueNotifier _switchNotifier = ValueNotifier(false);

    @override
    Widget build(BuildContext context) {
        return Container(
            child: ValueListenableBuilder(
                valueListenable: _switchNotifier,
                builder: (context, switchValue, child) {
                    return _buildSwitch(switchValue);
                },
            ),
        );
    }

    Widget _buildSwitch(bool switchValue) {
        return CustomSwitch(
            key: Key(value.toString()),
            initValue: switchValue,
            onChanged: (value) {
                setState(() {
                    _switchNotifier.value = value;
                });
            },
        );
    }
}

The widget that changes the switch value

class ChildWidget extends StatefulWidget {
    final ValueNotifier _switchNotifier;
    ChildWidget(this._switchNotifier);

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

class _ChildWidgetState extends State<ChildWidget> {
    @override
    Widget build(BuildContext context) {
        return Container(
            child: GestureDetector(
                onTap: () {
                    widget._switchNotifier.value = false;
                },
                child: Image(...),
            ),
        );
    }
}

Custom Switch

class CustomSwitch extends StatefulWidget {
    final ValueChanged<bool> onChanged;
    final bool initValue;
    final Key key;

    const CustomSwitch({
        @required this.key,
        this.onChanged,
        this.initValue,
    });

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

class _CustomSwitchState extends State<CustomSwitch> {
    bool value;

    @override
    void initState() {
        value = widget.initValue;
        super.initState();
    }

    @override
    Widget build(BuildContext context) {
        // the switch/toggle is animated just like the Material Switch
        return TheSwitch(...);
    }

    _toggle() {
        setState(() {
            value = !value;
            widget.onChanged(value);
        });
    }
}

If I'm calling _toggle() from the CustomSwitch the switch would toggle nicely with animation (I'm using AnimatedPositioned for the switch). This works fine if I'm only relying on user input, but I need to also programmatically toggle the switch and I feel like I'm missing something basic yet I'm stumped.

My current understanding is that the CustomSwitch would be rebuilt every time I change its value from ChildWidget because I'm using the switch value as a Key, but how to do it nicely with animation as if I'm calling _toggle() after it has been built?

Like in Java you'd usually do something like customSwitch.toggle().

bunbunn
  • 53
  • 8

1 Answers1

3

Majority of the flutter widgets use controllers to interact with widgets externally (TextFormField, ListView etc).

The most simple solution for your problem would also be to create a custom controller.

Firstly you'll create a controller class, which will have your custom widget's state as a parameter. This class will also expose your widget's methods.It would Look like this:

class CustomWidgetController{
  _CustomWidgetState _customWidgetState;

  void _addState(_CustomWidgetState customWidgetState){
    this._customWidgetState = customWidgetState;
  }

  /// Determine if the CustomWidgetController is attached to an instance
  /// of the CustomWidget (this property must return true before any other
  /// functions can be used)
  bool get isAttached => _customWidgetState != null;

  /// Here is the method you are exposing
  void toggle() {
    assert(isAttached, "CustomWidgetController must be attached to a CustomWidget");
    _customWidgetState.toggle();
  }
}

You'll need to accept the Custom controller as a parameter in your CustomWidget, and pass it to its state like this:

class CustomSwitch extends StatefulWidget {
    final CustomWidgetController customWidgetController;
    final bool initValue;
    final Key key;

    const CustomSwitch({
        @required this.key,
        this.customWidgetController,
        this.initValue,
    });

    @override
    _CustomSwitchState createState() => _CustomSwitchState(customWidgetController, initValue);
}

In your custom class's state, you'll assign you class's state to the controller using the addState method we created. You can do it the constructor like this:

class _CustomSwitchState extends State<CustomSwitch> {
    final CustomWidgetController _customWidgetController;
    bool value;

    _CustomSwitchState(this._customWidgetController, this.value) {
    if (_customWidgetController != null)
        _customWidgetController._addState(this);
    }

    @override
    Widget build(BuildContext context) {
        // the switch/toggle is animated just like the Material Switch
        return TheSwitch(...);
    }

    toggle() {
        setState(() {
            value = !value;
        });
    }
}

Now you can pass the controller and call methods using the controller in your parent widget like this:

  CustomWidgetController customWidgetController = new CustomWidgetController();

  @override
   Widget build(BuildContext context) {
        return CustomWidget(
            controller: customWidgetController,
            initValue: true
        );
    }

This is it! Now you can call customWidgetController.toggle() anywhere in your code to toggle the value! This is how native widgets let you interact with them.

OutOfnames
  • 39
  • 4