Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/widgets/radio_group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ To know more about the Radio widget in Flutter, refer to the [official documenta
| autofocus | `bool` | True if this widget will be selected as the initial focus when no other node in its scope is currently focused. |
| useCheckmarkStyle | `bool` | Controls whether the radio displays in a checkbox style or the default iOS radio style. |
| useCupertinoCheckmarkStyle | `bool` | Controls whether the checkmark style is used in an iOS-style radio. |
| enabled | `bool` | Whether this radio is enabled for user interaction. |
| backgroundColor | `String` | The background color of the radio. |
| side | `StacBorderSide` | The border side of the radio. |
| innerRadius | `double` | The inner radius of the radio in logical pixels. |


## Example JSON
Expand Down
191 changes: 100 additions & 91 deletions packages/stac/lib/src/parsers/widgets/stac_radio/stac_radio_parser.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stac/src/parsers/foundation/borders/stac_border_side_parser.dart';
import 'package:stac/src/parsers/foundation/geometry/stac_visual_density_parser.dart';
import 'package:stac/src/parsers/foundation/interaction/stac_mouse_cursor_parser.dart';
import 'package:stac/src/parsers/foundation/layout/stac_material_tap_target_size_parser.dart';
import 'package:stac/src/parsers/widgets/stac_radio_group/stac_radio_group_scope.dart';
import 'package:stac/src/utils/color_utils.dart';
import 'package:stac_core/stac_core.dart';
import 'package:stac_framework/stac_framework.dart';
Expand All @@ -19,116 +19,125 @@ class StacRadioParser extends StacParser<StacRadio> {

@override
Widget parse(BuildContext context, StacRadio model) {
return _RadioWidget(
model: model,
radioGroupScope: StacRadioGroupScope.of(context),
);
return _RadioWidget(model: model);
}
}

class _RadioWidget extends StatelessWidget {
const _RadioWidget({
required this.radioGroupScope,
required this.model,
});
class _RadioWidget extends StatefulWidget {
const _RadioWidget({required this.model});

final StacRadioGroupScope? radioGroupScope;
final StacRadio model;

@override
Widget build(BuildContext context) {
final FocusNode focusNode = FocusNode();
State<_RadioWidget> createState() => _RadioWidgetState();
}

class _RadioWidgetState extends State<_RadioWidget> {
late final FocusNode _focusNode;

@override
void initState() {
super.initState();
_focusNode = FocusNode();
}

@override
void dispose() {
_focusNode.dispose();
super.dispose();
}

switch (model.radioType ?? StacRadioType.material) {
@override
Widget build(BuildContext context) {
switch (widget.model.radioType ?? StacRadioType.material) {
case StacRadioType.cupertino:
return _buildCupertinoRadio(context, model, focusNode);
return _buildCupertinoRadio(context);
case StacRadioType.adaptive:
return _buildAdaptiveRadio(context, model, focusNode);
return _buildAdaptiveRadio(context);
case StacRadioType.material:
return _buildMaterialRadio(context, model, focusNode);
return _buildMaterialRadio(context);
}
}

Widget _buildCupertinoRadio(
BuildContext context,
StacRadio model,
FocusNode focusNode,
) {
return ValueListenableBuilder(
valueListenable: radioGroupScope!.radioGroupValue,
builder: (context, value, child) {
return CupertinoRadio(
value: model.value,
mouseCursor: model.mouseCursor?.parse,
toggleable: model.toggleable ?? false,
activeColor: model.activeColor?.toColor(context),
inactiveColor: model.inactiveColor?.toColor(context),
fillColor: model.fillColor?.toColor(context),
focusColor: model.focusColor?.toColor(context),
focusNode: focusNode,
autofocus: model.autofocus ?? false,
useCheckmarkStyle: model.useCheckmarkStyle ?? false,
);
},
Widget _buildCupertinoRadio(BuildContext context) {
return CupertinoRadio<dynamic>(
value: widget.model.value,
mouseCursor: widget.model.mouseCursor?.parse,
toggleable: widget.model.toggleable ?? false,
activeColor: widget.model.activeColor?.toColor(context),
inactiveColor: widget.model.inactiveColor?.toColor(context),
fillColor: widget.model.fillColor?.toColor(context),
focusColor: widget.model.focusColor?.toColor(context),
focusNode: _focusNode,
autofocus: widget.model.autofocus ?? false,
useCheckmarkStyle: widget.model.useCheckmarkStyle ?? false,
enabled: widget.model.enabled,
);
}

Widget _buildAdaptiveRadio(
BuildContext context,
StacRadio model,
FocusNode focusNode,
) {
return ValueListenableBuilder(
valueListenable: radioGroupScope!.radioGroupValue,
builder: (context, value, child) {
return Radio.adaptive(
value: model.value,
mouseCursor: model.mouseCursor?.parse,
toggleable: model.toggleable ?? false,
activeColor: model.activeColor?.toColor(context),
fillColor: WidgetStateProperty.all(model.fillColor?.toColor(context)),
focusColor: model.focusColor?.toColor(context),
hoverColor: model.hoverColor?.toColor(context),
overlayColor: WidgetStateProperty.all(
model.overlayColor?.toColor(context),
),
splashRadius: model.splashRadius,
materialTapTargetSize: model.materialTapTargetSize?.parse,
visualDensity: model.visualDensity?.parse,
focusNode: focusNode,
autofocus: model.autofocus ?? false,
useCupertinoCheckmarkStyle: model.useCupertinoCheckmarkStyle ?? false,
);
},
Widget _buildAdaptiveRadio(BuildContext context) {
return Radio<dynamic>.adaptive(
value: widget.model.value,
mouseCursor: widget.model.mouseCursor?.parse,
toggleable: widget.model.toggleable ?? false,
activeColor: widget.model.activeColor?.toColor(context),
fillColor: WidgetStateProperty.all(
widget.model.fillColor?.toColor(context),
),
focusColor: widget.model.focusColor?.toColor(context),
hoverColor: widget.model.hoverColor?.toColor(context),
overlayColor: WidgetStateProperty.all(
widget.model.overlayColor?.toColor(context),
),
splashRadius: widget.model.splashRadius,
materialTapTargetSize: widget.model.materialTapTargetSize?.parse,
visualDensity: widget.model.visualDensity?.parse,
focusNode: _focusNode,
autofocus: widget.model.autofocus ?? false,
useCupertinoCheckmarkStyle:
widget.model.useCupertinoCheckmarkStyle ?? false,
enabled: widget.model.enabled,
backgroundColor: widget.model.backgroundColor != null
? WidgetStateProperty.all(
widget.model.backgroundColor!.toColor(context),
)
: null,
side: widget.model.side?.parse(context),
innerRadius: widget.model.innerRadius != null
? WidgetStateProperty.all(widget.model.innerRadius)
: null,
);
}

Widget _buildMaterialRadio(
BuildContext context,
StacRadio model,
FocusNode focusNode,
) {
return ValueListenableBuilder(
valueListenable: radioGroupScope!.radioGroupValue,
builder: (context, value, child) {
return Radio(
value: model.value,
mouseCursor: model.mouseCursor?.parse,
toggleable: model.toggleable ?? false,
activeColor: model.activeColor?.toColor(context),
fillColor: WidgetStateProperty.all(model.fillColor?.toColor(context)),
focusColor: model.focusColor?.toColor(context),
hoverColor: model.hoverColor?.toColor(context),
overlayColor: WidgetStateProperty.all(
model.overlayColor?.toColor(context),
),
splashRadius: model.splashRadius,
materialTapTargetSize: model.materialTapTargetSize?.parse,
visualDensity: model.visualDensity?.parse,
focusNode: focusNode,
autofocus: model.autofocus ?? false,
);
},
Widget _buildMaterialRadio(BuildContext context) {
return Radio<dynamic>(
value: widget.model.value,
mouseCursor: widget.model.mouseCursor?.parse,
toggleable: widget.model.toggleable ?? false,
activeColor: widget.model.activeColor?.toColor(context),
fillColor: WidgetStateProperty.all(
widget.model.fillColor?.toColor(context),
),
focusColor: widget.model.focusColor?.toColor(context),
hoverColor: widget.model.hoverColor?.toColor(context),
overlayColor: WidgetStateProperty.all(
widget.model.overlayColor?.toColor(context),
),
splashRadius: widget.model.splashRadius,
materialTapTargetSize: widget.model.materialTapTargetSize?.parse,
visualDensity: widget.model.visualDensity?.parse,
focusNode: _focusNode,
autofocus: widget.model.autofocus ?? false,
enabled: widget.model.enabled,
backgroundColor: widget.model.backgroundColor != null
? WidgetStateProperty.all(
widget.model.backgroundColor!.toColor(context),
)
: null,
side: widget.model.side?.parse(context),
innerRadius: widget.model.innerRadius != null
? WidgetStateProperty.all(widget.model.innerRadius)
: null,
);
Comment on lines +62 to 141
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pass the group wiring to every Radio.

CupertinoRadio, Radio.adaptive, and Radio all require groupValue and onChanged. After removing StacRadioGroupScope, these calls no longer compile and, even if they did, taps couldn’t update the group. Please fetch the current group wiring from the new RadioGroup wrapper and feed it into each constructor.

Apply this diff to restore the required wiring:

   @override
   Widget build(BuildContext context) {
-    switch (widget.model.radioType ?? StacRadioType.material) {
+    final radioGroup = RadioGroup.of<dynamic>(context);
+    switch (widget.model.radioType ?? StacRadioType.material) {
       case StacRadioType.cupertino:
-        return _buildCupertinoRadio(context);
+        return _buildCupertinoRadio(context, radioGroup?.groupValue,
+            radioGroup?.onChanged ?? (_) {});
       case StacRadioType.adaptive:
-        return _buildAdaptiveRadio(context);
+        return _buildAdaptiveRadio(context, radioGroup?.groupValue,
+            radioGroup?.onChanged ?? (_) {});
       case StacRadioType.material:
-        return _buildMaterialRadio(context);
+        return _buildMaterialRadio(context, radioGroup?.groupValue,
+            radioGroup?.onChanged ?? (_) {});
     }
   }

-  Widget _buildCupertinoRadio(BuildContext context) {
+  Widget _buildCupertinoRadio(
+    BuildContext context,
+    dynamic groupValue,
+    ValueChanged<dynamic> onChanged,
+  ) {
     return CupertinoRadio<dynamic>(
       value: widget.model.value,
+      groupValue: groupValue,
+      onChanged: onChanged,
       mouseCursor: widget.model.mouseCursor?.parse,
       toggleable: widget.model.toggleable ?? false,
       activeColor: widget.model.activeColor?.toColor(context),
       inactiveColor: widget.model.inactiveColor?.toColor(context),
       fillColor: widget.model.fillColor?.toColor(context),
       focusColor: widget.model.focusColor?.toColor(context),
       focusNode: _focusNode,
       autofocus: widget.model.autofocus ?? false,
       useCheckmarkStyle: widget.model.useCheckmarkStyle ?? false,
       enabled: widget.model.enabled,
     );
   }

-  Widget _buildAdaptiveRadio(BuildContext context) {
+  Widget _buildAdaptiveRadio(
+    BuildContext context,
+    dynamic groupValue,
+    ValueChanged<dynamic> onChanged,
+  ) {
     return Radio<dynamic>.adaptive(
       value: widget.model.value,
+      groupValue: groupValue,
+      onChanged: onChanged,
       mouseCursor: widget.model.mouseCursor?.parse,
       toggleable: widget.model.toggleable ?? false,
       activeColor: widget.model.activeColor?.toColor(context),
       fillColor: WidgetStateProperty.all(
         widget.model.fillColor?.toColor(context),
       ),
       focusColor: widget.model.focusColor?.toColor(context),
       hoverColor: widget.model.hoverColor?.toColor(context),
       overlayColor: WidgetStateProperty.all(
         widget.model.overlayColor?.toColor(context),
       ),
       splashRadius: widget.model.splashRadius,
       materialTapTargetSize: widget.model.materialTapTargetSize?.parse,
       visualDensity: widget.model.visualDensity?.parse,
       focusNode: _focusNode,
       autofocus: widget.model.autofocus ?? false,
       useCupertinoCheckmarkStyle:
           widget.model.useCupertinoCheckmarkStyle ?? false,
       enabled: widget.model.enabled,
       backgroundColor: widget.model.backgroundColor != null
           ? WidgetStateProperty.all(
               widget.model.backgroundColor!.toColor(context),
             )
           : null,
       side: widget.model.side?.parse(context),
       innerRadius: widget.model.innerRadius != null
           ? WidgetStateProperty.all(widget.model.innerRadius)
           : null,
     );
   }

-  Widget _buildMaterialRadio(BuildContext context) {
+  Widget _buildMaterialRadio(
+    BuildContext context,
+    dynamic groupValue,
+    ValueChanged<dynamic> onChanged,
+  ) {
     return Radio<dynamic>(
       value: widget.model.value,
+      groupValue: groupValue,
+      onChanged: onChanged,
       mouseCursor: widget.model.mouseCursor?.parse,
       toggleable: widget.model.toggleable ?? false,
       activeColor: widget.model.activeColor?.toColor(context),
       fillColor: WidgetStateProperty.all(
         widget.model.fillColor?.toColor(context),
       ),
       focusColor: widget.model.focusColor?.toColor(context),
       hoverColor: widget.model.hoverColor?.toColor(context),
       overlayColor: WidgetStateProperty.all(
         widget.model.overlayColor?.toColor(context),
       ),
       splashRadius: widget.model.splashRadius,
       materialTapTargetSize: widget.model.materialTapTargetSize?.parse,
       visualDensity: widget.model.visualDensity?.parse,
       focusNode: _focusNode,
       autofocus: widget.model.autofocus ?? false,
       enabled: widget.model.enabled,
       backgroundColor: widget.model.backgroundColor != null
           ? WidgetStateProperty.all(
               widget.model.backgroundColor!.toColor(context),
             )
           : null,
       side: widget.model.side?.parse(context),
       innerRadius: widget.model.innerRadius != null
           ? WidgetStateProperty.all(widget.model.innerRadius)
           : null,
     );
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/stac/lib/src/parsers/widgets/stac_radio/stac_radio_parser.dart
around lines 62 to 141, each Radio constructor is missing the required group
wiring (groupValue and onChanged) after removal of StacRadioGroupScope; fetch
the current group wiring from the new RadioGroup wrapper via
RadioGroup.of(context) and pass its groupValue and onChanged into
CupertinoRadio, Radio.adaptive, and Radio constructors (handle null safely so
existing enabled/toggleable behavior remains intact); ensure the RadioGroup
import is present and remove any stale StacRadioGroupScope references.

}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:stac/src/parsers/core/stac_action_parser.dart';
import 'package:stac/src/parsers/core/stac_widget_parser.dart';
import 'package:stac/src/parsers/widgets/stac_form/stac_form_scope.dart';
import 'package:stac/src/parsers/widgets/stac_radio_group/stac_radio_group_scope.dart';
import 'package:stac_core/stac_core.dart';
import 'package:stac_framework/stac_framework.dart';

Expand All @@ -18,63 +17,71 @@ class StacRadioGroupParser extends StacParser<StacRadioGroup> {

@override
Widget parse(BuildContext context, StacRadioGroup model) {
return _RadioGroupWidget(
model: model,
formScope: StacFormScope.of(context),
);
return _RadioGroupWidget(model, StacFormScope.of(context));
}
}

class _RadioGroupWidget extends StatefulWidget {
const _RadioGroupWidget({
required this.model,
required this.formScope,
});
const _RadioGroupWidget(this.model, this.formScope);

final StacRadioGroup model;
final StacFormScope? formScope;

@override
State<_RadioGroupWidget> createState() => __RadioGroupWidgetState();
State<_RadioGroupWidget> createState() => _RadioGroupWidgetState();
}

class __RadioGroupWidgetState extends State<_RadioGroupWidget> {
late ValueNotifier<dynamic> groupValue;
class _RadioGroupWidgetState extends State<_RadioGroupWidget> {
dynamic _groupValue;

@override
void initState() {
super.initState();
groupValue = ValueNotifier<dynamic>(widget.model.groupValue);
_saveValueInFormData();
setState(() {
_groupValue = widget.model.groupValue;
});

// Initialize form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
Comment on lines 37 to 48
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Remove setState() from initState().

Calling setState() in initState() is incorrect because the widget hasn't been built yet. The framework will automatically schedule a build after initState() completes, so wrapping the assignment in setState() is unnecessary and violates Flutter's lifecycle contract.

Apply this diff to fix the issue:

   @override
   void initState() {
     super.initState();
-    setState(() {
-      _groupValue = widget.model.groupValue;
-    });
+    _groupValue = widget.model.groupValue;

     // Initialize form data if id is provided
     if (widget.model.id != null && widget.formScope != null) {
       widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@override
void initState() {
super.initState();
groupValue = ValueNotifier<dynamic>(widget.model.groupValue);
_saveValueInFormData();
setState(() {
_groupValue = widget.model.groupValue;
});
// Initialize form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
@override
void initState() {
super.initState();
_groupValue = widget.model.groupValue;
// Initialize form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
🤖 Prompt for AI Agents
In
packages/stac/lib/src/parsers/widgets/stac_radio_group/stac_radio_group_parser.dart
around lines 37 to 48, remove the call to setState() inside initState():
directly assign _groupValue = widget.model.groupValue without wrapping it in
setState, leaving the subsequent formScope initialization unchanged (i.e., keep
checking widget.model.id and widget.formScope and assigning
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue).


@override
void dispose() {
groupValue.dispose();
super.dispose();
}
void didUpdateWidget(covariant _RadioGroupWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.model.groupValue != widget.model.groupValue) {
_groupValue = widget.model.groupValue;

void _updateGroupValue(dynamic value) {
groupValue.value = value;
_saveValueInFormData();
// Save to form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
}
Comment on lines 50 to 61
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Wrap _groupValue assignment in setState().

Line 52 updates _groupValue without calling setState(), so the widget won't rebuild when groupValue changes externally (e.g., during form reset). This causes the UI to display a stale selection.

Apply this diff to trigger a rebuild:

   void didUpdateWidget(covariant _RadioGroupWidget oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (oldWidget.model.groupValue != widget.model.groupValue) {
-      _groupValue = widget.model.groupValue;
+      setState(() {
+        _groupValue = widget.model.groupValue;
+      });
 
       // Save to form data if id is provided
       if (widget.model.id != null && widget.formScope != null) {
         widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
       }
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@override
void dispose() {
groupValue.dispose();
super.dispose();
}
void didUpdateWidget(covariant _RadioGroupWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.model.groupValue != widget.model.groupValue) {
_groupValue = widget.model.groupValue;
void _updateGroupValue(dynamic value) {
groupValue.value = value;
_saveValueInFormData();
// Save to form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
}
@override
void didUpdateWidget(covariant _RadioGroupWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.model.groupValue != widget.model.groupValue) {
setState(() {
_groupValue = widget.model.groupValue;
});
// Save to form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = widget.model.groupValue;
}
}
}
🤖 Prompt for AI Agents
In
packages/stac/lib/src/parsers/widgets/stac_radio_group/stac_radio_group_parser.dart
around lines 48 to 59, _groupValue is updated directly in didUpdateWidget which
prevents the widget from rebuilding when groupValue changes externally; wrap the
assignment (and related stateful updates such as saving to formScope.formData)
inside setState(() { ... }) so the framework schedules a rebuild and the UI
reflects the new selection. Ensure setState only encloses mutable state changes
and leave the super.didUpdateWidget call unchanged.


void _saveValueInFormData() {
if (widget.model.id != null) {
widget.formScope?.formData[widget.model.id!] = groupValue.value;
void _onChanged(dynamic value) {
setState(() {
_groupValue = value;
});

// Save to form data if id is provided
if (widget.model.id != null && widget.formScope != null) {
widget.formScope!.formData[widget.model.id!] = value;
}

// Call the onChanged action if provided
if (widget.model.onChanged != null) {
widget.model.onChanged!.parse(context);
}
}

@override
Widget build(BuildContext context) {
final StacRadioGroup model = widget.model;

return StacRadioGroupScope(
radioGroupValue: groupValue,
onSelect: _updateGroupValue,
child: Builder(builder: (context) {
return model.child?.parse(context) ?? const SizedBox();
}),
return RadioGroup<dynamic>(
groupValue: _groupValue,
onChanged: _onChanged,
child: widget.model.child?.parse(context) ?? const SizedBox.shrink(),
);
}
}

This file was deleted.

Loading