Repository: ReactiveX/rxdart

Issue:
Add ValueStreamListener and ValueStreamConsumer widgets
I would like to propose adding two new widgets to complement the existing ValueStreamBuilder:

1. **ValueStreamListener**
A widget that listens to a ValueStream and executes a callback when the stream emits new values, without rebuilding the widget tree. Useful for side effects like showing snackbars or navigation.

Signature:

```dart
ValueStreamListener<T>({
  required ValueStream<T> stream,
  required void Function(BuildContext context, T previous, T current) listener,
  required Widget child,
  Key? key,
})
```


2. **ValueStreamConsumer**

A widget that combines both ValueStreamBuilder and ValueStreamListener functionality, allowing both rebuilding and side effects from a single widget.

Signature:

```dart
ValueStreamConsumer<T>({
  required ValueStream<T> stream,
  required Widget Function(BuildContext context, T value) builder,
  bool Function(T previous, T current)? buildWhen,
  void Function(BuildContext context, T previous, T current)? listener,
  Key? key,
})
```

Notes: 

- The builder callback uses `value` instead of `data` as its parameter name to better align with ValueStream's value-focused nature and its `.value` getter. This makes the API more intuitive when working with ValueStreams.
- Unlike other similar implementations, the `listener` in ValueStreamConsumer is optional. This design choice allows the Consumer to be used primarily as a builder while maintaining the flexibility to add or remove listeners as needed, without requiring a complete widget swap.


___

I have implementations of these widgets that I've been using in my projects and would be happy to contribute them via PR in the next few days.

Let me know if you're interested in this contribution and if you'd like to see the PR.

Thanks!


Discussion: PRs are welcome 🙏 
I think keeping ValueStreamBuilder makes sense because it provides a simple and straightforward way to work with streams in Flutter.


Fix patch:
diff --git a/examples/flutter/github_search/lib/search_screen.dart b/examples/flutter/github_search/lib/search_screen.dart
index 6e00f688a..0a19be9b7 100644
--- a/examples/flutter/github_search/lib/search_screen.dart
+++ b/examples/flutter/github_search/lib/search_screen.dart
@@ -47,7 +47,7 @@ class SearchScreenState extends State<SearchScreen> {
   Widget build(BuildContext context) {
     return ValueStreamBuilder<SearchState>(
       stream: bloc.state,
-      builder: (context, state) {
+      builder: (context, state, child) {
         return Scaffold(
           appBar: AppBar(
             title: const Text('RxDart Github Search'),
diff --git a/examples/flutter/github_search/macos/Runner.xcodeproj/project.pbxproj b/examples/flutter/github_search/macos/Runner.xcodeproj/project.pbxproj
index 46bc75f48..aeccdfcdf 100644
--- a/examples/flutter/github_search/macos/Runner.xcodeproj/project.pbxproj
+++ b/examples/flutter/github_search/macos/Runner.xcodeproj/project.pbxproj
@@ -21,6 +21,7 @@
 /* End PBXAggregateTarget section */
 
 /* Begin PBXBuildFile section */
+		331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
 		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
 		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
 		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
@@ -29,6 +30,13 @@
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
+		331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC10EC2044A3C60003C045;
+			remoteInfo = Runner;
+		};
 		33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
@@ -52,6 +60,8 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
 		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
 		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
 		33CC10ED2044A3C60003C045 /* github_search.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "github_search.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -71,6 +81,13 @@
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		331C80D2294CF70F00263BE5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		33CC10EA2044A3C60003C045 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -81,6 +98,14 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		331C80D6294CF71000263BE5 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				331C80D7294CF71000263BE5 /* RunnerTests.swift */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
 		33BA886A226E78AF003329D5 /* Configs */ = {
 			isa = PBXGroup;
 			children = (
@@ -97,6 +122,7 @@
 			children = (
 				33FAB671232836740065AC1E /* Runner */,
 				33CEB47122A05771004F2AC0 /* Flutter */,
+				331C80D6294CF71000263BE5 /* RunnerTests */,
 				33CC10EE2044A3C60003C045 /* Products */,
 				D73912EC22F37F3D000D13A0 /* Frameworks */,
 			);
@@ -106,6 +132,7 @@
 			isa = PBXGroup;
 			children = (
 				33CC10ED2044A3C60003C045 /* github_search.app */,
+				331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -155,6 +182,24 @@
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
+		331C80D4294CF70F00263BE5 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				331C80D1294CF70F00263BE5 /* Sources */,
+				331C80D2294CF70F00263BE5 /* Frameworks */,
+				331C80D3294CF70F00263BE5 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				331C80DA294CF71000263BE5 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		33CC10EC2044A3C60003C045 /* Runner */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -181,10 +226,15 @@
 		33CC10E52044A3C60003C045 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
+				BuildIndependentTargetsInParallel = YES;
 				LastSwiftUpdateCheck = 0920;
-				LastUpgradeCheck = 1300;
+				LastUpgradeCheck = 1510;
 				ORGANIZATIONNAME = "";
 				TargetAttributes = {
+					331C80D4294CF70F00263BE5 = {
+						CreatedOnToolsVersion = 14.0;
+						TestTargetID = 33CC10EC2044A3C60003C045;
+					};
 					33CC10EC2044A3C60003C045 = {
 						CreatedOnToolsVersion = 9.2;
 						LastSwiftMigration = 1100;
@@ -215,12 +265,20 @@
 			projectRoot = "";
 			targets = (
 				33CC10EC2044A3C60003C045 /* Runner */,
+				331C80D4294CF70F00263BE5 /* RunnerTests */,
 				33CC111A2044C6BA0003C045 /* Flutter Assemble */,
 			);
 		};
 /* End PBXProject section */
 
 /* Begin PBXResourcesBuildPhase section */
+		331C80D3294CF70F00263BE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		33CC10EB2044A3C60003C045 /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -274,6 +332,14 @@
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
+		331C80D1294CF70F00263BE5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		33CC10E92044A3C60003C045 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -287,6 +353,11 @@
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
+		331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC10EC2044A3C60003C045 /* Runner */;
+			targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
+		};
 		33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
@@ -307,11 +378,54 @@
 /* End PBXVariantGroup section */
 
 /* Begin XCBuildConfiguration section */
+		331C80DB294CF71000263BE5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.githubSearch.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/github_search.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/github_search";
+			};
+			name = Debug;
+		};
+		331C80DC294CF71000263BE5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.githubSearch.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/github_search.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/github_search";
+			};
+			name = Release;
+		};
+		331C80DD294CF71000263BE5 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.githubSearch.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/github_search.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/github_search";
+			};
+			name = Profile;
+		};
 		338D0CE9231458BD00FA5F75 /* Profile */ = {
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -335,9 +449,11 @@
 				CLANG_WARN_SUSPICIOUS_MOVE = YES;
 				CODE_SIGN_IDENTITY = "-";
 				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_NO_COMMON_BLOCKS = YES;
 				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -385,6 +501,7 @@
 			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -408,9 +525,11 @@
 				CLANG_WARN_SUSPICIOUS_MOVE = YES;
 				CODE_SIGN_IDENTITY = "-";
 				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
 				DEBUG_INFORMATION_FORMAT = dwarf;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_DYNAMIC_NO_PIC = NO;
 				GCC_NO_COMMON_BLOCKS = YES;
@@ -438,6 +557,7 @@
 			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -461,9 +581,11 @@
 				CLANG_WARN_SUSPICIOUS_MOVE = YES;
 				CODE_SIGN_IDENTITY = "-";
 				COPY_PHASE_STRIP = NO;
+				DEAD_CODE_STRIPPING = YES;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GCC_C_LANGUAGE_STANDARD = gnu11;
 				GCC_NO_COMMON_BLOCKS = YES;
 				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -537,6 +659,16 @@
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+		331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				331C80DB294CF71000263BE5 /* Debug */,
+				331C80DC294CF71000263BE5 /* Release */,
+				331C80DD294CF71000263BE5 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
diff --git a/examples/flutter/github_search/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/flutter/github_search/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 6e44efd89..d3d14384c 100644
--- a/examples/flutter/github_search/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/examples/flutter/github_search/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1300"
+   LastUpgradeVersion = "1510"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -37,6 +37,17 @@
          </BuildableReference>
       </MacroExpansion>
       <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "331C80D4294CF70F00263BE5"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
    </TestAction>
    <LaunchAction
diff --git a/examples/flutter/github_search/macos/Runner/AppDelegate.swift b/examples/flutter/github_search/macos/Runner/AppDelegate.swift
index d53ef6437..b3c176141 100644
--- a/examples/flutter/github_search/macos/Runner/AppDelegate.swift
+++ b/examples/flutter/github_search/macos/Runner/AppDelegate.swift
@@ -1,9 +1,13 @@
 import Cocoa
 import FlutterMacOS
 
-@NSApplicationMain
+@main
 class AppDelegate: FlutterAppDelegate {
   override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
     return true
   }
+
+  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+    return true
+  }
 }
diff --git a/examples/flutter/github_search/macos/Runner/DebugProfile.entitlements b/examples/flutter/github_search/macos/Runner/DebugProfile.entitlements
index dddb8a30c..08c3ab17c 100644
--- a/examples/flutter/github_search/macos/Runner/DebugProfile.entitlements
+++ b/examples/flutter/github_search/macos/Runner/DebugProfile.entitlements
@@ -8,5 +8,7 @@
 	<true/>
 	<key>com.apple.security.network.server</key>
 	<true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
 </dict>
 </plist>
diff --git a/examples/flutter/github_search/macos/Runner/MainFlutterWindow.swift b/examples/flutter/github_search/macos/Runner/MainFlutterWindow.swift
index 2722837ec..3cc05eb23 100644
--- a/examples/flutter/github_search/macos/Runner/MainFlutterWindow.swift
+++ b/examples/flutter/github_search/macos/Runner/MainFlutterWindow.swift
@@ -3,7 +3,7 @@ import FlutterMacOS
 
 class MainFlutterWindow: NSWindow {
   override func awakeFromNib() {
-    let flutterViewController = FlutterViewController.init()
+    let flutterViewController = FlutterViewController()
     let windowFrame = self.frame
     self.contentViewController = flutterViewController
     self.setFrame(windowFrame, display: true)
diff --git a/examples/flutter/github_search/macos/Runner/Release.entitlements b/examples/flutter/github_search/macos/Runner/Release.entitlements
index 852fa1a47..ee95ab7e5 100644
--- a/examples/flutter/github_search/macos/Runner/Release.entitlements
+++ b/examples/flutter/github_search/macos/Runner/Release.entitlements
@@ -4,5 +4,7 @@
 <dict>
 	<key>com.apple.security.app-sandbox</key>
 	<true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
 </dict>
 </plist>
diff --git a/examples/flutter/github_search/macos/RunnerTests/RunnerTests.swift b/examples/flutter/github_search/macos/RunnerTests/RunnerTests.swift
new file mode 100644
index 000000000..61f3bd1fc
--- /dev/null
+++ b/examples/flutter/github_search/macos/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Cocoa
+import FlutterMacOS
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+  func testExample() {
+    // If you add code to the Runner application, consider adding tests here.
+    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+  }
+
+}
diff --git a/examples/flutter/github_search/pubspec.lock b/examples/flutter/github_search/pubspec.lock
index e69e6b906..db1c28b85 100644
--- a/examples/flutter/github_search/pubspec.lock
+++ b/examples/flutter/github_search/pubspec.lock
@@ -5,26 +5,31 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
+      sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
       url: "https://pub.dev"
     source: hosted
-    version: "61.0.0"
+    version: "76.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.3.3"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
+      sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
       url: "https://pub.dev"
     source: hosted
-    version: "5.13.0"
+    version: "6.11.0"
   args:
     dependency: transitive
     description:
       name: args
-      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
       url: "https://pub.dev"
     source: hosted
-    version: "2.5.0"
+    version: "2.6.0"
   async:
     dependency: transitive
     description:
@@ -45,50 +50,50 @@ packages:
     dependency: transitive
     description:
       name: build
-      sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
+      sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.1"
+    version: "2.4.2"
   build_config:
     dependency: transitive
     description:
       name: build_config
-      sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
+      sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.1"
+    version: "1.1.2"
   build_daemon:
     dependency: transitive
     description:
       name: build_daemon
-      sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
+      sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
       url: "https://pub.dev"
     source: hosted
-    version: "4.0.1"
+    version: "4.0.3"
   build_resolvers:
     dependency: transitive
     description:
       name: build_resolvers
-      sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
+      sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.2"
+    version: "2.4.3"
   build_runner:
     dependency: "direct dev"
     description:
       name: build_runner
-      sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
+      sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.9"
+    version: "2.4.14"
   build_runner_core:
     dependency: transitive
     description:
       name: build_runner_core
-      sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
+      sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
       url: "https://pub.dev"
     source: hosted
-    version: "7.3.0"
+    version: "8.0.0"
   built_collection:
     dependency: transitive
     description:
@@ -101,10 +106,10 @@ packages:
     dependency: transitive
     description:
       name: built_value
-      sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
+      sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
       url: "https://pub.dev"
     source: hosted
-    version: "8.9.2"
+    version: "8.9.3"
   characters:
     dependency: transitive
     description:
@@ -133,10 +138,10 @@ packages:
     dependency: transitive
     description:
       name: code_builder
-      sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
+      sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
       url: "https://pub.dev"
     source: hosted
-    version: "4.10.0"
+    version: "4.10.1"
   collection:
     dependency: transitive
     description:
@@ -149,26 +154,26 @@ packages:
     dependency: transitive
     description:
       name: convert
-      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.1"
+    version: "3.1.2"
   coverage:
     dependency: transitive
     description:
       name: coverage
-      sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5
+      sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
       url: "https://pub.dev"
     source: hosted
-    version: "1.9.2"
+    version: "1.11.1"
   crypto:
     dependency: transitive
     description:
       name: crypto
-      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
+      sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.3"
+    version: "3.0.6"
   dart_either:
     dependency: transitive
     description:
@@ -181,18 +186,18 @@ packages:
     dependency: transitive
     description:
       name: dart_style
-      sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
+      sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "3.0.1"
   equatable:
     dependency: "direct main"
     description:
       name: equatable
-      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.5"
+    version: "2.0.7"
   fake_async:
     dependency: transitive
     description:
@@ -205,18 +210,18 @@ packages:
     dependency: transitive
     description:
       name: file
-      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
       url: "https://pub.dev"
     source: hosted
-    version: "7.0.0"
+    version: "7.0.1"
   fixnum:
     dependency: transitive
     description:
       name: fixnum
-      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+      sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -247,26 +252,26 @@ packages:
     dependency: transitive
     description:
       name: frontend_server_client
-      sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
+      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
       url: "https://pub.dev"
     source: hosted
-    version: "3.2.0"
+    version: "4.0.0"
   glob:
     dependency: transitive
     description:
       name: glob
-      sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.2"
+    version: "2.1.3"
   graphs:
     dependency: transitive
     description:
       name: graphs
-      sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
+      sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.1"
+    version: "2.3.2"
   http:
     dependency: "direct main"
     description:
@@ -279,34 +284,34 @@ packages:
     dependency: transitive
     description:
       name: http_multi_server
-      sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+      sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
       url: "https://pub.dev"
     source: hosted
-    version: "3.2.1"
+    version: "3.2.2"
   http_parser:
     dependency: transitive
     description:
       name: http_parser
-      sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
       url: "https://pub.dev"
     source: hosted
-    version: "4.0.2"
+    version: "4.1.2"
   io:
     dependency: transitive
     description:
       name: io
-      sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+      sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.4"
+    version: "1.0.5"
   js:
     dependency: transitive
     description:
       name: js
-      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+      sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
       url: "https://pub.dev"
     source: hosted
-    version: "0.6.7"
+    version: "0.7.1"
   json_annotation:
     dependency: transitive
     description:
@@ -351,10 +356,18 @@ packages:
     dependency: transitive
     description:
       name: logging
-      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.0"
+    version: "0.1.3-main.0"
   matcher:
     dependency: transitive
     description:
@@ -383,18 +396,18 @@ packages:
     dependency: transitive
     description:
       name: mime
-      sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
+      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.4"
+    version: "2.0.0"
   mockito:
     dependency: "direct dev"
     description:
       name: mockito
-      sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
+      sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6
       url: "https://pub.dev"
     source: hosted
-    version: "5.4.4"
+    version: "5.4.5"
   node_preamble:
     dependency: transitive
     description:
@@ -407,10 +420,10 @@ packages:
     dependency: transitive
     description:
       name: package_config
-      sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+      sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "2.1.1"
   path:
     dependency: transitive
     description:
@@ -431,18 +444,18 @@ packages:
     dependency: transitive
     description:
       name: pub_semver
-      sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+      sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.4"
+    version: "2.1.5"
   pubspec_parse:
     dependency: transitive
     description:
       name: pubspec_parse
-      sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
+      sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.3"
+    version: "1.5.0"
   rxdart:
     dependency: "direct main"
     description:
@@ -469,10 +482,10 @@ packages:
     dependency: transitive
     description:
       name: shelf
-      sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
+      sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
       url: "https://pub.dev"
     source: hosted
-    version: "1.4.1"
+    version: "1.4.2"
   shelf_packages_handler:
     dependency: transitive
     description:
@@ -485,18 +498,18 @@ packages:
     dependency: transitive
     description:
       name: shelf_static
-      sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
+      sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.2"
+    version: "1.1.3"
   shelf_web_socket:
     dependency: transitive
     description:
       name: shelf_web_socket
-      sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
+      sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.4"
+    version: "2.0.1"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -506,26 +519,26 @@ packages:
     dependency: transitive
     description:
       name: source_gen
-      sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
+      sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
       url: "https://pub.dev"
     source: hosted
-    version: "1.5.0"
+    version: "2.0.0"
   source_map_stack_trace:
     dependency: transitive
     description:
       name: source_map_stack_trace
-      sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
+      sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.2"
   source_maps:
     dependency: transitive
     description:
       name: source_maps
-      sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
+      sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
       url: "https://pub.dev"
     source: hosted
-    version: "0.10.12"
+    version: "0.10.13"
   source_span:
     dependency: transitive
     description:
@@ -554,10 +567,10 @@ packages:
     dependency: transitive
     description:
       name: stream_transform
-      sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+      sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "2.1.1"
   string_scanner:
     dependency: transitive
     description:
@@ -602,18 +615,18 @@ packages:
     dependency: transitive
     description:
       name: timing
-      sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
+      sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.1"
+    version: "1.0.2"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
       url: "https://pub.dev"
     source: hosted
-    version: "1.3.2"
+    version: "1.4.0"
   vector_math:
     dependency: transitive
     description:
@@ -634,18 +647,34 @@ packages:
     dependency: transitive
     description:
       name: watcher
-      sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+      sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
       url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
+  web_socket:
+    dependency: transitive
+    description:
+      name: web_socket
+      sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.6"
   web_socket_channel:
     dependency: transitive
     description:
       name: web_socket_channel
-      sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
+      sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.0"
+    version: "3.0.2"
   webkit_inspection_protocol:
     dependency: transitive
     description:
@@ -658,10 +687,10 @@ packages:
     dependency: transitive
     description:
       name: yaml
-      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.2"
+    version: "3.1.3"
 sdks:
-  dart: ">=3.4.0 <4.0.0"
+  dart: ">=3.6.0 <4.0.0"
   flutter: ">=3.18.0-18.0.pre.54"
diff --git a/packages/rxdart_flutter/README.md b/packages/rxdart_flutter/README.md
index f939e7cd2..c1704bd3e 100644
--- a/packages/rxdart_flutter/README.md
+++ b/packages/rxdart_flutter/README.md
@@ -18,57 +18,143 @@
 [![License](https://img.shields.io/github/license/ReactiveX/rxdart)](https://www.apache.org/licenses/LICENSE-2.0)
 [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FReactiveX%2Frxdart&count_bg=%23D71092&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
 
+`rxdart_flutter` is a Flutter package that provides a set of widgets for working with `rxdart`.
+
+These widgets are specifically designed to work with `ValueStream`s, making it easier to build reactive UIs in Flutter.
+
+## Overview
+
+This package provides three main widgets:
+- `ValueStreamBuilder`: A widget that rebuilds UI based on `ValueStream` updates
+- `ValueStreamListener`: A widget that performs side effects when `ValueStream` values change
+- `ValueStreamConsumer`: A widget combining both builder and listener capabilities for `ValueStream`s
+
+All widgets require a `ValueStream` that always has a value and never emits errors. If these conditions are not met, appropriate error widgets will be displayed.
+
 ## ValueStreamBuilder
 
-`ValueStreamBuilder` is a widget similar to `StreamBuilder`, but works with `ValueStream` and simplifies the process of rebuilding widgets in response to stream updates. It provides a more streamlined API and performance improvements for working with streams that always have a value and do not emit errors.
+`ValueStreamBuilder` is a widget that builds itself based on the latest value emitted by a `ValueStream`. It's similar to Flutter's `StreamBuilder` but specifically optimized for `ValueStream`s.
 
 ### Features
-- Works with `ValueStream` instead of `Stream`.
-- Automatically rebuilds the widget when the stream emits new data.
-- Supports optional `buildWhen` callback for more granular control over rebuild behavior.
 
-### Usage
+- Always has access to the current value (no `AsyncSnapshot` needed)
+- Optional `buildWhen` condition for controlling rebuilds
+- Proper error handling for streams without values or with errors
+- Efficient rebuilding only when necessary
 
-#### Basic Example
+### Example
 
 ```dart
-final valueStream = BehaviorSubject<int>.seeded(0);
+final counterStream = BehaviorSubject<int>.seeded(0); // Initial value required
 
 ValueStreamBuilder<int>(
-  stream: valueStream,
-  builder: (context, data) {
-    // return widget here based on data
-    return Text('Current value: $data');
+  stream: counterStream,
+  buildWhen: (previous, current) => current != previous, // Optional rebuild condition
+  builder: (context, value, child) {
+    return Column(
+      children: [
+        Text(
+          'Counter: $value',
+          style: Theme.of(context).textTheme.headlineMedium,
+        ),
+        if (child != null) child, // Use the stable child widget if provided
+      ],
+    );
   },
-);
+  child: const Text('This widget remains stable'), // Optional stable child widget
+)
 ```
 
-#### Example with `buildWhen`
+## ValueStreamListener
+
+`ValueStreamListener` is a widget that executes callbacks in response to stream value changes. It's perfect for handling side effects like showing snackbars, dialogs, or navigation.
 
-You can provide an optional `buildWhen` callback to control when the widget should be rebuilt based on changes to the data.
+### Features
+
+- Access to both previous and current values in the listener
+- No rebuilds on value changes (unlike ValueStreamBuilder)
+- Child widget is preserved across stream updates
+- Guaranteed to only call listener once per value change
+- Optional `child` for stable widgets that remain unchanged across stream updates
+
+### Example
 
 ```dart
-ValueStreamBuilder<int>(
-  stream: valueStream,
-  buildWhen: (previous, current) {
-    // Only rebuild if the current value is different from the previous value
-    return previous != current;
+final authStream = BehaviorSubject<AuthState>.seeded(AuthState.initial);
+
+ValueStreamListener<AuthState>(
+  stream: authStream,
+  listener: (context, previous, current) {
+    if (previous.isLoggedOut && current.isLoggedIn) {
+      Navigator.of(context).pushReplacementNamed('/home');
+    } else if (previous.isLoggedIn && current.isLoggedOut) {
+      Navigator.of(context).pushReplacementNamed('/login');
+    }
   },
-  builder: (context, data) {
-    return Text('Current value: $data');
+  child: MyApp(), // Child widget remains stable
+)
+```
+
+## ValueStreamConsumer
+
+`ValueStreamConsumer` combines the functionality of both `ValueStreamBuilder` and `ValueStreamListener`. Use it when you need to both rebuild the UI and perform side effects in response to stream changes.
+
+### Features
+
+- Combined builder and listener functionality
+- Optional `buildWhen` condition for controlling rebuilds
+- Access to previous and current values in listener
+- Efficient handling of both UI updates and side effects
+- Optional `child` for stable widgets that remain unchanged across stream updates
+
+### Example
+
+```dart
+final cartStream = BehaviorSubject<Cart>.seeded(Cart.empty());
+
+ValueStreamConsumer<Cart>(
+  stream: cartStream,
+  buildWhen: (previous, current) => current.itemCount != previous.itemCount,
+  listener: (context, previous, current) {
+    if (current.itemCount > previous.itemCount) {
+      ScaffoldMessenger.of(context).showSnackBar(
+        SnackBar(content: Text('Item added to cart')),
+      );
+    }
+  },
+  builder: (context, cart, child) {
+    return Column(
+      children: [
+        Text('Total items: ${cart.itemCount}'),
+        Text('Total price: \$${cart.totalPrice}'),
+        if (child != null) child, // Use the stable child widget if provided
+      ],
+    );
   },
-);
+  child: const Text('This widget remains stable'), // Optional stable child widget
+)
 ```
 
-### Parameters
+## Error Handling
 
-- **`stream`**: The `ValueStream` to listen to. The stream must have a value at all times and must not emit errors.
-- **`builder`**: A callback that returns the widget to display based on the stream data.
-- **`buildWhen`**: An optional callback that determines whether to rebuild the widget based on the previous and current data. Defaults to `true` (always rebuilds).
+All widgets in this package handle two types of errors:
 
-### Error Handling
+1. `ValueStreamHasNoValueError`: Thrown when the stream doesn't have an initial value
+2. `UnhandledStreamError`: Thrown when the stream emits an error
 
-`ValueStreamBuilder` requires the stream to always have a value and never emit errors. If an error occurs in the stream, it will be displayed using the `ErrorWidget`.
+To avoid these errors:
+- Always use `BehaviorSubject` or another `ValueStream` with an initial value
+- Handle stream errors before they reach these widgets
+- Consider using `stream.handleError()` to transform errors if needed
 
-If the stream has no value when the builder is first called, a `ValueStreamHasNoValueError` will be thrown. You can handle this by ensuring that the stream is seeded with an initial value or by checking if the stream has a value before using `ValueStreamBuilder`.
+Example of proper stream initialization:
+```dart
+// Good - stream has initial value
+final goodStream = BehaviorSubject<int>.seeded(0);
 
+// Bad - stream has no initial value
+final badStream = BehaviorSubject<int>(); // Will throw ValueStreamHasNoValueError
+
+// Bad - stream with error
+final errorStream = BehaviorSubject<int>.seeded(0)..addError(Exception()); // Will throw UnhandledStreamError
+```
diff --git a/packages/rxdart_flutter/example/.gitignore b/packages/rxdart_flutter/example/.gitignore
new file mode 100644
index 000000000..f46d61e22
--- /dev/null
+++ b/packages/rxdart_flutter/example/.gitignore
@@ -0,0 +1,54 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
+
+# Scaffolded code by `flutter create .`
+android/
+ios/
+linux/
+macos/
+web/
+windows/
+test/widget_test.dart
diff --git a/packages/rxdart_flutter/example/.metadata b/packages/rxdart_flutter/example/.metadata
new file mode 100644
index 000000000..b02a7e4bf
--- /dev/null
+++ b/packages/rxdart_flutter/example/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "17025dd88227cd9532c33fa78f5250d548d87e9a"
+  channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: android
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: ios
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: linux
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: macos
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: web
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+    - platform: windows
+      create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+      base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/packages/rxdart_flutter/example/README.md b/packages/rxdart_flutter/example/README.md
new file mode 100644
index 000000000..0f3282c4f
--- /dev/null
+++ b/packages/rxdart_flutter/example/README.md
@@ -0,0 +1,9 @@
+# rxdart_flutter_example
+
+## Getting Started
+
+Generate platform-specific code
+
+```bash
+flutter create .
+```
diff --git a/packages/rxdart_flutter/example/analysis_options.yaml b/packages/rxdart_flutter/example/analysis_options.yaml
new file mode 100644
index 000000000..f9b303465
--- /dev/null
+++ b/packages/rxdart_flutter/example/analysis_options.yaml
@@ -0,0 +1,1 @@
+include: package:flutter_lints/flutter.yaml
diff --git a/packages/rxdart_flutter/example/lib/main.dart b/packages/rxdart_flutter/example/lib/main.dart
new file mode 100644
index 000000000..9a3e98dda
--- /dev/null
+++ b/packages/rxdart_flutter/example/lib/main.dart
@@ -0,0 +1,228 @@
+import 'package:flutter/material.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+
+void main() {
+  runApp(const CounterApp());
+}
+
+class CounterApp extends StatefulWidget {
+  const CounterApp({
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<CounterApp> createState() => _CounterAppState();
+}
+
+class _CounterAppState extends State<CounterApp> {
+  final BehaviorSubject<int> valueStream = BehaviorSubject.seeded(0);
+
+  @override
+  void dispose() {
+    valueStream.close();
+    super.dispose();
+  }
+
+  void _valueStreamListenerListener(
+      BuildContext context, int previous, int current) {
+    if (current % 5 == 0) {
+      final String message = '''
+When value is a multiple of 5.
+
+Previous: $previous
+Current: $current''';
+
+      showDialog(
+        context: context,
+        builder: (context) {
+          return AlertDialog(
+            title: const Text('ValueStreamListener.listener'),
+            content: Text(message),
+            actions: [
+              TextButton(
+                onPressed: () => Navigator.of(context).pop(),
+                child: const Text('OK'),
+              ),
+            ],
+          );
+        },
+      );
+    }
+  }
+
+  void _valueStreamConsumerListener(
+      BuildContext context, int previous, int current) {
+    if (current % 3 == 0) {
+      final String message = '''
+When value is a multiple of 3.
+
+Previous: $previous
+Current: $current''';
+
+      ScaffoldMessenger.of(context).showSnackBar(
+        SnackBar(
+          content: Text(
+            message,
+            style: const TextStyle(
+              color: Colors.white,
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+          backgroundColor: Colors.blue.shade700,
+          duration: const Duration(seconds: 3),
+          action: SnackBarAction(
+            label: 'Dismiss',
+            textColor: Colors.white,
+            onPressed: () {
+              ScaffoldMessenger.of(context).hideCurrentSnackBar();
+            },
+          ),
+        ),
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      theme: ThemeData.dark(),
+      debugShowCheckedModeBanner: false,
+      home: SafeArea(
+        child: Scaffold(
+          body: Builder(builder: (context) {
+            return ValueStreamListener(
+              stream: valueStream,
+              listener: _valueStreamListenerListener,
+              child: ListView(
+                children: [
+                  ValueStreamConsumer(
+                    stream: valueStream,
+                    listener: _valueStreamConsumerListener,
+                    builder: (context, value, child) {
+                      return ValueCard(
+                        title: 'ValueStreamConsumer.builder for any value',
+                        value: value,
+                      );
+                    },
+                  ),
+                  ValueStreamBuilder(
+                    stream: valueStream,
+                    buildWhen: (previous, current) => current.isEven,
+                    builder: (context, value, child) {
+                      return ValueCard(
+                        title: 'ValueStreamBuilder.builder when value is even',
+                        value: value,
+                      );
+                    },
+                  ),
+                  ValueStreamBuilder(
+                    stream: valueStream,
+                    buildWhen: (previous, current) => current.isOdd,
+                    builder: (context, value, child) {
+                      return ValueCard(
+                        title: 'ValueStreamBuilder.builder when value is odd',
+                        value: value,
+                      );
+                    },
+                  ),
+                ],
+              ),
+            );
+          }),
+          floatingActionButton: FloatingActionButton(
+            onPressed: () {
+              valueStream.add(valueStream.value + 1);
+            },
+            child: const Icon(Icons.add),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+enum DataCardValueShape {
+  circle,
+  square,
+}
+
+class ValueCard extends StatelessWidget {
+  const ValueCard({
+    required this.title,
+    required this.value,
+    super.key,
+  });
+
+  final String title;
+  final int value;
+
+  DataCardValueShape get _valueShape {
+    if (value.isOdd) {
+      return DataCardValueShape.circle;
+    }
+    return DataCardValueShape.square;
+  }
+
+  Color get _color {
+    return switch (_valueShape) {
+      DataCardValueShape.circle => Colors.green.shade700,
+      DataCardValueShape.square => Colors.blue.shade700,
+    };
+  }
+
+  double get _size {
+    return switch (_valueShape) {
+      DataCardValueShape.circle => 75,
+      DataCardValueShape.square => 150,
+    };
+  }
+
+  double get _borderRadius {
+    return switch (_valueShape) {
+      DataCardValueShape.circle => _size / 2,
+      DataCardValueShape.square => 0,
+    };
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Card(
+      margin: const EdgeInsets.all(16),
+      child: AnimatedContainer(
+        duration: const Duration(milliseconds: 300),
+        padding: const EdgeInsets.all(16),
+        alignment: Alignment.center,
+        child: Column(
+          children: [
+            Text(
+              title,
+              style: const TextStyle(
+                fontSize: 16,
+                fontWeight: FontWeight.bold,
+              ),
+            ),
+            const SizedBox(height: 16),
+            AnimatedContainer(
+              duration: Durations.medium1,
+              height: _size,
+              width: _size,
+              padding: const EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                color: _color,
+                borderRadius: BorderRadius.circular(_borderRadius),
+              ),
+              child: FittedBox(
+                child: Text(
+                  value.toString(),
+                  style: const TextStyle(
+                    fontWeight: FontWeight.bold,
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/rxdart_flutter/example/pubspec.lock b/packages/rxdart_flutter/example/pubspec.lock
new file mode 100644
index 000000000..b097ba7ac
--- /dev/null
+++ b/packages/rxdart_flutter/example/pubspec.lock
@@ -0,0 +1,85 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.0"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.4"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.1"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.15.0"
+  rxdart:
+    dependency: "direct overridden"
+    description:
+      path: "../../rxdart"
+      relative: true
+    source: path
+    version: "0.28.0"
+  rxdart_flutter:
+    dependency: "direct main"
+    description:
+      path: ".."
+      relative: true
+    source: path
+    version: "0.0.1-dev.1"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+sdks:
+  dart: ">=3.4.0 <4.0.0"
diff --git a/packages/rxdart_flutter/example/pubspec.yaml b/packages/rxdart_flutter/example/pubspec.yaml
new file mode 100644
index 000000000..0bf2e76b7
--- /dev/null
+++ b/packages/rxdart_flutter/example/pubspec.yaml
@@ -0,0 +1,19 @@
+name: rxdart_flutter_example
+description: example for rxdart_flutter
+version: 1.0.0
+publish_to: none
+
+environment:
+  sdk: ">=3.0.0 <4.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  rxdart_flutter:
+    path: ../
+
+dev_dependencies:
+  flutter_lints: ^1.0.4
+
+flutter:
+  uses-material-design: true
diff --git a/packages/rxdart_flutter/lib/rxdart_flutter.dart b/packages/rxdart_flutter/lib/rxdart_flutter.dart
index d41e62e45..f16bd25f9 100644
--- a/packages/rxdart_flutter/lib/rxdart_flutter.dart
+++ b/packages/rxdart_flutter/lib/rxdart_flutter.dart
@@ -1,3 +1,7 @@
 library rxdart_flutter;
 
 export 'src/value_stream_builder.dart';
+export 'src/value_stream_consumer.dart';
+export 'src/value_stream_listener.dart';
+
+export 'package:rxdart/rxdart.dart';
diff --git a/packages/rxdart_flutter/lib/src/errors.dart b/packages/rxdart_flutter/lib/src/errors.dart
new file mode 100644
index 000000000..c8923b612
--- /dev/null
+++ b/packages/rxdart_flutter/lib/src/errors.dart
@@ -0,0 +1,107 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:rxdart/rxdart.dart';
+
+const _bullet = ' • ';
+const _indent = '   ';
+
+/// Error emitted from [ValueStream] when using [ValueStreamBuilder],
+/// [ValueStreamListener], or [ValueStreamConsumer].
+class UnhandledStreamError extends Error {
+  /// Error emitted from [ValueStream].
+  final Object error;
+
+  /// Create an [UnhandledStreamError] from [error].
+  UnhandledStreamError(this.error);
+
+  @override
+  String toString() {
+    return '''${_bullet}Unhandled error emitted from the ValueStream: "$error".
+
+${_bullet}The ValueStreamBuilder, ValueStreamListener, or ValueStreamConsumer requires the ValueStream to never emit any error events.
+
+${_bullet}If you are using a BehaviorSubject, ensure you only call subject.add(data),
+${_indent}and never call subject.addError(error).
+
+${_indent}To handle errors before passing the stream to ValueStreamBuilder, ValueStreamListener, or ValueStreamConsumer, you can use one of the following methods:
+$_indent  $_bullet stream.handleError((error, stackTrace) { })
+$_indent  $_bullet stream.onErrorReturn(value)
+$_indent  $_bullet stream.onErrorReturnWith((error, stackTrace) => value)
+$_indent  $_bullet stream.onErrorResumeNext(otherStream)
+$_indent  $_bullet stream.onErrorResume((error, stackTrace) => otherStream)
+$_indent  $_bullet stream.transform(
+$_indent  $_indent     StreamTransformer.fromHandlers(handleError: (error, stackTrace, sink) {}))
+$_indent  ...
+
+${_bullet}If none of these solutions work, please file a bug at:
+${_indent}https://github.com/ReactiveX/rxdart/issues/new
+''';
+  }
+}
+
+/// Error is thrown when [ValueStream.hasValue] is `false`.
+class ValueStreamHasNoValueError<T> extends Error {
+  final ValueStream<T> stream;
+
+  ValueStreamHasNoValueError(this.stream);
+
+  @override
+  String toString() {
+    return '''${_bullet}ValueStreamBuilder requires `hasValue` of "$stream" to be true.
+${_indent}This means the ValueStream must always have the value.
+
+${_indent}If you are using a BehaviorSubject, use `BehaviorSubject.seeded(value)`
+or call `subject.add(value)` to provide an initial value before using ValueStreamBuilder.
+
+${_indent}Alternatively, you can create a ValueStream with an initial value using:
+$_indent  $_bullet stream.publishValueSeeded(value)
+$_indent  $_bullet stream.shareValueSeeded(value)
+$_indent  ...
+
+${_bullet}Lastly, you can check if the ValueStream has a value before using ValueStreamBuilder, ValueStreamListener, or ValueStreamConsumer, for example:
+$_indent if (valueStream.hasValue) {
+$_indent   return ValueStreamBuilder(...);
+$_indent } else {
+$_indent   return FallBackWidget();
+$_indent }
+
+${_bullet}If none of these solutions work, please file a bug at:
+${_indent}https://github.com/ReactiveX/rxdart/issues/new
+''';
+  }
+}
+
+void reportError(ErrorAndStackTrace error) {
+  FlutterError.reportError(
+    FlutterErrorDetails(
+      exception: error.error,
+      stack: error.stackTrace,
+      library: 'rxdart_flutter',
+    ),
+  );
+}
+
+// @pragma('vm:notify-debugger-on-exception')
+ErrorAndStackTrace? validateValueStreamInitialValue<T>(ValueStream<T> stream) {
+  ErrorAndStackTrace? error;
+
+  if (!stream.hasValue) {
+    if (stream.hasError) {
+      error = ErrorAndStackTrace(
+        UnhandledStreamError(stream.error),
+        stream.stackTrace ?? StackTrace.current,
+      );
+    } else {
+      error = ErrorAndStackTrace(
+        ValueStreamHasNoValueError(stream),
+        stream.stackTrace ?? StackTrace.current,
+      );
+    }
+  }
+
+  if (error != null) {
+    reportError(error);
+  }
+
+  return error;
+}
diff --git a/packages/rxdart_flutter/lib/src/value_stream_builder.dart b/packages/rxdart_flutter/lib/src/value_stream_builder.dart
index 9bd3a62cc..b466ca297 100644
--- a/packages/rxdart_flutter/lib/src/value_stream_builder.dart
+++ b/packages/rxdart_flutter/lib/src/value_stream_builder.dart
@@ -1,41 +1,52 @@
-import 'dart:async';
-
 import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
 import 'package:rxdart/rxdart.dart';
+import 'package:flutter/widgets.dart';
 
-/// Signature for the `builder` function which takes the `BuildContext` and the current `data`
+import 'errors.dart';
+import 'value_stream_listener.dart';
+
+/// Signature for the `builder` function which takes the `BuildContext` and the current `value`
 /// and is responsible for returning a widget which is to be rendered.
 /// This is analogous to the `builder` function in [StreamBuilder].
 typedef ValueStreamWidgetBuilder<T> = Widget Function(
-    BuildContext context, T data);
-
-/// Signature for the `buildWhen` function which takes the previous `data` and
-/// the current `data` and is responsible for returning a [bool] which
-/// determines whether to rebuild [ValueStream] with the current `data`.
+  BuildContext context,
+  T value,
+  Widget? child,
+);
+
+/// Signature for the `buildWhen` function which takes the previous and
+/// current `value` and is responsible for returning a [bool] which
+/// determines whether to rebuild [ValueStreamBuilder] with the current `value`.
 typedef ValueStreamBuilderCondition<S> = bool Function(S previous, S current);
 
 /// {@template value_stream_builder}
-/// Similar to [StreamBuilder], but works with [ValueStream].
+/// [ValueStreamBuilder] handles building a widget in response to new `value`.
+/// [ValueStreamBuilder] is analogous to [StreamBuilder] but has simplified API to
+/// reduce the amount of boilerplate code needed as well as [ValueStream]-specific
+/// performance improvements.
 ///
 /// [ValueStreamBuilder] requires [stream.hasValue] to always be `true`,
-/// and the [stream] does not emit any error events. See [ValueStreamHasNoValueError]
-/// and [UnhandledStreamError] for more information.
+/// and the [stream] does not emit any error events.
+/// See [ValueStreamHasNoValueError] and [UnhandledStreamError]
+/// for more information.
+///
+/// Please refer to [ValueStreamListener] if you want to "do" anything in response to
+/// `value` changes such as navigation, showing a dialog, etc...
 ///
-/// [ValueStreamBuilder] handles building a widget in response to new `data`.
+/// [ValueStreamBuilder] handles building a widget in response to new `value`.
 /// [ValueStreamBuilder] is analogous to [StreamBuilder] but has simplified API to
 /// reduce the amount of boilerplate code needed as well as [ValueStream]-specific
 /// performance improvements.
 ///
-/// ### Example
-/// ```dart
-/// final valueStream = BehaviorSubject<int>.seeded(0);
+/// **Example**
 ///
-/// ValueStreamBuilder<int>(
+/// ```dart
+/// ValueStreamBuilder<T>(
 ///   stream: valueStream,
-///   builder: (context, data) {
-///     // return widget here based on data
+///   builder: (context, value, child) {
+///     // return widget here based on valueStream's value
 ///   },
+///   child: const SizedBox(), // Optional child widget that remains stable
 /// );
 /// ```
 /// {@endtemplate}
@@ -45,45 +56,75 @@ typedef ValueStreamBuilderCondition<S> = bool Function(S previous, S current);
 /// how often [ValueStreamBuilder] rebuilds.
 ///
 /// - [buildWhen] should only be used for performance optimizations as it
-/// provides no security about the data passed to the [builder] function.
-/// - [buildWhen] will be invoked on each [ValueStream] `data` change.
-/// - [buildWhen] takes the previous `data` and current `data` and must
+/// provides no security about the value passed to the [builder] function.
+/// - [buildWhen] will be invoked on each [stream] `value` change.
+/// - [buildWhen] takes the previous `value` and current `value` and must
 /// return a [bool] which determines whether or not the [builder] function will
 /// be invoked.
-/// - The previous `data` will be initialized to the `data` of the [ValueStream] when
+/// - The previous `value` will be initialized to the `value` of the [stream] when
 /// the [ValueStreamBuilder] is initialized.
 ///
 /// [buildWhen] is optional and if omitted, it will default to `true`.
 ///
-/// ### Example
+/// [child] is optional but is good practice to use if part of
+/// the widget subtree does not depend on the value of the [stream].
+///
+/// **Example**
+///
 /// ```dart
-/// ValueStreamBuilder<Data>(
+/// ValueStreamBuilder<T>(
+///   stream: valueStream,
 ///   buildWhen: (previous, current) {
 ///     // return true/false to determine whether or not
-///     // to rebuild the widget with data
+///     // to rebuild the widget with valueStream's value
 ///   },
-///   builder: (context, data) {
-///     // return widget here based on data
-///   }
+///   builder: (context, value, child) {
+///     // return widget here based on valueStream's value
+///   },
+///   child: const SizedBox(), // Optional child widget that remains stable
 /// )
 /// ```
 /// {@endtemplate}
 class ValueStreamBuilder<T> extends StatefulWidget {
-  final ValueStreamWidgetBuilder<T> _builder;
-  final ValueStream<T> _stream;
-  final ValueStreamBuilderCondition<T>? _buildWhen;
-
   /// {@macro value_stream_builder}
   /// {@macro value_stream_builder_build_when}
   const ValueStreamBuilder({
     Key? key,
-    required ValueStream<T> stream,
-    required ValueStreamWidgetBuilder<T> builder,
-    ValueStreamBuilderCondition<T>? buildWhen,
-  })  : _builder = builder,
-        _stream = stream,
-        _buildWhen = buildWhen,
-        super(key: key);
+    required this.stream,
+    required this.builder,
+    this.buildWhen,
+    this.child,
+    this.isReplayValueStream = true,
+  }) : super(key: key);
+
+  /// The [ValueStream] that the [ValueStreamBuilder] will interact with.
+  final ValueStream<T> stream;
+
+  /// The [builder] function which will be invoked on each widget build.
+  /// The [builder] takes the `BuildContext` and current `value` and
+  /// must return a widget.
+  /// This is analogous to the [builder] function in [StreamBuilder].
+  final ValueStreamWidgetBuilder<T> builder;
+
+  /// Takes the previous `value` and the current `value` and is responsible for
+  /// returning a [bool] which determines whether or not to trigger
+  /// [builder] with the current `value`.
+  final ValueStreamBuilderCondition<T>? buildWhen;
+
+  /// A [ValueStream]-independent widget which is passed back to the [builder].
+  ///
+  /// This argument is optional and can be null if the entire widget subtree the
+  /// [builder] builds depends on the value of the [stream]. For
+  /// example, in the case where the [stream] is a [String] and the
+  /// [builder] returns a [Text] widget with the current [String] value, there
+  /// would be no useful [child].
+  final Widget? child;
+
+  /// Whether or not the [stream] emits the last value
+  /// like [BehaviorSubject] does.
+  ///
+  /// Defaults to `true`.
+  final bool isReplayValueStream;
 
   @override
   State<ValueStreamBuilder<T>> createState() => _ValueStreamBuilderState();
@@ -92,182 +133,51 @@ class ValueStreamBuilder<T> extends StatefulWidget {
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     super.debugFillProperties(properties);
     properties
-      ..add(DiagnosticsProperty<ValueStream<T>>('stream', _stream))
-      ..add(ObjectFlagProperty<ValueStreamWidgetBuilder<T>>.has(
-          'builder', _builder))
+      ..add(DiagnosticsProperty<ValueStream<T>>('stream', stream))
       ..add(
-        ObjectFlagProperty<ValueStreamBuilderCondition<T>?>.has(
-          'buildWhen',
-          _buildWhen,
-        ),
-      );
+          DiagnosticsProperty<bool>('isReplayValueStream', isReplayValueStream))
+      ..add(ObjectFlagProperty<ValueStreamBuilderCondition<T>?>.has(
+        'buildWhen',
+        buildWhen,
+      ))
+      ..add(ObjectFlagProperty<ValueStreamWidgetBuilder<T>>.has(
+        'builder',
+        builder,
+      ))
+      ..add(ObjectFlagProperty<Widget?>.has('child', child));
   }
 }
 
 class _ValueStreamBuilderState<T> extends State<ValueStreamBuilder<T>> {
-  late T currentData;
-  StreamSubscription<T>? subscription;
-  ErrorAndStackTrace? error;
+  late T _currentValue;
+  ErrorAndStackTrace? _error;
 
   @override
   void initState() {
     super.initState();
-    subscribe();
-  }
-
-  @override
-  void didUpdateWidget(covariant ValueStreamBuilder<T> oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (oldWidget._stream != widget._stream) {
-      unsubscribe();
-      subscribe();
+    _error = validateValueStreamInitialValue(widget.stream);
+    if (_error != null) {
+      return;
     }
-  }
-
-  @override
-  void dispose() {
-    unsubscribe();
-    super.dispose();
+    _currentValue = widget.stream.value;
   }
 
   @override
   Widget build(BuildContext context) {
-    if (error != null) {
-      return ErrorWidget(error!.error);
+    if (_error != null) {
+      return ErrorWidget(_error!.error);
     }
-    return widget._builder(context, currentData);
-  }
-
-  @pragma('vm:notify-debugger-on-exception')
-  void subscribe() {
-    final stream = widget._stream;
-
-    try {
-      currentData = stream.value;
-      error = null;
-    } on ValueStreamError catch (e, s) {
-      error = ErrorAndStackTrace(ValueStreamHasNoValueError(stream), s);
-      reportError();
-      return;
-    } catch (e, s) {
-      error = ErrorAndStackTrace(e, s);
-      reportError();
-      return;
-    }
-
-    final buildWhen = widget._buildWhen;
-
-    assert(subscription == null, 'The stream is already listened to!');
-    subscription = stream.listen(
-      (newData) {
-        final previousData = currentData;
-        currentData = newData;
-        if (buildWhen == null || buildWhen(previousData, newData)) {
-          setState(emptyFn);
+    return ValueStreamListener<T>(
+      stream: widget.stream,
+      isReplayValueStream: widget.isReplayValueStream,
+      listener: (context, previous, current) {
+        if (widget.buildWhen?.call(previous, current) ?? true) {
+          setState(() {
+            _currentValue = current;
+          });
         }
       },
-      onError: (Object e, StackTrace s) {
-        error = ErrorAndStackTrace(UnhandledStreamError(e), s);
-        reportError();
-        setState(emptyFn);
-      },
+      child: widget.builder(context, _currentValue, widget.child),
     );
   }
-
-  void unsubscribe() {
-    subscription?.cancel();
-    subscription = null;
-  }
-
-  @override
-  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
-    super.debugFillProperties(properties);
-    properties.add(DiagnosticsProperty.lazy('data', () => currentData));
-    properties.add(DiagnosticsProperty('error', error));
-    properties.add(DiagnosticsProperty('subscription', subscription));
-  }
-
-  static void emptyFn() {}
-
-  void reportError() {
-    final error = this.error;
-    if (error == null) {
-      return;
-    }
-    FlutterError.reportError(
-      FlutterErrorDetails(
-        exception: error.error,
-        stack: error.stackTrace,
-        library: 'rxdart_flutter',
-      ),
-    );
-  }
-}
-
-const _bullet = ' • ';
-const _indent = '   ';
-
-/// Error emitted from [ValueStream] when using [ValueStreamBuilder].
-class UnhandledStreamError extends Error {
-  /// Error emitted from [ValueStream].
-  final Object error;
-
-  /// Create an [UnhandledStreamError] from [error].
-  UnhandledStreamError(this.error);
-
-  @override
-  String toString() {
-    return '''${_bullet}Unhandled error emitted from the ValueStream: "$error".
-
-${_bullet}The ValueStreamBuilder requires the ValueStream to never emit any error events.
-
-${_bullet}If you are using a BehaviorSubject, ensure you only call subject.add(data),
-${_indent}and never call subject.addError(error).
-
-${_indent}To handle errors before passing the stream to ValueStreamBuilder, you can use one of the following methods:
-$_indent  $_bullet stream.handleError((error, stackTrace) { })
-$_indent  $_bullet stream.onErrorReturn(value)
-$_indent  $_bullet stream.onErrorReturnWith((error, stackTrace) => value)
-$_indent  $_bullet stream.onErrorResumeNext(otherStream)
-$_indent  $_bullet stream.onErrorResume((error, stackTrace) => otherStream)
-$_indent  $_bullet stream.transform(
-$_indent  $_indent     StreamTransformer.fromHandlers(handleError: (error, stackTrace, sink) {}))
-$_indent  ...
-
-${_bullet}If none of these solutions work, please file a bug at:
-${_indent}https://github.com/ReactiveX/rxdart/issues/new
-''';
-  }
-}
-
-/// Error is thrown when [ValueStream.hasValue] is `false`.
-class ValueStreamHasNoValueError<T> extends Error {
-  final ValueStream<T> stream;
-
-  ValueStreamHasNoValueError(this.stream);
-
-  @override
-  String toString() {
-    return '''${_bullet}ValueStreamBuilder requires `hasValue` of "$stream" to be true.
-${_indent}This means the ValueStream must always have the value.
-
-${_indent}If you are using a BehaviorSubject, use `BehaviorSubject.seeded(value)` 
-or call `subject.add(value)` to provide an initial value before using ValueStreamBuilder.
-
-${_indent}Alternatively, you can create a ValueStream with an initial value using:
-$_indent  $_bullet stream.publishValueSeeded(value)
-$_indent  $_bullet stream.shareValueSeeded(value)
-$_indent  ...
-
-${_bullet}Lastly, you can check if the ValueStream has a value before using ValueStreamBuilder, for example:
-$_indent if (valueStream.hasValue) {
-$_indent   return ValueStreamBuilder(...);
-$_indent } else {
-$_indent   return FallBackWidget();
-$_indent }
-
-${_bullet}If none of these solutions work, please file a bug at:
-${_indent}https://github.com/ReactiveX/rxdart/issues/new
-''';
-  }
 }
diff --git a/packages/rxdart_flutter/lib/src/value_stream_consumer.dart b/packages/rxdart_flutter/lib/src/value_stream_consumer.dart
new file mode 100644
index 000000000..a2f9fae43
--- /dev/null
+++ b/packages/rxdart_flutter/lib/src/value_stream_consumer.dart
@@ -0,0 +1,184 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:rxdart/rxdart.dart';
+
+import 'errors.dart';
+import 'value_stream_builder.dart';
+import 'value_stream_listener.dart';
+
+/// {@template value_stream_consumer}
+/// [ValueStreamConsumer] exposes a [builder] and [listener] to react to new
+/// values from a [stream].
+///
+/// [ValueStreamConsumer] is analogous to a nested [ValueStreamListener] and
+/// [ValueStreamBuilder] but reduces the amount of boilerplate needed.
+///
+/// [ValueStreamConsumer] requires [stream.hasValue] to always be `true`,
+/// and the [stream] does not emit any error events.
+/// See [ValueStreamHasNoValueError] and [UnhandledStreamError]
+/// for more information.
+///
+/// [ValueStreamConsumer] should only be used when it is necessary to both
+/// rebuild UI and execute other reactions to value changes in the [stream].
+///
+/// [ValueStreamConsumer] takes a required  `ValueStream`,
+/// `ValueStreamWidgetListener` and  `ValueStreamWidgetBuilder`
+/// and an optional `ValueStreamBuilderCondition`.
+///
+/// **Example**
+///
+/// ```dart
+/// ValueStreamConsumer<T>(
+///   stream: valueStream,
+///   listener: (context, previous, current) {
+///     // do stuff here based on valueStream's
+///     // previous and current values
+///   },
+///   builder: (context, value, child) {
+///     // Build widget based on valueStream's value
+///   },
+///   child: const SizedBox(), // Optional child widget that remains stable
+/// )
+/// ```
+///
+/// An optional [buildWhen]  can be implemented for more
+/// granular control over when [builder] is called.
+/// The [buildWhen] will be invoked on each [stream] `value`
+/// change.
+/// It takes the previous `value` and current `value` and must return
+/// a [bool] which determines whether or not the [builder]
+/// function will be invoked.
+/// The previous `value` will be initialized to the `value` of the [stream] when
+/// the [ValueStreamConsumer] is initialized.
+/// [buildWhen] is optional and if it isn't implemented,
+/// it will default to `true`.
+///
+/// [child] is optional but is good practice to use if part of
+/// the widget subtree does not depend on the value of the [stream].
+///
+/// **Example**
+///
+/// ```dart
+/// ValueStreamConsumer<T>(
+///   stream: valueStream,
+///   listener: (context, previous, current) {
+///     // do stuff here based on valueStream's
+///     // previous and current values
+///   },
+///   buildWhen: (previous, current) {
+///     // return true/false to determine whether or not
+///     // to rebuild the widget with valueStream's value
+///   },
+///   builder: (context, value, child) {
+///     // Build widget based on valueStream's value
+///   },
+///   child: const SizedBox(), // Optional child widget that remains stable
+/// )
+/// ```
+/// {@endtemplate}
+class ValueStreamConsumer<T> extends StatefulWidget {
+  /// {@macro value_stream_consumer}
+  const ValueStreamConsumer({
+    Key? key,
+    required this.stream,
+    required this.listener,
+    required this.builder,
+    this.buildWhen,
+    this.child,
+    this.isReplayValueStream = true,
+  }) : super(key: key);
+
+  /// The [ValueStream] that the [ValueStreamConsumer] will interact with.
+  final ValueStream<T> stream;
+
+  /// The [builder] function which will be invoked on each widget build.
+  /// The [builder] takes the `BuildContext` and current `value` and
+  /// must return a widget.
+  /// This is analogous to the [builder] function in [StreamBuilder].
+  final ValueStreamWidgetBuilder<T> builder;
+
+  /// Takes the `BuildContext` along with the `previous` and `current` values
+  ///  and is responsible for executing in response to `value` changes.
+  final ValueStreamWidgetListener<T> listener;
+
+  /// Takes the previous `value` and the current `value` and is responsible for
+  /// returning a [bool] which determines whether or not to trigger
+  /// [builder] with the current `value`.
+  final ValueStreamBuilderCondition<T>? buildWhen;
+
+  /// A [ValueStream]-independent widget which is passed back to the [builder].
+  ///
+  /// This argument is optional and can be null if the entire widget subtree the
+  /// [builder] builds depends on the value of the [stream]. For
+  /// example, in the case where the [stream] is a [String] and the
+  /// [builder] returns a [Text] widget with the current [String] value, there
+  /// would be no useful [child].
+  final Widget? child;
+
+  /// Whether or not the [stream] emits the last value
+  /// like [BehaviorSubject] does.
+  ///
+  /// Defaults to `true`.
+  final bool isReplayValueStream;
+
+  @override
+  State<ValueStreamConsumer<T>> createState() => _ValueStreamConsumerState<T>();
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty<ValueStream<T>>('stream', stream))
+      ..add(
+          DiagnosticsProperty<bool>('isReplayValueStream', isReplayValueStream))
+      ..add(ObjectFlagProperty<ValueStreamWidgetBuilder<T>>.has(
+        'builder',
+        builder,
+      ))
+      ..add(ObjectFlagProperty<ValueStreamBuilderCondition<T>?>.has(
+        'buildWhen',
+        buildWhen,
+      ))
+      ..add(ObjectFlagProperty<ValueStreamWidgetListener<T>>.has(
+        'listener',
+        listener,
+      ))
+      ..add(ObjectFlagProperty<Widget?>.has('child', child));
+  }
+}
+
+class _ValueStreamConsumerState<T> extends State<ValueStreamConsumer<T>> {
+  late T _currentValue;
+  ErrorAndStackTrace? _error;
+
+  @override
+  void initState() {
+    super.initState();
+    _error = validateValueStreamInitialValue(widget.stream);
+    if (_error != null) {
+      return;
+    }
+    _currentValue = widget.stream.value;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_error != null) {
+      return ErrorWidget(_error!.error);
+    }
+    return ValueStreamListener<T>(
+      stream: widget.stream,
+      isReplayValueStream: widget.isReplayValueStream,
+      listener: (context, previous, current) {
+        widget.listener(context, previous, current);
+
+        if (widget.buildWhen?.call(previous, current) ?? true) {
+          setState(() {
+            _currentValue = current;
+          });
+        }
+      },
+      child: widget.builder(context, _currentValue, widget.child),
+    );
+  }
+}
diff --git a/packages/rxdart_flutter/lib/src/value_stream_listener.dart b/packages/rxdart_flutter/lib/src/value_stream_listener.dart
new file mode 100644
index 000000000..e4b328681
--- /dev/null
+++ b/packages/rxdart_flutter/lib/src/value_stream_listener.dart
@@ -0,0 +1,176 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:rxdart/rxdart.dart';
+
+import 'errors.dart';
+
+/// Signature for the `listener` function which takes the `BuildContext` along
+/// with the previous and current `value` and is responsible for
+/// executing in response to `value` changes.
+typedef ValueStreamWidgetListener<T> = void Function(
+  BuildContext context,
+  T previous,
+  T current,
+);
+
+/// {@template value_stream_listener}
+/// Takes a [ValueStreamWidgetListener] and a [stream] and invokes
+/// the [listener] in response to `value` changes in the [stream].
+///
+/// It should be used for functionality that needs to occur only in response to
+/// a `value` change such as navigation, showing a `SnackBar`, showing
+/// a `Dialog`, etc...
+///
+/// The [listener] is guaranteed to only be called once for each `value` change
+/// unlike the `builder` in `ValueStreamBuilder`.
+///
+/// [ValueStreamListener] requires [stream.hasValue] to always be `true`,
+/// and the [stream] does not emit any error events.
+/// See [ValueStreamHasNoValueError] and [UnhandledStreamError]
+/// for more information.
+///
+/// **Example**
+///
+/// ```dart
+/// ValueStreamListener<T>(
+///   stream: valueStream,
+///   listener: (context, previous, current) {
+///     // do stuff here based on valueStream's
+///     // previous and current values
+///   },
+///   child: Container(),
+/// )
+/// ```
+/// {@endtemplate}
+class ValueStreamListener<T> extends StatefulWidget {
+  /// {@macro value_stream_listener}
+  const ValueStreamListener({
+    Key? key,
+    required this.stream,
+    required this.listener,
+    required this.child,
+    this.isReplayValueStream = true,
+  }) : super(key: key);
+
+  /// The [ValueStream] that the [ValueStreamConsumer] will interact with.
+  final ValueStream<T> stream;
+
+  /// Takes the `BuildContext` along with the `previous` and `current` values
+  ///  and is responsible for executing in response to `value` changes.
+  final ValueStreamWidgetListener<T> listener;
+
+  /// The widget which will be rendered as a descendant of the
+  /// [ValueStreamListener].
+  final Widget child;
+
+  /// Whether or not the [stream] emits the last value
+  /// like [BehaviorSubject] does.
+  ///
+  /// Defaults to `true`.
+  final bool isReplayValueStream;
+
+  @override
+  State<ValueStreamListener<T>> createState() => _ValueStreamListenerState<T>();
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties
+      ..add(DiagnosticsProperty<ValueStream<T>>('stream', stream))
+      ..add(
+          DiagnosticsProperty<bool>('isReplayValueStream', isReplayValueStream))
+      ..add(ObjectFlagProperty<ValueStreamWidgetListener<T>>.has(
+          'listener', listener))
+      ..add(ObjectFlagProperty<Widget>.has('child', child));
+  }
+}
+
+class _ValueStreamListenerState<T> extends State<ValueStreamListener<T>> {
+  StreamSubscription<T>? _subscription;
+  late T _currentValue;
+  ErrorAndStackTrace? _error;
+  bool _initialized = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _subscribe();
+    _initialized = true;
+  }
+
+  @override
+  void didUpdateWidget(covariant ValueStreamListener<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.stream != oldWidget.stream) {
+      _unsubscribe();
+      _subscribe();
+    }
+  }
+
+  @override
+  void dispose() {
+    _unsubscribe();
+    super.dispose();
+  }
+
+  void _subscribe() {
+    final stream = widget.stream;
+
+    _error = validateValueStreamInitialValue(stream);
+    if (_error != null) {
+      return;
+    }
+
+    if (!_initialized) {
+      _currentValue = stream.value;
+    }
+
+    final int skipCount;
+
+    if (widget.isReplayValueStream) {
+      skipCount = _initialized ? 0 : 1;
+    } else {
+      skipCount = 0;
+      if (_initialized) {
+        WidgetsBinding.instance.addPostFrameCallback((_) {
+          _notifyListener(stream.value);
+        });
+      }
+    }
+
+    final streamToListen = skipCount > 0 ? stream.skip(skipCount) : stream;
+
+    _subscription = streamToListen.listen(
+      (value) {
+        if (!mounted) return;
+        _notifyListener(value);
+      },
+      onError: (Object e, StackTrace s) {
+        if (!mounted) return;
+        _error = ErrorAndStackTrace(UnhandledStreamError(e), s);
+        reportError(_error!);
+        setState(() {});
+      },
+    );
+  }
+
+  void _notifyListener(T value) {
+    final previousValue = _currentValue;
+    _currentValue = value;
+    widget.listener(context, previousValue, value);
+  }
+
+  void _unsubscribe() {
+    _subscription?.cancel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_error != null) {
+      return ErrorWidget(_error!.error);
+    }
+    return widget.child;
+  }
+}
diff --git a/packages/rxdart_flutter/pubspec.yaml b/packages/rxdart_flutter/pubspec.yaml
index aa111ab17..2941f5928 100644
--- a/packages/rxdart_flutter/pubspec.yaml
+++ b/packages/rxdart_flutter/pubspec.yaml
@@ -16,6 +16,7 @@ dev_dependencies:
   flutter_test:
     sdk: flutter
   flutter_lints: ^1.0.4
+  rxdart_ext: ^0.3.0
 
 topics:
   - rxdart


Test patch:
diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart
new file mode 100644
index 000000000..6f225452e
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_builder_test.dart
@@ -0,0 +1,689 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_ext/rxdart_ext.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+class BuilderApp<T> extends StatefulWidget {
+  const BuilderApp({
+    required this.stream1,
+    this.stream2,
+    this.buildWhen,
+    this.onBuild,
+    this.child,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamBuilderCondition<T>? buildWhen;
+  final void Function()? onBuild;
+  final Widget? child;
+
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<BuilderApp<T>> createState() => _BuilderAppState<T>();
+}
+
+class _BuilderAppState<T> extends State<BuilderApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamBuilder<T>(
+      stream: stream,
+      isReplayValueStream: false,
+      buildWhen: widget.buildWhen,
+      child: widget.child,
+      builder: (context, value, child) {
+        widget.onBuild?.call();
+        return MaterialApp(
+          key: BuilderApp.materialAppKey,
+          home: Column(
+            children: [
+              Text('$value'),
+              if (child != null) child,
+              TextButton(
+                key: BuilderApp.toggleStreamButtonKey,
+                child: const Text('Toggle Stream'),
+                onPressed: toggleStream,
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamBuilder', () {
+    testWidgets('renders initial value from stream - T Non Nullable',
+        (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(find.text('2'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          child: child,
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when switching between streams', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = ValueSubject<int>(100);
+
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('100'), findsOneWidget);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamBuilder<int>(
+        stream: ValueSubject<int>(0),
+        isReplayValueStream: false,
+        builder: (context, value, child) => const SizedBox(),
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          'stream: Instance of '
+              "'ValueSubject<int>'",
+          'isReplayValueStream: false',
+          'has buildWhen',
+          'has builder',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamBuilder - T Nullable', () {
+    testWidgets('renders initial value from stream - T Nullable',
+        (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          child: child,
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 5);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when switching between streams', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = ValueSubject<int?>(null);
+
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamBuilder<int?>(
+        stream: ValueSubject<int?>(null),
+        isReplayValueStream: false,
+        builder: (context, value, child) => const SizedBox(),
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'ValueSubject<int?>'",
+          'isReplayValueStream: false',
+          'has buildWhen',
+          'has builder',
+          'has child',
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart
new file mode 100644
index 000000000..26cdeacca
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_consumer_test.dart
@@ -0,0 +1,963 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_ext/rxdart_ext.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+class ConsumerApp<T> extends StatefulWidget {
+  const ConsumerApp({
+    required this.stream1,
+    this.stream2,
+    this.buildWhen,
+    required this.listener,
+    this.onBuild,
+    this.child,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamBuilderCondition<T>? buildWhen;
+  final ValueStreamWidgetListener<T> listener;
+  final void Function()? onBuild;
+  final Widget? child;
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<ConsumerApp<T>> createState() => _ConsumerAppState<T>();
+}
+
+class _ConsumerAppState<T> extends State<ConsumerApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamConsumer<T>(
+      stream: stream,
+      isReplayValueStream: false,
+      buildWhen: widget.buildWhen,
+      listener: widget.listener,
+      child: widget.child,
+      builder: (context, value, child) {
+        widget.onBuild?.call();
+        return MaterialApp(
+          key: ConsumerApp.materialAppKey,
+          home: Column(
+            children: [
+              Text('$value'),
+              if (child != null) child,
+              TextButton(
+                key: ConsumerApp.toggleStreamButtonKey,
+                child: const Text('Toggle Stream'),
+                onPressed: toggleStream,
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamConsumer - T Non Nullable', () {
+    // Builder related tests
+
+    testWidgets('renders initial value from stream', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(find.text('2'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          child: child,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Listener related tests
+
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 2]);
+    });
+
+    // Combined functionality tests
+
+    testWidgets(
+        'calls listener and rebuilds correctly when switching between streams',
+        (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = ValueSubject<int>(100);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+          listener: (_, previous, current) {
+            numListenerCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('100'), findsOneWidget);
+      expect(numBuilds, 4);
+      expect(numListenerCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 100]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+      expect(numBuilds, 5);
+      expect(numListenerCalls, 3);
+      expect(previousValues, [0, 1, 100]);
+      expect(currentValues, [1, 100, 200]);
+    });
+
+    testWidgets(
+        'buildWhen affects only rebuilds while listener is always called',
+        (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) => numListenerCalls++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(numListenerCalls, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Error handling tests
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamConsumer<int>(
+        stream: ValueSubject<int>(0),
+        isReplayValueStream: false,
+        builder: (context, value, child) => const SizedBox(),
+        listener: (context, previous, current) {},
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'ValueSubject<int>'",
+          'isReplayValueStream: false',
+          'has builder',
+          'has buildWhen',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamConsumer - T Nullable', () {
+    // Builder related tests
+
+    testWidgets('renders initial value from stream', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          child: child,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 5);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Listener related tests
+
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+    });
+
+    // Combined functionality tests
+
+    testWidgets(
+        'calls listener and rebuilds correctly when switching between streams',
+        (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+          listener: (_, previous, current) {
+            numListenerCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 4);
+      expect(numListenerCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+      expect(numBuilds, 5);
+      expect(numListenerCalls, 3);
+      expect(previousValues, [null, 1, null]);
+      expect(currentValues, [1, null, 200]);
+    });
+
+    testWidgets(
+        'buildWhen affects only rebuilds while listener is always called',
+        (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) => numListenerCalls++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 3);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(numListenerCalls, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Error handling tests
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamConsumer<int?>(
+        stream: ValueSubject<int?>(null),
+        isReplayValueStream: false,
+        builder: (context, value, child) => const SizedBox(),
+        listener: (context, previous, current) {},
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'ValueSubject<int?>'",
+          'isReplayValueStream: false',
+          'has builder',
+          'has buildWhen',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart b/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart
new file mode 100644
index 000000000..de5d59b6d
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/not_replay_value_stream_listener_test.dart
@@ -0,0 +1,566 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_ext/rxdart_ext.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+typedef Value<T> = void Function(T previous, T current);
+
+class ListenerApp<T> extends StatefulWidget {
+  const ListenerApp({
+    required this.stream1,
+    this.stream2,
+    required this.listener,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamWidgetListener<T> listener;
+
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<ListenerApp<T>> createState() => _ListenerAppState<T>();
+}
+
+class _ListenerAppState<T> extends State<ListenerApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamListener<T>(
+      stream: stream,
+      isReplayValueStream: false,
+      listener: widget.listener,
+      child: MaterialApp(
+        key: ListenerApp.materialAppKey,
+        home: Column(
+          children: [
+            TextButton(
+              key: ListenerApp.toggleStreamButtonKey,
+              child: const Text('Toggle Stream'),
+              onPressed: toggleStream,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamListener - T Non Nullable', () {
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 2]);
+    });
+
+    testWidgets('calls listener with correct values when switching streams',
+        (tester) async {
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = ValueSubject<int>(100);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 100]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(numCalls, 3);
+      expect(previousValues, [0, 1, 100]);
+      expect(currentValues, [1, 100, 200]);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int>(0);
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamListener<int>(
+        stream: ValueSubject<int>(0),
+        isReplayValueStream: false,
+        listener: (context, previous, current) {},
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'ValueSubject<int>'",
+          'isReplayValueStream: false',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamListener - T Nullable', () {
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+    });
+
+    testWidgets('calls listener with correct values when switching streams',
+        (tester) async {
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = ValueSubject<int?>(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(numCalls, 3);
+      expect(previousValues, [null, 1, null]);
+      expect(currentValues, [1, null, 200]);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = ValueSubject<int?>(null);
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamListener<int?>(
+        stream: ValueSubject<int?>(null),
+        isReplayValueStream: false,
+        listener: (context, previous, current) {},
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'ValueSubject<int?>'",
+          'isReplayValueStream: false',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/rxdart_flutter/test/src/value_stream_builder_test.dart b/packages/rxdart_flutter/test/src/value_stream_builder_test.dart
new file mode 100644
index 000000000..04a021ce1
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/value_stream_builder_test.dart
@@ -0,0 +1,685 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+class BuilderApp<T> extends StatefulWidget {
+  const BuilderApp({
+    required this.stream1,
+    this.stream2,
+    this.buildWhen,
+    this.onBuild,
+    this.child,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamBuilderCondition<T>? buildWhen;
+  final void Function()? onBuild;
+  final Widget? child;
+
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<BuilderApp<T>> createState() => _BuilderAppState<T>();
+}
+
+class _BuilderAppState<T> extends State<BuilderApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamBuilder<T>(
+      stream: stream,
+      buildWhen: widget.buildWhen,
+      child: widget.child,
+      builder: (context, value, child) {
+        widget.onBuild?.call();
+        return MaterialApp(
+          key: BuilderApp.materialAppKey,
+          home: Column(
+            children: [
+              Text('$value'),
+              if (child != null) child,
+              TextButton(
+                key: BuilderApp.toggleStreamButtonKey,
+                child: const Text('Toggle Stream'),
+                onPressed: toggleStream,
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamBuilder', () {
+    testWidgets('renders initial value from stream - T Non Nullable',
+        (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(find.text('2'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          child: child,
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when switching between streams', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>.seeded(100);
+
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('100'), findsOneWidget);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+
+      await tester.pumpWidget(
+        BuilderApp<int>(
+          stream1: stream1,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamBuilder<int>(
+        stream: BehaviorSubject<int>.seeded(0),
+        builder: (context, value, child) => const SizedBox(),
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          'stream: Instance of '
+              "'BehaviorSubject<int>'",
+          'isReplayValueStream: true',
+          'has buildWhen',
+          'has builder',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamBuilder - T Nullable', () {
+    testWidgets('renders initial value from stream - T Nullable',
+        (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(stream1: stream1, onBuild: () => numBuilds++),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          child: child,
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 5);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when switching between streams', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>.seeded(null);
+
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(BuilderApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+
+      await tester.pumpWidget(
+        BuilderApp<int?>(
+          stream1: stream1,
+        ),
+      );
+
+      expect(find.byKey(BuilderApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(BuilderApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamBuilder<int?>(
+        stream: BehaviorSubject<int?>.seeded(null),
+        builder: (context, value, child) => const SizedBox(),
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'BehaviorSubject<int?>'",
+          'isReplayValueStream: true',
+          'has buildWhen',
+          'has builder',
+          'has child',
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/rxdart_flutter/test/src/value_stream_consumer_test.dart b/packages/rxdart_flutter/test/src/value_stream_consumer_test.dart
new file mode 100644
index 000000000..5b1bfbea5
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/value_stream_consumer_test.dart
@@ -0,0 +1,959 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+class ConsumerApp<T> extends StatefulWidget {
+  const ConsumerApp({
+    required this.stream1,
+    this.stream2,
+    this.buildWhen,
+    required this.listener,
+    this.onBuild,
+    this.child,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamBuilderCondition<T>? buildWhen;
+  final ValueStreamWidgetListener<T> listener;
+  final void Function()? onBuild;
+  final Widget? child;
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<ConsumerApp<T>> createState() => _ConsumerAppState<T>();
+}
+
+class _ConsumerAppState<T> extends State<ConsumerApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamConsumer<T>(
+      stream: stream,
+      buildWhen: widget.buildWhen,
+      listener: widget.listener,
+      child: widget.child,
+      builder: (context, value, child) {
+        widget.onBuild?.call();
+        return MaterialApp(
+          key: ConsumerApp.materialAppKey,
+          home: Column(
+            children: [
+              Text('$value'),
+              if (child != null) child,
+              TextButton(
+                key: ConsumerApp.toggleStreamButtonKey,
+                child: const Text('Toggle Stream'),
+                onPressed: toggleStream,
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamConsumer - T Non Nullable', () {
+    // Builder related tests
+
+    testWidgets('renders initial value from stream', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(find.text('2'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          child: child,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Listener related tests
+
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 2]);
+    });
+
+    // Combined functionality tests
+
+    testWidgets(
+        'calls listener and rebuilds correctly when switching between streams',
+        (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>.seeded(100);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+          listener: (_, previous, current) {
+            numListenerCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      expect(find.text('0'), findsOneWidget);
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('100'), findsOneWidget);
+      expect(numBuilds, 4);
+      expect(numListenerCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 100]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+      expect(numBuilds, 5);
+      expect(numListenerCalls, 3);
+      expect(previousValues, [0, 1, 100]);
+      expect(currentValues, [1, 100, 200]);
+    });
+
+    testWidgets(
+        'buildWhen affects only rebuilds while listener is always called',
+        (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) => numListenerCalls++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+      expect(find.text('0'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(numListenerCalls, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Error handling tests
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+
+      await tester.pumpWidget(
+        ConsumerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamConsumer<int>(
+        stream: BehaviorSubject<int>.seeded(0),
+        builder: (context, value, child) => const SizedBox(),
+        listener: (context, previous, current) {},
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'BehaviorSubject<int>'",
+          'isReplayValueStream: true',
+          'has builder',
+          'has buildWhen',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamConsumer - T Nullable', () {
+    // Builder related tests
+
+    testWidgets('renders initial value from stream', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+    });
+
+    testWidgets('rebuilds widget when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 3);
+    });
+
+    testWidgets('passes child parameter to builder', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      const child = Text('child');
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          child: child,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+
+      expect(find.text('1'), findsOneWidget);
+      expect(find.text('child'), findsOneWidget);
+    });
+
+    testWidgets('skips rebuild when buildWhen returns false', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    testWidgets('rebuilds when buildWhen returns true', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => true,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(find.text('2'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 4);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 5);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Listener related tests
+
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+    });
+
+    // Combined functionality tests
+
+    testWidgets(
+        'calls listener and rebuilds correctly when switching between streams',
+        (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          onBuild: () => numBuilds++,
+          listener: (_, previous, current) {
+            numListenerCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(find.text('1'), findsOneWidget);
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(find.text('null'), findsOneWidget);
+      expect(numBuilds, 4);
+      expect(numListenerCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(find.text('200'), findsOneWidget);
+      expect(numBuilds, 5);
+      expect(numListenerCalls, 3);
+      expect(previousValues, [null, 1, null]);
+      expect(currentValues, [1, null, 200]);
+    });
+
+    testWidgets(
+        'buildWhen affects only rebuilds while listener is always called',
+        (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numBuilds = 0;
+      var numListenerCalls = 0;
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          buildWhen: (previous, current) => current != null && current.isOdd,
+          onBuild: () => numBuilds++,
+          listener: (_, __, ___) => numListenerCalls++,
+        ),
+      );
+
+      expect(numBuilds, 1);
+      expect(numListenerCalls, 0);
+      expect(find.text('null'), findsOneWidget);
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 1);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 2);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 2);
+      expect(numListenerCalls, 3);
+      expect(find.text('1'), findsOneWidget);
+
+      stream1.add(3);
+      await tester.pumpAndSettle();
+      expect(numBuilds, 3);
+      expect(numListenerCalls, 4);
+      expect(find.text('3'), findsOneWidget);
+    });
+
+    // Error handling tests
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ConsumerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+
+      await tester.pumpWidget(
+        ConsumerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ConsumerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ConsumerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamConsumer<int?>(
+        stream: BehaviorSubject<int?>.seeded(null),
+        builder: (context, value, child) => const SizedBox(),
+        listener: (context, previous, current) {},
+        buildWhen: (previous, current) => previous != current,
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'BehaviorSubject<int?>'",
+          'isReplayValueStream: true',
+          'has builder',
+          'has buildWhen',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/rxdart_flutter/test/src/value_stream_listener_test.dart b/packages/rxdart_flutter/test/src/value_stream_listener_test.dart
new file mode 100644
index 000000000..4125f8b52
--- /dev/null
+++ b/packages/rxdart_flutter/test/src/value_stream_listener_test.dart
@@ -0,0 +1,562 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:rxdart_flutter/rxdart_flutter.dart';
+import 'package:rxdart_flutter/src/errors.dart';
+
+typedef Value<T> = void Function(T previous, T current);
+
+class ListenerApp<T> extends StatefulWidget {
+  const ListenerApp({
+    required this.stream1,
+    this.stream2,
+    required this.listener,
+    Key? key,
+  }) : super(key: key);
+
+  final ValueStream<T> stream1;
+  final ValueStream<T>? stream2;
+  final ValueStreamWidgetListener<T> listener;
+
+  static const materialAppKey = Key('material_app');
+  static const toggleStreamButtonKey = Key('toggle_stream_button');
+
+  @override
+  State<ListenerApp<T>> createState() => _ListenerAppState<T>();
+}
+
+class _ListenerAppState<T> extends State<ListenerApp<T>> {
+  late ValueStream<T> stream;
+
+  @override
+  void initState() {
+    super.initState();
+    stream = widget.stream1;
+  }
+
+  void toggleStream() {
+    setState(() {
+      if (widget.stream2 != null) {
+        if (widget.stream2 == stream) {
+          stream = widget.stream1;
+        } else {
+          stream = widget.stream2!;
+        }
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueStreamListener<T>(
+      stream: stream,
+      listener: widget.listener,
+      child: MaterialApp(
+        key: ListenerApp.materialAppKey,
+        home: Column(
+          children: [
+            TextButton(
+              key: ListenerApp.toggleStreamButtonKey,
+              child: const Text('Toggle Stream'),
+              onPressed: toggleStream,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+void main() {
+  group('ValueStreamListener - T Non Nullable', () {
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      stream1.add(2);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 2]);
+    });
+
+    testWidgets('calls listener with correct values when switching streams',
+        (tester) async {
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>.seeded(100);
+      var numCalls = 0;
+      final previousValues = <int>[];
+      final currentValues = <int>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [0]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [0, 1]);
+      expect(currentValues, [1, 100]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(numCalls, 3);
+      expect(previousValues, [0, 1, 100]);
+      expect(currentValues, [1, 100, 200]);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int>>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+      final stream2 = BehaviorSubject<int>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int>.seeded(0);
+
+      await tester.pumpWidget(
+        ListenerApp<int>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamListener<int>(
+        stream: BehaviorSubject<int>.seeded(0),
+        listener: (context, previous, current) {},
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'BehaviorSubject<int>'",
+          'isReplayValueStream: true',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+
+  group('ValueStreamListener - T Nullable', () {
+    testWidgets('does not call listener with initial value', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numCalls = 0;
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+          },
+        ),
+      );
+
+      expect(numCalls, 0);
+    });
+
+    testWidgets('calls listener when stream emits new values', (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      stream1.add(null);
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+    });
+
+    testWidgets('calls listener with correct values when switching streams',
+        (tester) async {
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>.seeded(null);
+      var numCalls = 0;
+      final previousValues = <int?>[];
+      final currentValues = <int?>[];
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, previous, current) {
+            numCalls++;
+            previousValues.add(previous);
+            currentValues.add(current);
+          },
+        ),
+      );
+
+      stream1.add(1);
+      await tester.pumpAndSettle();
+      expect(numCalls, 1);
+      expect(previousValues, [null]);
+      expect(currentValues, [1]);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+      expect(numCalls, 2);
+      expect(previousValues, [null, 1]);
+      expect(currentValues, [1, null]);
+
+      stream2.add(200);
+      await tester.pumpAndSettle();
+      expect(numCalls, 3);
+      expect(previousValues, [null, 1, null]);
+      expect(currentValues, [1, null, 200]);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when initial stream has no value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when initial stream has error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>();
+      stream1.addError(Exception());
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      final error = await completer.future;
+      expect(error, isA<UnhandledStreamError>());
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws ValueStreamHasNoValueError when switching to stream without value',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<ValueStreamHasNoValueError<int?>>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets(
+        'throws UnhandledStreamError when switching to stream with error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+      final stream2 = BehaviorSubject<int?>();
+      stream2.addError(Exception());
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          stream2: stream2,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      await tester.tap(find.byKey(ListenerApp.toggleStreamButtonKey));
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('throws UnhandledStreamError when stream emits error',
+        (tester) async {
+      final completer = Completer<Object>();
+      FlutterError.onError = (errorDetails) {
+        completer.complete(errorDetails.exception);
+      };
+      final stream1 = BehaviorSubject<int?>.seeded(null);
+
+      await tester.pumpWidget(
+        ListenerApp<int?>(
+          stream1: stream1,
+          listener: (_, __, ___) {},
+        ),
+      );
+
+      expect(find.byKey(ListenerApp.materialAppKey), findsOneWidget);
+
+      stream1.addError(Exception());
+      await tester.pumpAndSettle();
+
+      final error = await completer.future;
+
+      expect(error, isA<UnhandledStreamError>());
+      expect(find.byKey(ListenerApp.materialAppKey), findsNothing);
+      expect(find.byType(ErrorWidget), findsOneWidget);
+    });
+
+    testWidgets('provides debug properties for diagnostics', (tester) async {
+      final builder = DiagnosticPropertiesBuilder();
+
+      ValueStreamListener<int?>(
+        stream: BehaviorSubject<int?>.seeded(null),
+        listener: (context, previous, current) {},
+        child: const SizedBox(),
+      ).debugFillProperties(builder);
+
+      final description = builder.properties
+          .where((node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        <String>[
+          "stream: Instance of 'BehaviorSubject<int?>'",
+          'isReplayValueStream: true',
+          'has listener',
+          'has child',
+        ],
+      );
+    });
+  });
+}