Problem Statement
How do you properly manage StreamController lifecycle? Explain creation, usage, and disposal.
Explanation
Create StreamController in initState() or as a State class field, choosing between regular StreamController for single listener or StreamController.broadcast() for multiple listeners. Store the controller in State to keep it alive across rebuilds and provide access for adding events. Expose only controller.stream publicly, keeping the controller private to encapsulate control over event emission.
Add events to the stream using controller.add(value) in response to user actions, timers, or other events. Add errors with controller.addError(error) and close the stream with controller.close() when done. Never add to closed controllers as this throws errors - check controller.isClosed before adding if there's any possibility of the controller being closed.
Always close StreamController in dispose() to free resources and prevent memory leaks. Failing to close controllers leaves subscriptions active and memory allocated even after widgets are removed. Use controller.close() which completes the stream, notifying listeners it's done. For broadcast streams, call close() even with multiple listeners - it properly cleans up all subscriptions.
Common pattern: create controller in initState, add events in response to actions, expose stream to UI through widget properties or state management, and close in dispose. For complex cases, use StreamController.stream.asBroadcastStream() to convert single-subscription to broadcast. Handle async adds carefully - don't add to controller after dispose, and await controller.close() if needed. Proper StreamController management prevents memory leaks and ensures clean async code.
