Skip to content

Conversation

@chunhtai
Copy link
Contributor

@chunhtai chunhtai commented Apr 17, 2025

fixes #113562

This pr introduce a new widget RadioGroup

It does two things:

  1. maintain the group value and provide a simpler api to get the group value
  2. Provide semantics and keyboard navigation matching https://www.w3.org/WAI/ARIA/apg/patterns/radio/

If for some reason, one only wants to have (2), they can use RadioGroupPolicy.

Before this change developers have to implement

int? selection;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Radio<int>(
            value: 0,
            groupValue: selection,
            onChanged: (int? value) {
              setState(() {
                selection = value;
              });
            },
          ),
        Radio<int>(
            value: 1,
            groupValue: selection,
            onChanged: (int? value) {
              setState(() {
                selection = value;
              });
            },
          ),
      ],
    );
  }

After this change

  @override
  Widget build(BuildContext context) {
    return RadioGroup<int>(
      onChange: (int? value) => print(value)
      child: Column(
        children: <Widget>[
          Radio<int>(value: 0),
          Radio<int>(value: 1),
        ],
      ),
    );
  }

They also get keyboard behaviors match Accessibility standard

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: focus Focus traversal, gaining or losing focus labels Apr 17, 2025
@chunhtai
Copy link
Contributor Author

The pr right now only support material radio, or cupertino radio built as a result of Radio.adaptive. If we want to support CupertinoRadio, we have to move some of the Radio logic and RadioGroup to the widget layer. that is something that we can do as a follow up

Copy link
Contributor

@Piinks Piinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First impression looking at that code snippet ➡️ 💯
Really nice simplification of the API, like seriously.

IMO we should just deprecate the old way - unless is there a way someone could be using Radios that is not compatible with RadioGroup?
We won't be able to assert it is configured right until runtime, and the code is so much nicer in this new way you have designed - and more accessible! I don't think dart fix will be able to help here, so a migration guide would also be helpful.

Comment on lines +46 to +47
/// same value type. To use this widget, the [Radio.groupValue] must leave as
/// null. Otherwise, an assertion error is thrown.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// same value type. To use this widget, the [Radio.groupValue] must leave as
/// null. Otherwise, an assertion error is thrown.
/// same value type. To use this widget, the [Radio.groupValue] must remain
/// null, for the RadioGroup to manage. Otherwise, an assertion error is thrown.

/// same value type. To use this widget, the [Radio.groupValue] must leave as
/// null. Otherwise, an assertion error is thrown.
///
/// This widget internal builds a [RadioGroupPolicy] to provide an accessible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// This widget internal builds a [RadioGroupPolicy] to provide an accessible
/// This widget internally builds a [RadioGroupPolicy] to provide an accessible


/// A container that provides semantics and keyboard navigation.
///
/// This widget treats all [Radio]s in the subtree with the same generic type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it assert this statement?

required this.value,
required this.groupValue,
required this.onChanged,
this.groupValue,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we deprecate groupValue and really encourage folks to migrate to RadioGroup so that folks are using best practices for accessibility?
The fact that this also reduces verbosity is a huge win.

_group = context.dependOnInheritedWidgetOfExactType<_RadioGroupStateScope<T>>()?.state;
assert(
_group == null || widget.groupValue == null,
'A RadioGroup can not wrap Radio widget with a non null groupValue. '
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message was a little confusing, but we can revisit - I know you asked for early feedback.

const Radio({
super.key,
required this.value,
required this.groupValue,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For where we assert against the new and old API choice, I think some of them are not yet considering that both groupValue and onChanged are required for the old path - but I did not look too closely since this is just an initial pass. We can check later.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to think of edge cases:

  • What about programmatically selecting a radio button? Especially after the first build, where initialValue gets more complicated.
  • What if the developer wants to do some calculation at the time that a radio button is tapped before deciding whether to select it or not?
  • Are we sure that selecting multiple radio buttons is completely disallowed? Maybe that's a good thing actually.

Typically I like the purely declarative approach where the app developer controls which button is selected by calling setState (similar to React). The old way did follow this approach even if it was verbose and a little awkward. The new way does not if I'm not mistaken, because a radio button will automatically be checked when it is tapped, right? So my instincts make me wary that the new way will cause trouble in certain situations.

Maybe we can do a purely declarative RadioGroup and get the best of both worlds? Something like:

  int? selection;

  @override
  Widget build(BuildContext context) {
    return RadioGroup<int>(
      value: selection,
      onChange: (int? value) {
        setState(() {
          selection = value;
        });
      child: Column(
        children: <Widget>[
          Radio<int>(value: 0),
          Radio<int>(value: 1),
        ],
      ),
    );
  }

: super();

/// The initial selection on the [Radio] of the matching [Radio.value].
final T? initialGroupValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern makes me hesitate, even though I know we've done it elsewhere in the framework. I worry that there are cases where it gets confusing whether an initialValue should be applied or not, like rebuilding the widget, changing the initialValue after user interaction, moving the widget, etc.

@chunhtai
Copy link
Contributor Author

Maybe we can do a purely declarative RadioGroup and get the best of both worlds?

I like the idea, I do think this not only makes it more flexible but also simply some of the implementation. I will make the change.

Are we sure that selecting multiple radio buttons is completely disallowed? Maybe that's a good thing actually.

Not according to material guide, but if they really want to they can do something like

  child: RadioGroup<int>(
      onChange: (int? value) => print(value)
      child: Column(
        children: <Widget>[
          Radio<int>(value: 0),
          Radio<int>(value: 1),
          RadioGroup<int>(child: Radio<int>(value: 3)),
        ],
      ),
    );

so I think they can still achieve the same behavior. If this is a very common case, we can consider factor out a pure UI with minimal gesture RadioBase widget that basically just build a selected and unselected state without the value and groupvalue logic, which can be done in a separate pr.

@chunhtai
Copy link
Contributor Author

IMO we should just deprecate the old way - unless is there a way someone could be using Radios that is not compatible with RadioGroup?

If we go with declarative API @justinmc mentioned, I think they should be able to migrate for various use cases. I also can't find a reasonable use case that would prefer the old API. I agree we should deprecate it.

github-merge-queue bot pushed a commit that referenced this pull request May 1, 2025
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

This is in prepare for #167363

The goal for this refactoring is to make sure the new RadioGroup widget
can be used for both CupertinoRadio and Radio, and thus the widget has
to be in widget layer to avoid duplicating code.

Currently CupertinoRadio and Radio shares ToggleableStateMixin in widget
layer. but the mixin itself doesn't contains enough data to implement
radioGroup. Therefore, I moved more share logic from CupertinoRadio and
Radio to widget layer and created a RadioBase widget.

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@chunhtai chunhtai mentioned this pull request May 1, 2025
9 tasks
@chunhtai
Copy link
Contributor Author

chunhtai commented May 1, 2025

close in the favor of #168161

@chunhtai chunhtai closed this May 1, 2025
github-merge-queue bot pushed a commit that referenced this pull request Jun 2, 2025
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

previous #167363

I have to factor out abstract class for RadioGroupRegistry and
RadioClient which are subclassed by RadioGroupState and RawRadio
respectively.

I have to do this because the RadioListTile that has 2 focusnode one for
listTile and one for the radio it builds. The issue is that RadioGroup's
keyboard shortcut has to tightly couples with the focus node of each
radio, but the radioListtile has to mute the radio's focusnode so it can
act as one control under keyboard shortcut

https://github.com/flutter/flutter/blob/d582b358092d736c79b43a43e9b68b83a779cb6c/packages/flutter/lib/src/material/radio_list_tile.dart#L484

Therefore i abstract the out the logic of RadioGroup so that another
widget can participate by implementing the interface.

fixes #113562

migration guide: https://github.com/flutter/website/pull/12080/files

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
romanejaquez pushed a commit to romanejaquez/flutter that referenced this pull request Aug 14, 2025
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

This is in prepare for flutter#167363

The goal for this refactoring is to make sure the new RadioGroup widget
can be used for both CupertinoRadio and Radio, and thus the widget has
to be in widget layer to avoid duplicating code.

Currently CupertinoRadio and Radio shares ToggleableStateMixin in widget
layer. but the mixin itself doesn't contains enough data to implement
radioGroup. Therefore, I moved more share logic from CupertinoRadio and
Radio to widget layer and created a RadioBase widget.

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: focus Focus traversal, gaining or losing focus f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improper keyboard behavior of radio buttons

3 participants