Flutter Web and Streams
Tags: flutter, dart, webdev, productivity
Published: 2020-08-22 06:59:41

*In case it helped :)*
<a href="https://www.buymeacoffee.com/aseemwangoo" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Pass Me A Coffee!!" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>

<!-- wp:heading {"level":3} -->
<h3>Begin…</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>View the demo here</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong><em>Website: </em></strong><a href="https://fir-signin-4477d.firebaseapp.com/#/" rel="noreferrer noopener" target="_blank"><strong><em>https://fir-signin-4477d.firebaseapp.com/#/</em></strong></a></p>
<!-- /wp:paragraph -->

{% youtube c_mjIb3dkFM %}

<!-- wp:paragraph -->
<p>We will cover briefly about</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>Streams inside Forms</li><li>Using Flutter Hooks</li></ol>
<!-- /wp:list -->

####Article here: https://flatteredwithflutter.com/flutter-web-and-streams/

<!-- wp:paragraph -->
<p>Although the article says Flutter Web and Streams, this can be used for any platform (desktop, mobile, etc)</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4>Streams inside&nbsp;Forms</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The parent widget is a <a href="https://pub.dev/packages/flutter_hooks" rel="noreferrer noopener" target="_blank"><strong>HookWidget</strong></a></p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">class _StreamsView extends HookWidget {}</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Inside this widget, we have created a form having 3 fields</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>The first field accepts <strong>anything except a blank</strong></li><li>The second field accepts <strong>any valid integer</strong></li><li>The third field accepts <strong>any valid double</strong></li></ul>
<!-- /wp:list -->

<!-- wp:image -->
<figure class="wp-block-image"><img src="https://cdn-images-1.medium.com/max/1600/1*uFg6ClWHHKgUketmvbOZCQ.png" alt="Flutter Web and Streams"/><figcaption>Flutter Web and Streams</figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p><strong>First Field</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>At the very basic, it is a <a rel="noreferrer noopener" href="https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html" target="_blank"><strong>StreamBuilder</strong></a> widget</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">StreamBuilder&lt;String&gt;(<br>      stream: _formHooks.field1Stream,<br>      builder: (context, snapshot) =&gt; CustomInputField(<br>       onChanged: (val) {<br>          formHooks.field1Controller.add(val); <br>       },<br>       initialValue: data.first,<br>       showError: snapshot.hasError,<br>       errorText: snapshot.error.toString(),<br>    ),<br>)</pre>
<!-- /wp:preformatted -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>final _formHooks = FormHooks();</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>This <strong>FormHooks</strong> is a class which comprises of all the hooks used for this form.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Things needed for our first widget: One <strong>stream controller</strong> and one <strong>stream</strong></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>FormHooks() {
  field1Controller = useStreamController&lt;String>();
}
// INPUT FIELD 1 (STRING)
Stream&lt;String> get field1Stream => field1Controller.stream;
StreamController&lt;String> field1Controller;</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p><strong>field1Stream</strong> is bind to our StreamBuilder’s stream.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>stream: <strong>_formHooks.field1Stream</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Any changes to the input field, while typing is handled by the <strong>field1Controller.</strong></p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">onChanged: (val) {<br>    formHooks.field1Controller.add(val); <br>},</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>There are different ways to add data inside a <a href="https://api.dart.dev/stable/2.9.1/dart-async/StreamController-class.html" rel="noreferrer noopener" target="_blank"><strong>StreamController</strong></a>.</p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>fieldController.add(data)</li><li>fieldController.sink.add(data)</li></ol>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>What’s the difference? Well, they both do the same <a href="https://stackoverflow.com/questions/51395729/what-is-the-difference-between-streamcontroller-add-and-streamcontroller-sink" rel="noreferrer noopener" target="_blank">thing</a>!</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p><strong>We have used 1st approach.</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><em>Note: Other 2 fields (Field2 and Field3), only use different streams, rest everything remains the same, as explained above.</em></p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>Using Flutter&nbsp;Hooks</strong></h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We use the <strong>useStreamController</strong> from the Flutter Hooks for our stream.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Notes:</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list {"ordered":true} -->
<ol><li>Creates an [StreamController] automatically disposed of.</li><li>By default, the stream is a broadcast stream</li></ol>
<!-- /wp:list -->

<!-- wp:heading {"level":4} -->
<h4>Validating User&nbsp;Inputs</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We don’t want to assume the data entered would always be correct, hence we want to validate our form.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The approach taken in this article was using <a href="https://api.dart.dev/stable/2.9.1/dart-async/StreamTransformer-class.html" rel="noreferrer noopener" target="_blank"><strong>StreamTransformer</strong></a><strong>.</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>As per docs, <strong>StreamTransformer</strong> is</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Transforms a Stream.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>When a stream’s <a href="https://api.dart.dev/stable/2.9.1/dart-async/Stream/transform.html" rel="noreferrer noopener" target="_blank">Stream.transform</a> method is invoked with a <a href="https://api.dart.dev/stable/2.9.1/dart-async/StreamTransformer-class.html" rel="noreferrer noopener" target="_blank">StreamTransformer</a>, the stream calls the <a href="https://api.dart.dev/stable/2.9.1/dart-async/StreamTransformer/bind.html" rel="noreferrer noopener" target="_blank">bind</a> method on the provided transformer. The resulting stream is then returned from the <a href="https://api.dart.dev/stable/2.9.1/dart-async/Stream/transform.html" rel="noreferrer noopener" target="_blank">Stream.transform</a> method.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>As we have 3 fields accepting different types of data, we create our transformers accordingly.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>StringValidator</strong></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>StreamTransformer&lt;String, String> validation() =>
   StreamTransformer&lt;String, String>.fromHandlers(
      handleData: (field, sink) {
      if (field.isNotEmpty) {
         sink.add(field);
      } else {
         sink.addError('YOUR ERROR MESSAGE');
      }
   },
);</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>We used the <strong>fromHandlers </strong>which</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><em>Creates a </em><a href="https://api.dart.dev/stable/2.9.1/dart-async/StreamTransformer-class.html" rel="noreferrer noopener" target="_blank"><em>StreamTransformer</em></a><em> that delegates events to the given functions.</em></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>handleData: (field, sink) -&gt; </strong>field is of type <strong>String</strong> and sink is of type <strong>EventSink&lt;String&gt;</strong></p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>Validation</strong></h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The validation logic for the string is&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">if (field.isNotEmpty) {<br>     sink.add(field);<br>} else {<br>     sink.addError('YOUR ERROR MESSAGE');<br>}</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>In short, we are checking our <strong>input for length=0 only</strong>. However, you can customize this logic as per your requirement.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Notice, the <strong>addError</strong> property, if the logic goes inside the else case, <strong>addError</strong> is activated and the error stream is passed back to the StreamBuilder.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Our StreamBuilder displays the error message as:</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">showError: snapshot.hasError,<br>errorText: snapshot.error.toString(),</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>and now our new stream would look like this&nbsp;:</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">// INPUT FIELD 1 (STRING)<br>Stream&lt;String&gt; get field1Stream =&gt; field1Controller.stream.transform&lt;String&gt;(validation());</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p><em>Notice the </em><strong><em>stream.transform </em></strong><em>here. In the final product, we have created a factory for the validators.</em></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>IntegerValidator</strong></p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">StreamTransformer&lt;String, String&gt; validation() =&gt;<br>    StreamTransformer&lt;String, String&gt;.fromHandlers(<br>      handleData: (field, sink) {<br>        final _kInt = int.tryParse(field);<br>        if (_kInt != null &amp;&amp; !_kInt.isNegative) {<br>          sink.add(field);<br>        } else {<br>          sink.addError('YOUR ERROR MESSAGE');<br>        }<br>    },<br>);</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Parameters and the rest remain the same as per the above explanation, except the validation logic.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here, we are validating the input as</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">final _kInt = int.tryParse(field);<br>if (_kInt != null &amp;&amp; !_kInt.isNegative) {<br>   sink.add(field);<br>} else {<br>   sink.addError('YOUR ERROR MESSAGE');<br>}</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>The input is checked if can be parsed (using <strong>tryParse</strong>) or is non-negative.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><em>Note: In case you use </em><strong><em>parse</em></strong><em>, you will encounter exceptions for invalid inputs. However, </em><strong><em>tryParse</em></strong><em> returns null for those cases and the result is caught inside else statement.</em></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><em>Note: This validator doesn’t accept decimals, since an integer doesn’t accept a decimal.</em></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>DoubleValidator</strong></p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">StreamTransformer&lt;String, String&gt; validation() =&gt;<br>    StreamTransformer&lt;String, String&gt;.fromHandlers(<br>      handleData: (field, sink) {<br>      final _kDouble = double.tryParse(field);<br>      if (_kDouble != null &amp;&amp; !_kDouble.isNegative) {<br>         sink.add(field);<br>      } else {<br>         sink.addError('YOUR ERROR MESSAGE');<br>      }<br>    },<br>);</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Parameters and the rest remain the same as per the StringValidator explanation, except the validation logic.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here, we are validating the input as</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">final _kDouble = double.tryParse(field);<br>if (_kDouble != null &amp;&amp; !_kDouble.isNegative) {<br>    sink.add(field);<br>} else {<br>    sink.addError('YOUR ERROR MESSAGE');<br>}</pre>
<!-- /wp:preformatted -->

<!-- wp:separator -->
<hr class="wp-block-separator"/>
<!-- /wp:separator -->

<!-- wp:paragraph -->
<p>The input is checked if can be parsed (using <strong>tryParse</strong>) or is non-negative. Similar to the above.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><em>Note: This validator accepts decimals since a double accepts a decimal.</em></p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4>Save Form</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Our button saves, should only be enabled if all the validators are passed.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">StreamBuilder&lt;bool&gt;(<br>stream: formHooks.isFormValid,<br>   builder: (context, snapshot) {<br>   final _isEnabled = snapshot.data;<br>   return RaisedButton.icon(<br>      onPressed: _isEnabled ?()=&gt;debugPrint(data.toString()):null,                                                                                                               label: const Text(StreamFormConstants.save),<br>      icon: const Icon(Icons.save),<br>   );<br> },<br>),</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p><strong>isFormValid </strong>is a Stream that listens to all the three input field streams.&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">Stream&lt;bool&gt; get isFormValid {<br> _saveForm.listen([field1Stream, field2Stream, field3Stream]);<br> <br> return _saveForm.status;<br>}</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>There is a good and detailed explanation for this part <a rel="noreferrer noopener" href="https://www.didierboelens.com/2020/01/form-validation-using-combinelatest-might-give-false-positives/" target="_blank">here</a>.&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4><strong>Production Tips&nbsp;:</strong></h4>
<!-- /wp:heading -->

<!-- wp:list {"ordered":true} -->
<ol><li>Use <strong>initialData</strong> for streams, they are useful, to begin with, some data.</li><li>Use <strong>tryParse</strong> for validations (int or double)</li><li>Combine the transformers as a Factory, and expose a single class. Link <a href="https://dart.academy/creational-design-patterns-for-dart-and-flutter-factory-method/" rel="noreferrer noopener" target="_blank">here</a>.</li></ol>
<!-- /wp:list -->

*In case it helped :)*
<a href="https://www.buymeacoffee.com/aseemwangoo" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Pass Me A Coffee!!" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>

{% youtube c_mjIb3dkFM %}

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Hosted URL&nbsp;: <a href="https://fir-signin-4477d.firebaseapp.com/#/" rel="noreferrer noopener" target="_blank">https://fir-signin-4477d.firebaseapp.com/#/</a></p></blockquote>
<!-- /wp:quote -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p><a href="https://github.com/AseemWangoo/experiments_with_web" rel="noreferrer noopener" target="_blank">Source code for Flutter Web App..</a></p></blockquote>
<!-- /wp:quote -->