Repository: flutter-form-builder-ecosystem/flutter_form_builder

Issue:
[FormBuilderCheckboxGroup FormBuilderRadioGroup]: add decorators to items `itemDecorators` to be be able to box the choices
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Package/Plugin version

9.1.0

### What you'd like to happen

```Markdown
We want to make radio or check box cards with images and text surrounded by a box.

We actually want the ability to also put the checkbox / radio at top, middle or bottom when next to a stack of widgets.  Currently can only be centered on the left or right.
```


### Alternatives you've considered

```Markdown
Tried subclassing but the super call in `FormBuilderCheckboxGroup` and `FormBuilderRadioGroup` make it impossible to intercept or replace the `GroupedCheckBox` or `GroupedRadioButtion` invocations with a subclass.

The alternative is to create a custom component but that seems like for a different purpose
```


### Aditional information

![flutter-form-builder-inputDecoratin](https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/assets/1217160/585df56c-611b-4df6-a58b-f533e467dcc3)




Fix patch:
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 7abd2057d..ede785cd0 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,4 +1,5 @@
 import 'package:example/sources/conditional_fields.dart';
+import 'package:example/sources/decorated_radio_checkbox.dart';
 import 'package:example/sources/dynamic_fields.dart';
 import 'package:example/sources/related_fields.dart';
 import 'package:flutter/material.dart';
@@ -17,16 +18,19 @@ class MyApp extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return const MaterialApp(
+    return MaterialApp(
       title: 'Flutter FormBuilder Demo',
       debugShowCheckedModeBanner: false,
-      localizationsDelegates: [
+      localizationsDelegates: const [
         FormBuilderLocalizations.delegate,
         ...GlobalMaterialLocalizations.delegates,
         GlobalWidgetsLocalizations.delegate,
       ],
       supportedLocales: FormBuilderLocalizations.supportedLocales,
-      home: _HomePage(),
+      theme: ThemeData.light().copyWith(
+          appBarTheme: const AppBarTheme()
+              .copyWith(backgroundColor: Colors.blue.shade200)),
+      home: const _HomePage(),
     );
   }
 }
@@ -141,6 +145,23 @@ class _HomePage extends StatelessWidget {
               );
             },
           ),
+          const Divider(),
+          ListTile(
+            title: const Text('Radio Checkbox itemDecorator'),
+            trailing: const Icon(Icons.arrow_right_sharp),
+            onTap: () {
+              Navigator.of(context).push(
+                MaterialPageRoute(
+                  builder: (context) {
+                    return const CodePage(
+                      title: 'ItemDecorators',
+                      child: DecoratedRadioCheckbox(),
+                    );
+                  },
+                ),
+              );
+            },
+          ),
         ],
       ),
     );
diff --git a/example/lib/sources/decorated_radio_checkbox.dart b/example/lib/sources/decorated_radio_checkbox.dart
new file mode 100644
index 000000000..ecab94c34
--- /dev/null
+++ b/example/lib/sources/decorated_radio_checkbox.dart
@@ -0,0 +1,203 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_form_builder/flutter_form_builder.dart';
+
+/// Demonstrates the use of itemDecorators to wrap a box around selection items
+class DecoratedRadioCheckbox extends StatefulWidget {
+  const DecoratedRadioCheckbox({Key? key}) : super(key: key);
+
+  @override
+  State<DecoratedRadioCheckbox> createState() => _DecoratedRadioCheckboxState();
+}
+
+class _DecoratedRadioCheckboxState extends State<DecoratedRadioCheckbox> {
+  final _formKey = GlobalKey<FormBuilderState>();
+  int? option;
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+        child: FormBuilder(
+      key: _formKey,
+      child: Column(
+        children: <Widget>[
+          const SizedBox(height: 20),
+          // this text appears correctly if the textScaler <> 1.0
+          const Text(
+              'label:column of Widgets itemBorder:true orient:wrap wrapSpacing:5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderCheckboxGroup(
+            name: 'aCheckboxGroup1',
+            options: getDemoOptionsWidgets(),
+            wrapSpacing: 5.0,
+            itemDecoration: BoxDecoration(
+                color: Colors.orange.shade200,
+                border: Border.all(color: Colors.blue),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text(
+              'label:column of Widgets itemBorder:true orient:wrap wrapSpacing:5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderCheckboxGroup(
+            name: 'aCheckboxGroup2',
+            options: getDemoOptionsWidgets(),
+            wrapSpacing: 5.0,
+            controlAffinity: ControlAffinity.trailing,
+            itemDecoration: BoxDecoration(
+                color: Colors.amber.shade200,
+                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text(
+              'label:column of Widgets itemBorder:true orient:wrap wrapSpacing: 5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderRadioGroup(
+            name: 'aRadioGroup1',
+            options: getDemoOptionsWidgets(),
+            wrapSpacing: 5.0,
+            itemDecoration: BoxDecoration(
+                color: Colors.green.shade200,
+                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text('label:value itemBorder:true orient:wrap wrapSpacing:10.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderRadioGroup(
+            name: 'aRadioGroup2',
+            options: getDemoOptions(),
+            wrapSpacing: 10.0,
+            wrapRunSpacing: 10.0,
+            decoration: InputDecoration(
+                border: const OutlineInputBorder(),
+                contentPadding: const EdgeInsets.only(left: 20, top: 40),
+                labelText: 'hello there',
+                icon: const Icon(Icons.access_alarm_outlined),
+                fillColor: Colors.red.shade200),
+            itemDecoration: BoxDecoration(
+                color: Colors.blueGrey.shade200,
+                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text(
+              'itemDecoration:false label:value orient:wrap wrapSpacing:10.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderRadioGroup(
+            name: 'aRadioGroup3',
+            options: getDemoOptions(),
+            wrapSpacing: 10.0,
+          ),
+          const SizedBox(height: 20),
+          const Text('orient:horiz itemBorder:false wrapSpacing:5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderCheckboxGroup(
+            name: 'aCheckboxGroup3',
+            options: getDemoOptionsWidgets(),
+            wrapSpacing: 5.0,
+            orientation: OptionsOrientation.horizontal,
+            itemDecoration: BoxDecoration(
+                color: Colors.grey.shade300,
+//                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text('orient:vert itemBorder:true wrapSpacing:5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderCheckboxGroup(
+            name: 'aCheckboxGroup3',
+            options: getDemoOptionsWidgets(),
+            wrapSpacing: 5.0,
+            orientation: OptionsOrientation.vertical,
+            itemDecoration: BoxDecoration(
+                color: Colors.red.shade100,
+                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+          const SizedBox(height: 20),
+          const Text(
+              'label:w/sizebox orient:vert itemBorder:true wrapSpacing:5.0',
+              textScaler: TextScaler.linear(1.01)),
+          FormBuilderRadioGroup(
+            name: 'aRadioGroup4',
+            options: getDemoOptionsWidgets(forceMinWidth: 80.0),
+            wrapSpacing: 5.0,
+            orientation: OptionsOrientation.vertical,
+            itemDecoration: BoxDecoration(
+                color: Colors.lightBlue.shade100,
+                border: Border.all(color: Colors.blueAccent),
+                borderRadius: BorderRadius.circular(5.0)),
+          ),
+        ],
+      ),
+    ));
+  }
+
+  /// options using column of widgets for the label
+  /// We can force a min width by creating a sized box so we don't need another parameter
+  List<FormBuilderFieldOption> getDemoOptionsWidgets({forceMinWidth = 0.0}) {
+    return [
+      FormBuilderFieldOption(
+        value: "airplane",
+        child: Container(
+            padding: const EdgeInsets.all(5.0),
+            child: Column(
+              children: [
+                const Text("Airplane"),
+                const Icon(Icons.airplanemode_on),
+                SizedBox(width: forceMinWidth, height: 0.0),
+              ],
+            )),
+      ),
+      FormBuilderFieldOption(
+        value: "fire-truck",
+        child: Container(
+            padding: const EdgeInsets.all(5.0),
+            child: Column(children: [
+              const Text("Fire Truck"),
+              const Icon(Icons.fire_truck),
+              SizedBox(width: forceMinWidth, height: 0.0),
+            ])),
+      ),
+      FormBuilderFieldOption(
+        value: "bus-alert",
+        child: Container(
+            padding: const EdgeInsets.all(5.0),
+            child: Column(children: [
+              const Text("Bus Alert"),
+              const Icon(Icons.bus_alert),
+              SizedBox(width: forceMinWidth, height: 0.0),
+            ])),
+      ),
+      FormBuilderFieldOption(
+        value: "firetruck",
+        child: Container(
+            padding: const EdgeInsets.all(5.0),
+            child: Column(children: [
+              const Text("Motorcycle"),
+              const Icon(Icons.motorcycle),
+              SizedBox(width: forceMinWidth, height: 0.0),
+            ])),
+      ),
+    ];
+  }
+
+  /// opens using just values
+  List<FormBuilderFieldOption> getDemoOptions() {
+    return const [
+      FormBuilderFieldOption(
+        value: "airplane",
+      ),
+      FormBuilderFieldOption(
+        value: "fire-truck",
+      ),
+      FormBuilderFieldOption(
+        value: "bus-alert",
+      ),
+      FormBuilderFieldOption(
+        value: "firetruck",
+      ),
+    ];
+  }
+}
diff --git a/lib/src/fields/form_builder_checkbox_group.dart b/lib/src/fields/form_builder_checkbox_group.dart
index 4c7a0f861..7bd4c71d1 100644
--- a/lib/src/fields/form_builder_checkbox_group.dart
+++ b/lib/src/fields/form_builder_checkbox_group.dart
@@ -25,6 +25,11 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
   final ControlAffinity controlAffinity;
   final OptionsOrientation orientation;
 
+  /// A BoxDecoration that is added to each item if provided
+  /// WrapSpacing is reused for the the padding inside the itemDecoration
+  /// on the side opposite from the control
+  final BoxDecoration? itemDecoration;
+
   /// Creates a list of Checkboxes for selecting multiple options
   FormBuilderCheckboxGroup({
     super.key,
@@ -60,6 +65,7 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
     this.separator,
     this.controlAffinity = ControlAffinity.leading,
     this.orientation = OptionsOrientation.wrap,
+    this.itemDecoration,
   }) : super(
           builder: (FormFieldState<List<T>?> field) {
             final state = field as _FormBuilderCheckboxGroupState<T>;
@@ -93,6 +99,7 @@ class FormBuilderCheckboxGroup<T> extends FormBuilderFieldDecoration<List<T>> {
                 wrapVerticalDirection: wrapVerticalDirection,
                 separator: separator,
                 controlAffinity: controlAffinity,
+                itemDecoration: itemDecoration,
               ),
             );
           },
diff --git a/lib/src/fields/form_builder_radio_group.dart b/lib/src/fields/form_builder_radio_group.dart
index 7696480c2..0c6acb983 100644
--- a/lib/src/fields/form_builder_radio_group.dart
+++ b/lib/src/fields/form_builder_radio_group.dart
@@ -22,6 +22,11 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
   final WrapAlignment wrapRunAlignment;
   final WrapCrossAlignment wrapCrossAxisAlignment;
 
+  /// A BoxDecoration that is added to each item if provided
+  /// WrapSpacing is reused for the the padding inside the itemDecoration
+  /// on the side opposite from the control
+  final BoxDecoration? itemDecoration;
+
   /// Creates field to select one value from a list of Radio Widgets
   FormBuilderRadioGroup({
     super.autovalidateMode = AutovalidateMode.disabled,
@@ -54,6 +59,7 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
     super.valueTransformer,
     super.onReset,
     super.restorationId,
+    this.itemDecoration,
   }) : super(
           builder: (FormFieldState<T?> field) {
             final state = field as _FormBuilderRadioGroupState<T>;
@@ -84,6 +90,7 @@ class FormBuilderRadioGroup<T> extends FormBuilderFieldDecoration<T> {
                 wrapSpacing: wrapSpacing,
                 wrapTextDirection: wrapTextDirection,
                 wrapVerticalDirection: wrapVerticalDirection,
+                itemDecoration: itemDecoration,
               ),
             );
           },
diff --git a/lib/src/widgets/grouped_checkbox.dart b/lib/src/widgets/grouped_checkbox.dart
index 1cefe1802..31f977347 100644
--- a/lib/src/widgets/grouped_checkbox.dart
+++ b/lib/src/widgets/grouped_checkbox.dart
@@ -181,6 +181,12 @@ class GroupedCheckbox<T> extends StatelessWidget {
 
   final ControlAffinity controlAffinity;
 
+  /// A BoxDecoration that is added to each item if provided
+  /// [wrapSpacing] is used as inter-item bottom margin for [Orientation.vertical]
+  /// [wrapSpacing] is used as inter-item right margin for [Orientation.horizontal].
+  /// on the side opposite from the control
+  final BoxDecoration? itemDecoration;
+
   const GroupedCheckbox({
     super.key,
     required this.options,
@@ -205,13 +211,14 @@ class GroupedCheckbox<T> extends StatelessWidget {
     this.separator,
     this.controlAffinity = ControlAffinity.leading,
     this.visualDensity,
+    this.itemDecoration,
   });
 
   @override
   Widget build(BuildContext context) {
     final widgetList = <Widget>[];
     for (var i = 0; i < options.length; i++) {
-      widgetList.add(item(i));
+      widgetList.add(buildItem(i));
     }
     Widget finalWidget;
     if (orientation == OptionsOrientation.vertical) {
@@ -249,7 +256,8 @@ class GroupedCheckbox<T> extends StatelessWidget {
     return finalWidget;
   }
 
-  Widget item(int index) {
+  /// the composite of all the components for the option at index
+  Widget buildItem(int index) {
     final option = options[index];
     final optionValue = option.value;
     final isOptionDisabled = true == disabled?.contains(optionValue);
@@ -287,7 +295,7 @@ class GroupedCheckbox<T> extends StatelessWidget {
       child: option,
     );
 
-    return Row(
+    Widget compositeItem = Row(
       mainAxisSize: MainAxisSize.min,
       children: <Widget>[
         if (controlAffinity == ControlAffinity.leading) control,
@@ -296,5 +304,20 @@ class GroupedCheckbox<T> extends StatelessWidget {
         if (separator != null && index != options.length - 1) separator!,
       ],
     );
+
+    if (this.itemDecoration != null) {
+      compositeItem = Container(
+        decoration: this.itemDecoration,
+        margin: EdgeInsets.only(
+          bottom:
+              orientation == OptionsOrientation.vertical ? wrapSpacing : 0.0,
+          right:
+              orientation == OptionsOrientation.horizontal ? wrapSpacing : 0.0,
+        ),
+        child: compositeItem,
+      );
+    }
+
+    return compositeItem;
   }
 }
diff --git a/lib/src/widgets/grouped_radio.dart b/lib/src/widgets/grouped_radio.dart
index 16e0ab8b9..7e8e365a6 100644
--- a/lib/src/widgets/grouped_radio.dart
+++ b/lib/src/widgets/grouped_radio.dart
@@ -172,6 +172,12 @@ class GroupedRadio<T> extends StatefulWidget {
 
   final ControlAffinity controlAffinity;
 
+  /// A BoxDecoration that is added to each item if provided
+  /// [wrapSpacing] is used as inter-item bottom margin for [Orientation.vertical]
+  /// [wrapSpacing] is used as inter-item right margin for [Orientation.horizontal].
+  /// on the side opposite from the control
+  final BoxDecoration? itemDecoration;
+
   const GroupedRadio({
     super.key,
     required this.options,
@@ -193,6 +199,7 @@ class GroupedRadio<T> extends StatefulWidget {
     this.wrapVerticalDirection = VerticalDirection.down,
     this.separator,
     this.controlAffinity = ControlAffinity.leading,
+    this.itemDecoration,
   });
 
   @override
@@ -204,7 +211,7 @@ class _GroupedRadioState<T> extends State<GroupedRadio<T?>> {
   Widget build(BuildContext context) {
     final widgetList = <Widget>[];
     for (int i = 0; i < widget.options.length; i++) {
-      widgetList.add(_buildRadioButton(i));
+      widgetList.add(buildItem(i));
     }
 
     switch (widget.orientation) {
@@ -239,7 +246,8 @@ class _GroupedRadioState<T> extends State<GroupedRadio<T?>> {
     }
   }
 
-  Widget _buildRadioButton(int index) {
+  /// the composite of all the components for the option at index
+  Widget buildItem(int index) {
     final option = widget.options[index];
     final optionValue = option.value;
     final isOptionDisabled = true == widget.disabled?.contains(optionValue);
@@ -266,7 +274,7 @@ class _GroupedRadioState<T> extends State<GroupedRadio<T?>> {
       child: option,
     );
 
-    return Column(
+    Widget compositeItem = Column(
       mainAxisSize: MainAxisSize.min,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
@@ -288,5 +296,22 @@ class _GroupedRadioState<T> extends State<GroupedRadio<T?>> {
           widget.separator!,
       ],
     );
+
+    if (widget.itemDecoration != null) {
+      compositeItem = Container(
+        decoration: widget.itemDecoration,
+        margin: EdgeInsets.only(
+          bottom: widget.orientation == OptionsOrientation.vertical
+              ? widget.wrapSpacing
+              : 0.0,
+          right: widget.orientation == OptionsOrientation.horizontal
+              ? widget.wrapSpacing
+              : 0.0,
+        ),
+        child: compositeItem,
+      );
+    }
+
+    return compositeItem;
   }
 }


Test patch:
diff --git a/test/form_builder_checkbox_group_test.dart b/test/form_builder_checkbox_group_test.dart
index 20693a125..9982fe34f 100644
--- a/test/form_builder_checkbox_group_test.dart
+++ b/test/form_builder_checkbox_group_test.dart
@@ -28,6 +28,55 @@ void main() {
     expect(formSave(), isTrue);
     expect(formValue(widgetName), equals(const [1, 3]));
   });
+
+  testWidgets('FormBuilderCheckboxGroup -- decoration horizontal',
+      (WidgetTester tester) async {
+    const widgetName = 'cbg1';
+    final testWidget = FormBuilderCheckboxGroup<int>(
+      name: widgetName,
+      orientation: OptionsOrientation.horizontal,
+      wrapSpacing: 10.0,
+      options: const [
+        FormBuilderFieldOption(key: ValueKey('1'), value: 1),
+        FormBuilderFieldOption(key: ValueKey('2'), value: 2),
+      ],
+      itemDecoration:
+          BoxDecoration(border: Border.all(color: Colors.blueAccent)),
+    );
+    await tester.pumpWidget(buildTestableFieldWidget(testWidget));
+
+    // this is a brittle test knowing how we use container for a border
+    // there is one container for each option
+    expect(find.byType(Container), findsExactly(2));
+    // same as wrapSpacing
+    Container foo = tester.firstWidget(find.byType(Container));
+    expect(foo.margin, const EdgeInsets.only(right: 10.0));
+  });
+
+  testWidgets('FormBuilderCheckboxGroup -- decoration vertical',
+      (WidgetTester tester) async {
+    const widgetName = 'cbg1';
+    final testWidget = FormBuilderCheckboxGroup<int>(
+      name: widgetName,
+      orientation: OptionsOrientation.vertical,
+      wrapSpacing: 10.0,
+      options: const [
+        FormBuilderFieldOption(key: ValueKey('1'), value: 1),
+        FormBuilderFieldOption(key: ValueKey('2'), value: 2),
+      ],
+      itemDecoration:
+          BoxDecoration(border: Border.all(color: Colors.blueAccent)),
+    );
+    await tester.pumpWidget(buildTestableFieldWidget(testWidget));
+
+    // this is a brittle test knowing how we use container for a border
+    // there is one container for each option
+    expect(find.byType(Container), findsExactly(2));
+    // same as wrapSpacing
+    Container foo = tester.firstWidget(find.byType(Container));
+    expect(foo.margin, const EdgeInsets.only(bottom: 10.0));
+  });
+
   testWidgets('FormBuilderCheckboxGroup -- didChange',
       (WidgetTester tester) async {
     const fieldName = 'cbg1';
diff --git a/test/form_builder_radio_group_test.dart b/test/form_builder_radio_group_test.dart
index f39cf1278..80bfae791 100644
--- a/test/form_builder_radio_group_test.dart
+++ b/test/form_builder_radio_group_test.dart
@@ -27,4 +27,52 @@ void main() {
     expect(formSave(), isTrue);
     expect(formValue(widgetName), equals(3));
   });
+
+  testWidgets('FormBuilderRadioGroup -- decoration horizontal',
+      (WidgetTester tester) async {
+    const widgetName = 'rg1';
+    final testWidget = FormBuilderRadioGroup<int>(
+      name: widgetName,
+      orientation: OptionsOrientation.horizontal,
+      wrapSpacing: 10.0,
+      options: const [
+        FormBuilderFieldOption(key: ValueKey('1'), value: 1),
+        FormBuilderFieldOption(key: ValueKey('2'), value: 2),
+      ],
+      itemDecoration:
+          BoxDecoration(border: Border.all(color: Colors.blueAccent)),
+    );
+    await tester.pumpWidget(buildTestableFieldWidget(testWidget));
+
+    // this is a brittle test knowing how we use container for a border
+    // there is one container for each option
+    expect(find.byType(Container), findsExactly(2));
+    // same as wrapSpacing
+    Container foo = tester.firstWidget(find.byType(Container));
+    expect(foo.margin, const EdgeInsets.only(right: 10.0));
+  });
+
+  testWidgets('FormBuilderRadioGroup -- decoration vertical',
+      (WidgetTester tester) async {
+    const widgetName = 'rg1';
+    final testWidget = FormBuilderRadioGroup<int>(
+      name: widgetName,
+      orientation: OptionsOrientation.vertical,
+      wrapSpacing: 10.0,
+      options: const [
+        FormBuilderFieldOption(key: ValueKey('1'), value: 1),
+        FormBuilderFieldOption(key: ValueKey('2'), value: 2),
+      ],
+      itemDecoration:
+          BoxDecoration(border: Border.all(color: Colors.blueAccent)),
+    );
+    await tester.pumpWidget(buildTestableFieldWidget(testWidget));
+
+    // this is a brittle test knowing how we use container for a border
+    // there is one container for each option
+    expect(find.byType(Container), findsExactly(2));
+    // same as wrapSpacing
+    Container foo = tester.firstWidget(find.byType(Container));
+    expect(foo.margin, const EdgeInsets.only(bottom: 10.0));
+  });
 }