Skip to content

Conversation

@jacobsimionato
Copy link
Collaborator

@jacobsimionato jacobsimionato commented Nov 12, 2025

Summary of Changes

This pull request introduces a new SurfaceController class to centralize the management of individual UI surfaces within the genui package. This refactoring aims to improve modularity, encapsulation, and maintainability by moving surface-specific state and logic out of the GenUiManager and into dedicated controllers. The GenUiManager now acts as a coordinator for these SurfaceController instances, and the GenUiSurface widget directly consumes a SurfaceController, streamlining the UI rendering process. This change impacts several example applications and tests, which have been updated to align with the new architecture.

Highlights

  • Introduction of SurfaceController: A new class, SurfaceController, has been added to centralize the management of individual UI surfaces, encapsulating UiDefinition and DataModel.
  • Refactoring of GenUiManager: The GenUiManager has been refactored to delegate surface-specific state management to SurfaceController instances, acting as a coordinator for their lifecycle.
  • GenUiSurface Widget Update: The GenUiSurface widget now directly accepts a SurfaceController instead of separate host and surfaceId parameters, simplifying its API and improving encapsulation.
  • GenUiConversation Class Update: The GenUiConversation class has been updated to utilize SurfaceController for managing UI surfaces, including handling onSurfaceAdded, onSurfaceDeleted, and onSurfaceUpdated callbacks with SurfaceController objects.
  • Examples and Tests Alignment: All example applications and unit tests have been updated to align with the new SurfaceController-based API, ensuring consistency and correctness across the codebase.
Changelog
  • examples/custom_backend/lib/main.dart
    • Updated GenUiSurface to use controller instead of surfaceId and host.
  • examples/simple_chat/lib/main.dart
    • Modified _handleSurfaceAdded to accept SurfaceController.
    • Updated MessageView to receive SurfaceController.
  • examples/simple_chat/lib/message.dart
    • Refactored MessageView to accept messageController and surfaceController directly.
    • Updated GenUiSurface usage to pass the controller.
  • examples/travel_app/lib/src/widgets/conversation.dart
    • Updated AiUiMessage case to retrieve and pass SurfaceController to GenUiSurface.
  • examples/travel_app/test/widgets/conversation_test.dart
    • Removed a test case related to userPromptBuilder.
  • examples/verdure/client/lib/features/screens/order_confirmation_screen.dart
    • Updated GenUiSurface to use controller.
    • Updated ValueListenableBuilder to listen to controller.uiDefinitionNotifier.
  • examples/verdure/client/lib/features/screens/presentation_screen.dart
    • Updated GenUiSurface to use controller.
    • Updated ValueListenableBuilder to listen to controller.uiDefinitionNotifier.
  • examples/verdure/client/lib/features/screens/questionnaire_screen.dart
    • Updated GenUiSurface to use controller.
    • Updated ValueListenableBuilder to listen to controller.uiDefinitionNotifier.
  • examples/verdure/client/lib/features/screens/shopping_cart_screen.dart
    • Updated GenUiSurface to use controller.
    • Updated ValueListenableBuilder to listen to controller.uiDefinitionNotifier.
  • packages/genui/.guides/docs/connect_to_agent_provider.md
    • Updated example code to use SurfaceController with GenUiSurface.
  • packages/genui/.guides/examples/riddles.dart
    • Modified _MyHomePageState to use SurfaceController in onSurfaceAdded and GenUiSurface.
  • packages/genui/README.md
    • Updated example code to use SurfaceController with GenUiSurface.
  • packages/genui/lib/genui.dart
    • Exported src/core/surface_controller.dart.
  • packages/genui/lib/src/conversation/gen_ui_conversation.dart
    • Imported surface_controller.dart.
    • Updated onSurfaceAdded, onSurfaceDeleted, onSurfaceUpdated callbacks to use SurfaceController.
    • Refactored _handleSurfaceUpdate and _handleDefinitionUpdate to manage AiUiMessage history based on SurfaceController's notifier.
    • Replaced surface() method with getSurfaceController().
  • packages/genui/lib/src/core/genui_manager.dart
    • Refactored GenUiUpdate to use SurfaceController.
    • Removed SurfaceUpdated event class.
    • Removed GenUiHost interface implementation.
    • Replaced _surfaces map with _surfaceControllers map.
    • Updated getSurfaceController to create and manage SurfaceController instances.
    • Delegated message handling to SurfaceController instances.
  • packages/genui/lib/src/core/genui_surface.dart
    • Imported surface_controller.dart.
    • Updated GenUiSurface constructor to take a controller parameter.
    • Modified build and _dispatchEvent methods to use widget.controller.
  • packages/genui/lib/src/core/surface_controller.dart
    • Added new file defining SurfaceController class to manage UiDefinition and DataModel for a single UI surface.
  • packages/genui/lib/src/development_utilities/catalog_view.dart
    • Updated GenUiSurface to use SurfaceController.
  • packages/genui/test/catalog/core_widgets/button_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/card_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/check_box_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
    • Accessed dataModel via SurfaceController.
  • packages/genui/test/catalog/core_widgets/column_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/date_time_input_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
    • Accessed dataModel via SurfaceController.
  • packages/genui/test/catalog/core_widgets/divider_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/icon_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/list_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/modal_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/multiple_choice_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
    • Accessed dataModel via SurfaceController.
  • packages/genui/test/catalog/core_widgets/row_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets/slider_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
    • Accessed dataModel via SurfaceController.
  • packages/genui/test/catalog/core_widgets/tabs_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/catalog/core_widgets_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
    • Accessed dataModel via SurfaceController.
  • packages/genui/test/core/genui_manager_test.dart
    • Removed FlutterError import.
    • Refactored tests to use getSurfaceController.
    • Removed tests for SurfaceUpdated and surface() methods.
    • Updated handleMessage and dispose tests to reflect SurfaceController changes.
  • packages/genui/test/core/surface_controller_test.dart
    • Added new file with unit tests for SurfaceController's disposal and initial state.
  • packages/genui/test/genui_surface_test.dart
    • Updated tests to use SurfaceController with GenUiSurface.
  • packages/genui/test/ui_tools_test.dart
    • Imported dart:async.
    • Updated expectLater for SurfaceAdded to check controller.surfaceId and controller.uiDefinitionNotifier.value.
    • Refactored SurfaceUpdated test to use a Completer and addListener on uiDefinitionNotifier.
  • packages/genui_a2ui/example/lib/main.dart
    • Updated _ChatScreenState to handle SurfaceAdded and SurfaceRemoved events using SurfaceController.
    • Removed SurfaceUpdated event handling.
    • Updated GenUiSurface to use controller.

@jacobsimionato
Copy link
Collaborator Author

/gemini summarize

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and positive refactoring by adding the SurfaceController. This encapsulates surface-specific state and logic, simplifying the GenUiManager and making the overall architecture cleaner and more scalable. The changes are consistently applied across the examples and tests. I've provided a few suggestions to further improve maintainability by removing some dead code, simplifying repetitive checks, fixing a test, and making listener management more explicit.

Comment on lines +51 to +68
if (message is SurfaceUpdate && message.surfaceId != surfaceId) {
throw ArgumentError(
'Mismatched surfaceId in message: '
'expected $surfaceId, got ${message.surfaceId}',
);
}
if (message is BeginRendering && message.surfaceId != surfaceId) {
throw ArgumentError(
'Mismatched surfaceId in message: '
'expected $surfaceId, got ${message.surfaceId}',
);
}
if (message is DataModelUpdate && message.surfaceId != surfaceId) {
throw ArgumentError(
'Mismatched surfaceId in message: '
'expected $surfaceId, got ${message.surfaceId}',
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

These checks for surfaceId are repetitive. You can make this code more concise and maintainable by using a switch expression to extract the surfaceId from the message and then performing a single check. This will also make it easier to add new message types in the future.

    final String messageSurfaceId = switch (message) {
      SurfaceUpdate(:final surfaceId) => surfaceId,
      BeginRendering(:final surfaceId) => surfaceId,
      DataModelUpdate(:final surfaceId) => surfaceId,
      SurfaceDeletion(:final surfaceId) => surfaceId,
    };

    if (messageSurfaceId != surfaceId) {
      throw ArgumentError(
        'Mismatched surfaceId in message: '
        'expected $surfaceId, got $messageSurfaceId',
      );
    }

Comment on lines +87 to +95
// Listen for updates to this specific controller to manage history.
// We don't store a reference to the listener, so it can't be removed
// later. However, since the lifecycle of the controller is managed by
// the GenUiManager and tied to the conversation, this should not
// result in a memory leak in practice as the controller itself will be
// disposed.
controller.uiDefinitionNotifier.addListener(() {
_handleDefinitionUpdate(controller);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The long comment explaining the listener lifecycle suggests the implementation could be clearer. While the current logic is sound, explicitly managing listeners makes the code more robust and self-documenting.

You can achieve this by storing listeners in a map and removing them when a surface is removed. This removes the need for the long explanatory comment.

For example, you could add a _surfaceListeners map to your class:

final _surfaceListeners = <String, VoidCallback>{};

Then, in _handleSurfaceUpdate:

// When adding a listener for SurfaceAdded:
final listener = () => _handleDefinitionUpdate(controller);
_surfaceListeners[controller.surfaceId] = listener;
controller.uiDefinitionNotifier.addListener(listener);

// When handling SurfaceRemoved:
final listener = _surfaceListeners.remove(controller.surfaceId);
if (listener != null) {
  controller.uiDefinitionNotifier.removeListener(listener);
}

This makes the listener management explicit and easier to follow.

Comment on lines +33 to +47
// After dispose, trying to add a listener should throw an error.
// The test runner is catching this error before the try/catch block can,
// so this part of the test is commented out. The error message proves
// that dispose() is working as intended.
// try {
// notifier.addListener(() {});
// fail('Should have thrown an error');
// } catch (e) {
// expect(e, isA<Error>());
// expect(
// e.toString(),
// contains('A ValueNotifier<UiDefinition?> was used after being disposed.'),
// );
// }
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This test case for dispose is commented out. It can be fixed and enabled by using expect(..., throwsA(...)) to verify that using the notifier after disposal throws an error. This is a more standard way to test for exceptions/errors in Flutter tests than a try-catch block.

Suggested change
// After dispose, trying to add a listener should throw an error.
// The test runner is catching this error before the try/catch block can,
// so this part of the test is commented out. The error message proves
// that dispose() is working as intended.
// try {
// notifier.addListener(() {});
// fail('Should have thrown an error');
// } catch (e) {
// expect(e, isA<Error>());
// expect(
// e.toString(),
// contains('A ValueNotifier<UiDefinition?> was used after being disposed.'),
// );
// }
});
// After dispose, trying to add a listener should throw an error.
expect(() => notifier.addListener(() {}), throwsA(isA<FlutterError>()));

@flutter flutter deleted a comment from gemini-code-assist bot Nov 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant