0

I am trying to create my own custom segment in flutter. That segment has two buttons, one for teachers and other for students. What I am trying to do, it's encapsulate the buttons in one Stateful Widget to handle the setState of both buttons, because I want the buttons to be an AnimatedContainer and if I rebuild the childrens (the buttons) from the parent the transition doesn't works.

Note that the buttons are Stack positioned and I reorder the content to get the tapped button over the other (that will has effect when I set more width in the tapped button, now this is not created yet).

Here is my code:

import 'package:flutter/cupertino.dart';

import '../../app_localizations.dart';
import '../../styles.dart';

GlobalKey<_ButtonState> teachersButtonKey = GlobalKey();
GlobalKey<_ButtonState> studentsButtonKey = GlobalKey();
String _globalTappedButtonId = 'teachersButton';

class FiltersAppBarSegment extends StatefulWidget {
  @override
  _FiltersAppBarSegmentState createState() => _FiltersAppBarSegmentState();
}

class _FiltersAppBarSegmentState extends State<FiltersAppBarSegment> {
  List<Widget> buildStackChildren(SegmentChangedCallBack handleSegmentChanged) {
    if (_globalTappedButtonId == 'teachersButton') {
      return <Widget>[
        Container(
          key: UniqueKey(),
          child: _Button(
            key: studentsButtonKey,
            id: 'studentsButton',
            label: 'seeStudents',
            rightPosition: 1,
            onSegmentChanged: handleSegmentChanged,
          ),
        ),
        Container(
          key: UniqueKey(),
          child: _Button(
            key: teachersButtonKey,
            id: 'teachersButton',
            label: 'amTeacher',
            rightPosition: null,
            onSegmentChanged: handleSegmentChanged,
          ),
        ),
      ];
    } else {
      return <Widget>[
        Container(
          key: UniqueKey(),
          child: _Button(
            key: driverButtonKey,
            id: 'driverButton',
            label: 'amDriver',
            rightPosition: null,
            onSegmentChanged: handleSegmentChanged,
          ),
        ),
        Container(
          key: UniqueKey(),
          child: _Button(
            key: studentsButtonKey,
            id: 'studentButton',
            label: 'amStudent',
            rightPosition: 1,
            onSegmentChanged: handleSegmentChanged,
          ),
        ),
      ];
    }
  }

  void handleSegmentChanged(String clickedButtonId) {
    teachersButtonKey.currentState._handleButtonTapped();
    studentsButtonKey.currentState._handleButtonTapped();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 42,
      padding: EdgeInsets.symmetric(horizontal: 20),
      child: Stack(children: buildStackChildren(handleSegmentChanged)),
    );
  }
}

class _Button extends StatefulWidget {
  final String id;
  final String label;
  final double rightPosition;
  final void onSegmentChanged;

  _Button({
    Key key,
    this.id,
    this.label,
    this.rightPosition,
    this.onSegmentChanged,
  }) : super(key: key);

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

class _ButtonState extends State<_Button> {
  bool _tapped;
  double _topPosition;
  double _width;
  double _height;

  double _getTopPosition() => _tapped ? 0 : 5;
  double _getHeight() => _tapped ? 42 : 32;

  Gradient _getGradient() {
    if (_tapped) {
      return Styles.darkAccentColorGradient;
    } else {
      return Styles.darkAccentColorGradientDisabled;
    }
  }

  void _handleButtonTapped() {
    setState(() {
      _globalTappedButtonId = widget.id;
      _tapped = (widget.id == _globalTappedButtonId);
      _topPosition = _getTopPosition();
      _height = _getHeight();
    });
  }

  @override
  void initState() {
    super.initState();

    _tapped = (widget.id == _globalTappedButtonId);
    _topPosition = _getTopPosition();
    _height = _getHeight();
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: _topPosition,
      right: widget.rightPosition,
      child: GestureDetector(
        onTap: () {
          widget.onSegmentChanged('test');
        },
        child: AnimatedContainer(
          duration: Duration(seconds: 1),
          curve: Curves.fastOutSlowIn,
          width: _width,
          height: _height,
          decoration: BoxDecoration(
            gradient: _getGradient(),
            borderRadius: BorderRadius.circular(13),
          ),
          child: Center(
            child: Text(
              AppLocalizations.of(context).translate(widget.label),
              style: Styles.bodyWhiteText,
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );
  }
}

1 Answers1

0

I'm sure you have already found a solution to your problem by now, but this question is one of the first search results when looking at this error.

As you already know, per the Flutter doc on GlobalKey:

"You cannot simultaneously include two widgets in the tree with the same global key. Attempting to do so will assert at runtime."

You can define your own individual keys like:

import 'package:flutter/widgets.dart';

class TestKeys{
  static final testKey1 = const Key('__TESTKEY1__');
  static final testKey2 = const Key('__TESTKEY2__');
  ...
}

And then reference them in the widget with key: TestKeys.testKey1

This was described in this question here so perhaps it can help someone with the need for a similar use case.

There are also a few solutions listed in this GitHub issue

Sludge
  • 6,072
  • 5
  • 31
  • 43
  • but how will you be able to get hold of the current state? Key does not have a currentState property – chitgoks Apr 01 '21 at 06:16