diff --git a/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart b/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart index 905d8d508..be557b21b 100644 --- a/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart +++ b/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart @@ -109,6 +109,7 @@ final checkboxFilterChipsInput = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final checkboxFilterChipsData = _CheckboxFilterChipsInputData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/date_input_chip.dart b/examples/travel_app/lib/src/catalog/date_input_chip.dart index 30627dad0..51b7bbda4 100644 --- a/examples/travel_app/lib/src/catalog/date_input_chip.dart +++ b/examples/travel_app/lib/src/catalog/date_input_chip.dart @@ -136,6 +136,7 @@ final dateInputChip = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final datePickerData = _DatePickerData.fromMap(data as JsonMap); final notifier = dataContext.subscribeToString(datePickerData.value); diff --git a/examples/travel_app/lib/src/catalog/information_card.dart b/examples/travel_app/lib/src/catalog/information_card.dart index 5f41aec2d..ac4486b1e 100644 --- a/examples/travel_app/lib/src/catalog/information_card.dart +++ b/examples/travel_app/lib/src/catalog/information_card.dart @@ -90,6 +90,7 @@ final informationCard = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final cardData = _InformationCardData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/input_group.dart b/examples/travel_app/lib/src/catalog/input_group.dart index 1c9fa82b6..213cf9699 100644 --- a/examples/travel_app/lib/src/catalog/input_group.dart +++ b/examples/travel_app/lib/src/catalog/input_group.dart @@ -126,6 +126,7 @@ final inputGroup = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final inputGroupData = _InputGroupData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/itinerary.dart b/examples/travel_app/lib/src/catalog/itinerary.dart index 6d53cfd5a..6cac200df 100644 --- a/examples/travel_app/lib/src/catalog/itinerary.dart +++ b/examples/travel_app/lib/src/catalog/itinerary.dart @@ -227,6 +227,7 @@ final itinerary = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final itineraryData = _ItineraryData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/listings_booker.dart b/examples/travel_app/lib/src/catalog/listings_booker.dart index aaa3afc78..f82034c93 100644 --- a/examples/travel_app/lib/src/catalog/listings_booker.dart +++ b/examples/travel_app/lib/src/catalog/listings_booker.dart @@ -60,6 +60,7 @@ final listingsBooker = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final listingsBookerData = _ListingsBookerData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart b/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart index 0b0a22945..b500f2054 100644 --- a/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart +++ b/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart @@ -101,6 +101,7 @@ final optionsFilterChipInput = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final optionsFilterChipData = _OptionsFilterChipInputData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/tabbed_sections.dart b/examples/travel_app/lib/src/catalog/tabbed_sections.dart index 0039a4d3d..26f7bcf13 100644 --- a/examples/travel_app/lib/src/catalog/tabbed_sections.dart +++ b/examples/travel_app/lib/src/catalog/tabbed_sections.dart @@ -110,6 +110,7 @@ final tabbedSections = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final tabbedSectionsData = _TabbedSectionsData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/text_input_chip.dart b/examples/travel_app/lib/src/catalog/text_input_chip.dart index e2163b2c7..d4cb9f92b 100644 --- a/examples/travel_app/lib/src/catalog/text_input_chip.dart +++ b/examples/travel_app/lib/src/catalog/text_input_chip.dart @@ -79,6 +79,7 @@ final textInputChip = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final textInputChipData = _TextInputChipData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/trailhead.dart b/examples/travel_app/lib/src/catalog/trailhead.dart index fec211523..b2e117f56 100644 --- a/examples/travel_app/lib/src/catalog/trailhead.dart +++ b/examples/travel_app/lib/src/catalog/trailhead.dart @@ -78,6 +78,7 @@ final trailhead = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final trailheadData = _TrailheadData.fromMap( data as Map, diff --git a/examples/travel_app/lib/src/catalog/travel_carousel.dart b/examples/travel_app/lib/src/catalog/travel_carousel.dart index 233935dff..0aebe5e10 100644 --- a/examples/travel_app/lib/src/catalog/travel_carousel.dart +++ b/examples/travel_app/lib/src/catalog/travel_carousel.dart @@ -73,6 +73,7 @@ final travelCarousel = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final carouselData = _TravelCarouselData.fromMap( (data as Map).cast(), diff --git a/examples/travel_app/test/checkbox_filter_chips_input_test.dart b/examples/travel_app/test/checkbox_filter_chips_input_test.dart index 5249d78fd..8d5f48117 100644 --- a/examples/travel_app/test/checkbox_filter_chips_input_test.dart +++ b/examples/travel_app/test/checkbox_filter_chips_input_test.dart @@ -31,6 +31,7 @@ void main() { dispatchEvent: (_) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ), ); }, diff --git a/examples/travel_app/test/date_input_chip_test.dart b/examples/travel_app/test/date_input_chip_test.dart index 212ff1a1f..618398472 100644 --- a/examples/travel_app/test/date_input_chip_test.dart +++ b/examples/travel_app/test/date_input_chip_test.dart @@ -28,6 +28,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), @@ -59,6 +60,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), @@ -95,6 +97,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), @@ -132,6 +135,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/examples/travel_app/test/input_group_test.dart b/examples/travel_app/test/input_group_test.dart index b79fdb896..63e57b729 100644 --- a/examples/travel_app/test/input_group_test.dart +++ b/examples/travel_app/test/input_group_test.dart @@ -35,6 +35,7 @@ void main() { }, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), @@ -78,6 +79,7 @@ void main() { dispatchEvent: (UiEvent _) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/examples/travel_app/test/itinerary_test.dart b/examples/travel_app/test/itinerary_test.dart index 0039cb8d5..157c44610 100644 --- a/examples/travel_app/test/itinerary_test.dart +++ b/examples/travel_app/test/itinerary_test.dart @@ -53,6 +53,7 @@ void main() { dispatchEvent: mockDispatchEvent, context: tester.element(find.byType(Container)), dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); // 2. Pump the widget diff --git a/examples/travel_app/test/options_filter_chip_input_test.dart b/examples/travel_app/test/options_filter_chip_input_test.dart index 95cccf368..650a9a106 100644 --- a/examples/travel_app/test/options_filter_chip_input_test.dart +++ b/examples/travel_app/test/options_filter_chip_input_test.dart @@ -32,6 +32,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), @@ -90,6 +91,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/examples/travel_app/test/tabbed_sections_test.dart b/examples/travel_app/test/tabbed_sections_test.dart index 08dbaa93b..c77bfa6cb 100644 --- a/examples/travel_app/test/tabbed_sections_test.dart +++ b/examples/travel_app/test/tabbed_sections_test.dart @@ -53,6 +53,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/examples/travel_app/test/trailhead_test.dart b/examples/travel_app/test/trailhead_test.dart index 9d1a6e360..a8edd7fc6 100644 --- a/examples/travel_app/test/trailhead_test.dart +++ b/examples/travel_app/test/trailhead_test.dart @@ -35,6 +35,7 @@ void main() { }, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), @@ -75,6 +76,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/examples/travel_app/test/travel_carousel_test.dart b/examples/travel_app/test/travel_carousel_test.dart index d77b0f115..1ad9c6d3b 100644 --- a/examples/travel_app/test/travel_carousel_test.dart +++ b/examples/travel_app/test/travel_carousel_test.dart @@ -48,6 +48,7 @@ void main() { }, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), @@ -109,6 +110,7 @@ void main() { }, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), @@ -143,6 +145,7 @@ void main() { dispatchEvent: (event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/audio_player.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/audio_player.dart index 6f7d312f4..755164898 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/audio_player.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/audio_player.dart @@ -36,6 +36,7 @@ final audioPlayer = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200, maxHeight: 100), diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/button.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/button.dart index 66c681b9d..a1e221c50 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/button.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/button.dart @@ -68,6 +68,7 @@ final button = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final buttonData = _ButtonData.fromMap(data as JsonMap); final child = buildChild(buttonData.child); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/card.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/card.dart index e6c05658e..8453ede63 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/card.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/card.dart @@ -41,6 +41,7 @@ final card = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final cardData = _CardData.fromMap(data as JsonMap); return Card(child: buildChild(cardData.child)); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/check_box.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/check_box.dart index 90504be9e..7455d1dfc 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/check_box.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/check_box.dart @@ -48,6 +48,7 @@ final checkBox = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final checkBoxData = _CheckBoxData.fromMap(data as JsonMap); final labelNotifier = dataContext.subscribeToString(checkBoxData.label); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/column.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/column.dart index a00830a31..d9f41ad5c 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/column.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/column.dart @@ -113,23 +113,36 @@ final column = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final columnData = _ColumnData.fromMap(data as JsonMap); return ComponentChildrenBuilder( childrenData: columnData.children, dataContext: dataContext, buildChild: buildChild, - explicitListBuilder: (children) { - return Column( - mainAxisAlignment: _parseMainAxisAlignment( - columnData.distribution, - ), - crossAxisAlignment: _parseCrossAxisAlignment( - columnData.alignment, - ), - children: children, - ); - }, + getComponent: getComponent, + explicitListBuilder: + (childIds, buildChild, getComponent, dataContext) { + return Column( + mainAxisAlignment: _parseMainAxisAlignment( + columnData.distribution, + ), + crossAxisAlignment: _parseCrossAxisAlignment( + columnData.alignment, + ), + mainAxisSize: MainAxisSize.min, + children: childIds + .map( + (componentId) => buildWeightedChild( + componentId: componentId, + dataContext: dataContext, + buildChild: buildChild, + component: getComponent(componentId), + ), + ) + .toList(), + ); + }, templateListWidgetBuilder: (context, list, componentId, dataBinding) { return Column( mainAxisAlignment: _parseMainAxisAlignment( @@ -138,12 +151,18 @@ final column = CatalogItem( crossAxisAlignment: _parseCrossAxisAlignment( columnData.alignment, ), + mainAxisSize: MainAxisSize.min, children: [ - for (var i = 0; i < list.length; i++) - buildChild( - componentId, - dataContext.nested(DataPath('$dataBinding[$i]')), + for (var i = 0; i < list.length; i++) ...[ + buildWeightedChild( + componentId: componentId, + dataContext: dataContext.nested( + DataPath('$dataBinding/$i'), + ), + buildChild: buildChild, + component: getComponent(componentId), ), + ], ], ); }, diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/date_time_input.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/date_time_input.dart index 8f9066f01..234df414d 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/date_time_input.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/date_time_input.dart @@ -67,6 +67,7 @@ final dateTimeInput = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final dateTimeInputData = _DateTimeInputData.fromMap(data as JsonMap); final valueNotifier = dataContext.subscribeToString( diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/divider.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/divider.dart index d97649baf..2a0e8ce06 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/divider.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/divider.dart @@ -40,6 +40,7 @@ final divider = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final dividerData = _DividerData.fromMap(data as JsonMap); if (dividerData.axis == 'vertical') { diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/heading.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/heading.dart index c91158e02..282116210 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/heading.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/heading.dart @@ -47,6 +47,7 @@ final heading = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final headingData = _HeadingData.fromMap(data as JsonMap); final notifier = dataContext.subscribeToString(headingData.text); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/image.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/image.dart index 8498a30c0..eaf7950c5 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/image.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/image.dart @@ -72,6 +72,7 @@ final image = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final imageData = _ImageData.fromMap(data as JsonMap); final notifier = dataContext.subscribeToString(imageData.url); @@ -88,11 +89,14 @@ final image = CatalogItem( } final fit = imageData.fit; + late Widget child; + if (location.startsWith('assets/')) { - return Image.asset(location, fit: fit); + child = Image.asset(location, fit: fit); } else { - return Image.network(location, fit: fit); + child = Image.network(location, fit: fit); } + return SizedBox(width: 150, height: 150, child: child); }, ); }, diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/list.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/list.dart index 03482b811..d80d3ac62 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/list.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/list.dart @@ -59,6 +59,7 @@ final list = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final listData = _ListData.fromMap(data as JsonMap); final direction = listData.direction == 'horizontal' @@ -68,13 +69,17 @@ final list = CatalogItem( childrenData: listData.children, dataContext: dataContext, buildChild: buildChild, - explicitListBuilder: (children) { - return ListView( - shrinkWrap: true, - scrollDirection: direction, - children: children, - ); - }, + getComponent: getComponent, + explicitListBuilder: + (childIds, buildChild, getComponent, dataContext) { + return ListView( + shrinkWrap: true, + scrollDirection: direction, + children: childIds + .map((id) => buildChild(id, dataContext)) + .toList(), + ); + }, templateListWidgetBuilder: (context, Map data, componentId, dataBinding) { final values = data.values.toList(); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/modal.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/modal.dart index ef7115152..e7a2dd4ec 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/modal.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/modal.dart @@ -58,6 +58,7 @@ final modal = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final modalData = _ModalData.fromMap(data as JsonMap); return buildChild(modalData.entryPointChild); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/multiple_choice.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/multiple_choice.dart index f058d4480..ea2efbf74 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/multiple_choice.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/multiple_choice.dart @@ -68,6 +68,7 @@ final multipleChoice = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final multipleChoiceData = _MultipleChoiceData.fromMap(data as JsonMap); final selectionsNotifier = dataContext.subscribeToObjectArray( diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/row.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/row.dart index 0d7724962..ac317ef30 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/row.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/row.dart @@ -113,29 +113,52 @@ final row = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final rowData = _RowData.fromMap(data as JsonMap); return ComponentChildrenBuilder( childrenData: rowData.children, dataContext: dataContext, buildChild: buildChild, - explicitListBuilder: (children) { - return Row( - mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution), - crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment), - children: children, - ); - }, + getComponent: getComponent, + explicitListBuilder: + (childIds, buildChild, getComponent, dataContext) { + return Row( + mainAxisAlignment: _parseMainAxisAlignment( + rowData.distribution, + ), + crossAxisAlignment: _parseCrossAxisAlignment( + rowData.alignment, + ), + mainAxisSize: MainAxisSize.min, + children: childIds + .map( + (componentId) => buildWeightedChild( + componentId: componentId, + dataContext: dataContext, + buildChild: buildChild, + component: getComponent(componentId), + ), + ) + .toList(), + ); + }, templateListWidgetBuilder: (context, list, componentId, dataBinding) { return Row( mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution), crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment), + mainAxisSize: MainAxisSize.min, children: [ - for (var i = 0; i < list.length; i++) - buildChild( - componentId, - dataContext.nested(DataPath('$dataBinding[$i]')), + for (var i = 0; i < list.length; i++) ...[ + buildWeightedChild( + componentId: componentId, + dataContext: dataContext.nested( + DataPath('$dataBinding/$i'), + ), + buildChild: buildChild, + component: getComponent(componentId), ), + ], ], ); }, diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/slider.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/slider.dart index cacd4ff3e..7b53b4a2c 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/slider.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/slider.dart @@ -58,6 +58,7 @@ final slider = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final sliderData = _SliderData.fromMap(data as JsonMap); final valueNotifier = dataContext.subscribeToValue( diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/tabs.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/tabs.dart index c227dc8ee..13b011028 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/tabs.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/tabs.dart @@ -53,6 +53,7 @@ final tabs = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final tabsData = _TabsData.fromMap(data as JsonMap); return DefaultTabController( diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/text.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/text.dart index 0495c51a5..3f1c605d9 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/text.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/text.dart @@ -60,6 +60,7 @@ final text = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final textData = _TextData.fromMap(data as JsonMap); final notifier = dataContext.subscribeToString(textData.text); diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/text_field.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/text_field.dart index ba9ccbfc0..2544496d9 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/text_field.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/text_field.dart @@ -173,6 +173,7 @@ final textField = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { final textFieldData = _TextFieldData.fromMap(data as JsonMap); final valueRef = textFieldData.text; diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/video.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/video.dart index 71e6643b6..94c74161b 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/video.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/video.dart @@ -36,6 +36,7 @@ final video = CatalogItem( required dispatchEvent, required context, required dataContext, + required getComponent, }) { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200, maxHeight: 100), diff --git a/packages/flutter_genui/lib/src/catalog/core_widgets/widget_helpers.dart b/packages/flutter_genui/lib/src/catalog/core_widgets/widget_helpers.dart index cdd2d4895..7ccdeb5c4 100644 --- a/packages/flutter_genui/lib/src/catalog/core_widgets/widget_helpers.dart +++ b/packages/flutter_genui/lib/src/catalog/core_widgets/widget_helpers.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../../model/catalog_item.dart'; import '../../model/data_model.dart'; +import '../../model/ui_models.dart'; import '../../primitives/logging.dart'; import '../../primitives/simple_items.dart'; @@ -22,11 +23,17 @@ typedef TemplateListWidgetBuilder = ); /// Builder function for creating a parent widget given a list of pre-built -/// [children]. +/// [childIds]. /// /// This is used by [ComponentChildrenBuilder] when children are defined as an /// explicit list of component IDs. -typedef ExplicitListWidgetBuilder = Widget Function(List children); +typedef ExplicitListWidgetBuilder = + Widget Function( + List childIds, + ChildBuilderCallback buildChild, + GetComponentCallback getComponent, + DataContext dataContext, + ); /// A helper widget to build widgets from component data that contains a list /// of children. @@ -45,6 +52,7 @@ class ComponentChildrenBuilder extends StatelessWidget { required this.childrenData, required this.dataContext, required this.buildChild, + required this.getComponent, required this.explicitListBuilder, required this.templateListWidgetBuilder, super.key, @@ -59,6 +67,9 @@ class ComponentChildrenBuilder extends StatelessWidget { /// The callback to build a child widget. final ChildBuilderCallback buildChild; + /// The callback to get a component's data by ID. + final GetComponentCallback getComponent; + /// The builder for an explicit list of children. final ExplicitListWidgetBuilder explicitListBuilder; @@ -74,7 +85,10 @@ class ComponentChildrenBuilder extends StatelessWidget { if (explicitList != null) { return explicitListBuilder( - explicitList.map((String id) => buildChild(id, dataContext)).toList(), + explicitList, + buildChild, + getComponent, + dataContext, ); } @@ -113,3 +127,19 @@ class ComponentChildrenBuilder extends StatelessWidget { return const SizedBox.shrink(); } } + +/// Builds a child widget, wrapping it in a [Flexible] if a weight is provided +/// in the component. +Widget buildWeightedChild({ + required String componentId, + required DataContext dataContext, + required ChildBuilderCallback buildChild, + required Component? component, +}) { + final weight = component?.weight; + final childWidget = buildChild(componentId, dataContext); + if (weight != null) { + return Flexible(flex: weight, child: childWidget); + } + return childWidget; +} diff --git a/packages/flutter_genui/lib/src/core/genui_surface.dart b/packages/flutter_genui/lib/src/core/genui_surface.dart index 2735e82a6..876a0314e 100644 --- a/packages/flutter_genui/lib/src/core/genui_surface.dart +++ b/packages/flutter_genui/lib/src/core/genui_surface.dart @@ -90,6 +90,7 @@ class _GenUiSurfaceState extends State { dispatchEvent: _dispatchEvent, context: context, dataContext: dataContext, + getComponent: (String componentId) => definition.components[componentId], ); } diff --git a/packages/flutter_genui/lib/src/model/a2ui_schemas.dart b/packages/flutter_genui/lib/src/model/a2ui_schemas.dart index 482ed36de..6069fde6c 100644 --- a/packages/flutter_genui/lib/src/model/a2ui_schemas.dart +++ b/packages/flutter_genui/lib/src/model/a2ui_schemas.dart @@ -178,6 +178,10 @@ class A2uiSchemas { 'This component could be one of many supported types.', properties: { 'id': S.string(), + 'weight': S.integer( + description: + 'Optional layout weight for use in Row/Column children.', + ), 'component': S.object( description: '''A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.''', diff --git a/packages/flutter_genui/lib/src/model/catalog.dart b/packages/flutter_genui/lib/src/model/catalog.dart index 68931ebe7..576ba6aaf 100644 --- a/packages/flutter_genui/lib/src/model/catalog.dart +++ b/packages/flutter_genui/lib/src/model/catalog.dart @@ -69,6 +69,7 @@ class Catalog { required DispatchEventCallback dispatchEvent, required BuildContext context, required DataContext dataContext, + required GetComponentCallback getComponent, }) { final widgetType = widgetData.keys.firstOrNull; final item = items.firstWhereOrNull((item) => item.name == widgetType); @@ -86,6 +87,7 @@ class Catalog { dispatchEvent: dispatchEvent, context: context, dataContext: dataContext, + getComponent: getComponent, ); } diff --git a/packages/flutter_genui/lib/src/model/catalog_item.dart b/packages/flutter_genui/lib/src/model/catalog_item.dart index fb1918b9a..d1d74739d 100644 --- a/packages/flutter_genui/lib/src/model/catalog_item.dart +++ b/packages/flutter_genui/lib/src/model/catalog_item.dart @@ -8,6 +8,9 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import 'data_model.dart'; import 'ui_models.dart'; +/// A callback to get a component definition by its ID. +typedef GetComponentCallback = Component? Function(String componentId); + /// A callback that builds a child widget for a catalog item. typedef ChildBuilderCallback = Widget Function(String id, [DataContext? dataContext]); @@ -34,6 +37,8 @@ typedef CatalogWidgetBuilder = required BuildContext context, // The current data context for this widget. required DataContext dataContext, + // A function to retrieve a component definition by its ID. + required GetComponentCallback getComponent, }); /// Defines a UI layout type, its schema, and how to build its widget. diff --git a/packages/flutter_genui/lib/src/model/ui_models.dart b/packages/flutter_genui/lib/src/model/ui_models.dart index 34ecec828..017b65930 100644 --- a/packages/flutter_genui/lib/src/model/ui_models.dart +++ b/packages/flutter_genui/lib/src/model/ui_models.dart @@ -134,7 +134,11 @@ class UiDefinition { /// A component in the UI. final class Component { /// Creates a [Component]. - const Component({required this.id, required this.componentProperties}); + const Component({ + required this.id, + required this.componentProperties, + this.weight, + }); /// Creates a [Component] from a JSON map. factory Component.fromJson(JsonMap json) { @@ -144,6 +148,7 @@ final class Component { return Component( id: json['id'] as String, componentProperties: json['component'] as JsonMap, + weight: json['weight'] as int?, ); } @@ -153,9 +158,16 @@ final class Component { /// The properties of the component. final JsonMap componentProperties; + /// The weight of the component, used for layout in Row/Column. + final int? weight; + /// Converts this object to a JSON map. JsonMap toJson() { - return {'id': id, 'component': componentProperties}; + return { + 'id': id, + 'component': componentProperties, + if (weight != null) 'weight': weight, + }; } /// The type of the component. @@ -165,12 +177,16 @@ final class Component { bool operator ==(Object other) => other is Component && id == other.id && + weight == other.weight && const DeepCollectionEquality().equals( componentProperties, other.componentProperties, ); @override - int get hashCode => - Object.hash(id, const DeepCollectionEquality().hash(componentProperties)); + int get hashCode => Object.hash( + id, + weight, + const DeepCollectionEquality().hash(componentProperties), + ); } diff --git a/packages/flutter_genui/test/catalog/core_widgets/column_test.dart b/packages/flutter_genui/test/catalog/core_widgets/column_test.dart new file mode 100644 index 000000000..3a27c9935 --- /dev/null +++ b/packages/flutter_genui/test/catalog/core_widgets/column_test.dart @@ -0,0 +1,151 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_genui/flutter_genui.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Column widget renders children', (WidgetTester tester) async { + final manager = GenUiManager( + catalog: Catalog([CoreCatalogItems.column, CoreCatalogItems.text]), + configuration: const GenUiConfiguration(), + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'column', + componentProperties: { + 'Column': { + 'children': { + 'explicitList': ['text1', 'text2'], + }, + }, + }, + ), + const Component( + id: 'text1', + componentProperties: { + 'Text': { + 'text': {'literalString': 'First'}, + }, + }, + ), + const Component( + id: 'text2', + componentProperties: { + 'Text': { + 'text': {'literalString': 'Second'}, + }, + }, + ), + ]; + manager.handleMessage( + SurfaceUpdate(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const BeginRendering(surfaceId: surfaceId, root: 'column'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GenUiSurface(host: manager, surfaceId: surfaceId), + ), + ), + ); + + expect(find.text('First'), findsOneWidget); + expect(find.text('Second'), findsOneWidget); + }); + + testWidgets('Column widget applies weight property to children', ( + WidgetTester tester, + ) async { + final manager = GenUiManager( + catalog: Catalog([CoreCatalogItems.column, CoreCatalogItems.text]), + configuration: const GenUiConfiguration(), + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'column', + componentProperties: { + 'Column': { + 'children': { + 'explicitList': ['text1', 'text2', 'text3'], + }, + }, + }, + ), + const Component( + id: 'text1', + componentProperties: { + 'Text': { + 'text': {'literalString': 'First'}, + }, + }, + weight: 1, + ), + const Component( + id: 'text2', + componentProperties: { + 'Text': { + 'text': {'literalString': 'Second'}, + }, + }, + weight: 2, + ), + const Component( + id: 'text3', + componentProperties: { + 'Text': { + 'text': {'literalString': 'Third'}, + }, + }, + ), + ]; + manager.handleMessage( + SurfaceUpdate(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const BeginRendering(surfaceId: surfaceId, root: 'column'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GenUiSurface(host: manager, surfaceId: surfaceId), + ), + ), + ); + + expect(find.text('First'), findsOneWidget); + expect(find.text('Second'), findsOneWidget); + expect(find.text('Third'), findsOneWidget); + + final flexibleWidgets = tester + .widgetList(find.byType(Flexible)) + .toList(); + expect(flexibleWidgets.length, 2); + + // Check flex values + expect(flexibleWidgets[0].flex, 1); + expect(flexibleWidgets[1].flex, 2); + + // Check that the correct children are wrapped + expect( + find.ancestor(of: find.text('First'), matching: find.byType(Flexible)), + findsOneWidget, + ); + expect( + find.ancestor(of: find.text('Second'), matching: find.byType(Flexible)), + findsOneWidget, + ); + expect( + find.ancestor(of: find.text('Third'), matching: find.byType(Flexible)), + findsNothing, + ); + }); +} diff --git a/packages/flutter_genui/test/catalog/core_widgets/row_test.dart b/packages/flutter_genui/test/catalog/core_widgets/row_test.dart index 298e4023c..6ded202e9 100644 --- a/packages/flutter_genui/test/catalog/core_widgets/row_test.dart +++ b/packages/flutter_genui/test/catalog/core_widgets/row_test.dart @@ -59,4 +59,93 @@ void main() { expect(find.text('First'), findsOneWidget); expect(find.text('Second'), findsOneWidget); }); + + testWidgets('Row widget applies weight property to children', ( + WidgetTester tester, + ) async { + final manager = GenUiManager( + catalog: Catalog([CoreCatalogItems.row, CoreCatalogItems.text]), + configuration: const GenUiConfiguration(), + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'row', + componentProperties: { + 'Row': { + 'children': { + 'explicitList': ['text1', 'text2', 'text3'], + }, + }, + }, + ), + const Component( + id: 'text1', + componentProperties: { + 'Text': { + 'text': {'literalString': 'First'}, + }, + }, + weight: 1, + ), + const Component( + id: 'text2', + componentProperties: { + 'Text': { + 'text': {'literalString': 'Second'}, + }, + }, + weight: 2, + ), + const Component( + id: 'text3', + componentProperties: { + 'Text': { + 'text': {'literalString': 'Third'}, + }, + }, + ), + ]; + manager.handleMessage( + SurfaceUpdate(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const BeginRendering(surfaceId: surfaceId, root: 'row'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GenUiSurface(host: manager, surfaceId: surfaceId), + ), + ), + ); + + expect(find.text('First'), findsOneWidget); + expect(find.text('Second'), findsOneWidget); + expect(find.text('Third'), findsOneWidget); + + final flexibleWidgets = tester + .widgetList(find.byType(Flexible)) + .toList(); + expect(flexibleWidgets.length, 2); + + // Check flex values + expect(flexibleWidgets[0].flex, 1); + expect(flexibleWidgets[1].flex, 2); + + // Check that the correct children are wrapped + expect( + find.ancestor(of: find.text('First'), matching: find.byType(Flexible)), + findsOneWidget, + ); + expect( + find.ancestor(of: find.text('Second'), matching: find.byType(Flexible)), + findsOneWidget, + ); + expect( + find.ancestor(of: find.text('Third'), matching: find.byType(Flexible)), + findsNothing, + ); + }); } diff --git a/packages/flutter_genui/test/catalog_test.dart b/packages/flutter_genui/test/catalog_test.dart index 1ebe40e92..78f5b7a40 100644 --- a/packages/flutter_genui/test/catalog_test.dart +++ b/packages/flutter_genui/test/catalog_test.dart @@ -34,6 +34,7 @@ void main() { dispatchEvent: (UiEvent event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); }, ), @@ -78,6 +79,7 @@ void main() { dispatchEvent: (UiEvent event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ); expect(widget, isA()); return widget; diff --git a/packages/flutter_genui/test/core/ui_tools_test.dart b/packages/flutter_genui/test/core/ui_tools_test.dart index 810447bbe..379398836 100644 --- a/packages/flutter_genui/test/core/ui_tools_test.dart +++ b/packages/flutter_genui/test/core/ui_tools_test.dart @@ -34,6 +34,7 @@ void main() { required dispatchEvent, required context, required dataContext, + required getComponent, }) { return const Text(''); }, diff --git a/packages/flutter_genui/test/image_test.dart b/packages/flutter_genui/test/image_test.dart index ce4835191..6a4ef906d 100644 --- a/packages/flutter_genui/test/image_test.dart +++ b/packages/flutter_genui/test/image_test.dart @@ -30,6 +30,7 @@ void main() { dispatchEvent: (UiEvent event) {}, context: context, dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 4f6bd509c..df196bbb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,3 +21,6 @@ workspace: - examples/travel_app - tool/fix_copyright - tool/test_and_fix + +flutter: + uses-material-design: true