Flutter Web and Search
Tags: flutter, dart, webdev, productivity
Published: 2020-10-16 02:21:15

*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:paragraph -->
<p><strong><em>Website: </em></strong><a href="https://web.flatteredwithflutter.com/#/" rel="noreferrer noopener" target="_blank"><strong><em>https://web.flatteredwithflutter.com/#/</em></strong></a></p>
<!-- /wp:paragraph -->

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

<!-- wp:list {"ordered":true} -->
<ol><li>Implement Search in Flutter Web</li><li>Saving the user searches <strong>(Bonus)</strong></li><li>Displaying user search history (<strong>Bonus</strong>)</li></ol>
<!-- /wp:list -->

{% youtube iNjzG3mkF8I %}

####Article here: https://flatteredwithflutter.com/how-to-implement-search-in-flutter-web

<!-- wp:heading {"level":4} -->
<h4>1. Implement Search in Flutter&nbsp;Web</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Let’s start with the algorithm</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>We extracted out the algorithm(<strong>StringSearch</strong>) for searching and put inside a separate file called <strong>string_search.dart</strong></p></blockquote>
<!-- /wp:quote -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">StringSearch(this.input, this.searchTerm)</pre>
<!-- /wp:preformatted -->

<!-- wp:list -->
<ul><li><strong>input</strong>: List&lt;String&gt;, e.g [‘Hey’, ‘there’]</li><li><strong>searchTerm</strong>: String which the user searched. e.g here</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Condition: Length of<strong> searchTerm should be more than 1 letter</strong>. e.g If user searches for h, we return empty.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>To get the relevant results, we have a method called</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">List&lt;String> relevantResults() {
  if (searchTerm.length &lt; 2) {
      return [];
  }
  return input
        .where((item) => item.toLowerCase().contains(searchTerm.toLowerCase()))
        .toList();
}</pre>
<!-- /wp:preformatted -->

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

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>We check if the search term is present inside the list of inputs. (<strong>Linear Search</strong>)</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>The matches found are returned as a list.</p>
<!-- /wp:paragraph -->

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

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

<!-- wp:heading {"level":4} -->
<h4>Integrate with&nbsp;UI</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Our search bar (<strong>a custom widget</strong>) exposes a property called <strong>onChange. </strong></p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Theoretically, when a user starts typing, it should get the relevant results.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>But for making this more human-like, we listen to user input after every 300 milliseconds.</li><li>Introduce <strong>debouncer</strong>, (a custom class)</li></ul>
<!-- /wp:list -->

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

<!-- wp:paragraph -->
<p>This fires a timer (after 300 milliseconds).</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>We include <strong>debouncer</strong> within the <strong>onChange</strong> property</li></ul>
<!-- /wp:list -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">final _debouncer = Debouncer();

// INTERNAL IMPLEMENTATION

onChanged: (val) {
  _debouncer.call(() {
     if (widget.onChanged != null) widget.onChanged(val);
  });
},</pre>
<!-- /wp:preformatted -->

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

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

<!-- wp:heading {"level":4} -->
<h4>Displaying Search&nbsp;Results</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We create a class <strong>SearchCommand </strong>which is</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>class SearchCommand extends GenericNotifier {
   List&lt;String> showSearchResults(String searchTerm) {
      _searchedResults = StringSearch(_articles,searchTerm).relevantResults();
       notify();
       return _searchedResults;
   }
}
class GenericNotifier extends ChangeNotifier {
  void notify() => notifyListeners();
}</code></pre>
<!-- /wp:code -->

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

<!-- wp:paragraph -->
<p>This search command class is basically a change notifier. It has a method called <strong>showSearchResults,&nbsp;</strong></p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>which takes in the user typed search term</li><li>searches using <strong>StringSearch</strong> (described above)</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Note: Input to StringSearch is the list of articles (predefined inside our app)</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>returns the relevant results and calls <strong>notify</strong></li></ul>
<!-- /wp:list -->

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

<!-- wp:paragraph -->
<p>Now, for showing these results, we need an <a href="https://api.flutter.dev/flutter/widgets/Overlay-class.html" rel="noreferrer noopener" target="_blank"><strong>overlay</strong></a>. Luckily, the Flutter framework has an <a href="https://docs.flutter.io/flutter/widgets/Overlay-class.html" rel="noreferrer noopener" target="_blank"><strong>Overlay</strong></a> widget to create widgets that float on top of everything else, without having to restructure your whole view.</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Interesting article on <a href="https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9" target="_blank" rel="noreferrer noopener">overlays</a></p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>We create an overlay entry</li></ul>
<!-- /wp:list -->

<!-- wp:image -->
<figure class="wp-block-image"><img src="https://cdn-images-1.medium.com/max/1600/1*CwmviZkMRp6u4UIg7LvXkQ.png" alt=""/><figcaption>Flutter Web and Search — Overlay&nbsp;Entry</figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>We simply render the results from our <strong>SearchCommand</strong> class, as a <a href="https://api.flutter.dev/flutter/material/ListTile-class.html" rel="noreferrer noopener" target="_blank">ListTile</a></p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Note: If our search bar is in focus, we need to display the above overlay entry and remove, when the bar is unfocused.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>Attach a focus node to our search bar</li></ul>
<!-- /wp:list -->

<!-- wp:code -->
<pre class="wp-block-code"><code>RoundedShape(
   textController: controller,
   focusNode: focusNode,
   onChanged: (term) => searchCommand.showSearchResults(term),
)</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>and for showing/hiding the overlay entry, we simply check</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>OverlayEntry overlayEntry;
// inside initState
if (focusNode.hasFocus) {
   overlayEntry = createOverlayEntry();
   Overlay.of(context).insert(overlayEntry);
} else {
   overlayEntry?.remove();
}</code></pre>
<!-- /wp:code -->

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

<!-- wp:paragraph -->
<p>The overlay entry does not move as we scroll our web page.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Flutter provides us <code><a href="https://docs.flutter.io/flutter/widgets/CompositedTransformFollower-class.html" rel="noreferrer noopener" target="_blank">CompositedTransformFollower</a></code> and<code><a href="https://docs.flutter.io/flutter/widgets/CompositedTransformTarget-class.html" rel="noreferrer noopener" target="_blank">CompositedTransformTarget</a></code> for exactly our use case.</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p><code>follower</code>will <em>follow</em> the <code>target</code> wherever it goes.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>Wrap overlay entry with CompositedTransformFollower</li><li>Wrap the search bar with CompositedTransformTarget</li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>This makes sure if we scroll our webpage, then the overlay results follow the search bar widget.</p>
<!-- /wp:paragraph -->

<!-- wp:image -->
<figure class="wp-block-image"><img src="https://cdn-images-1.medium.com/max/1600/1*0_OBjdD-JRrEoO2Oyl54WQ.png" alt=""/></figure>
<!-- /wp:image -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Interesting article on <a href="https://medium.com/flutter/how-to-float-an-overlay-widget-over-a-possibly-transformed-ui-widget-1d15ca7667b6" target="_blank" rel="noreferrer noopener">CompositeTransform</a></p></blockquote>
<!-- /wp:quote -->

<!-- wp:heading {"level":4} -->
<h4>2. Saving the user&nbsp;searches</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We want to save the user search terms and display them later for step 3.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For saving the search terms, we will use <strong>Hive in Flutter Web.</strong></p>
<!-- /wp:paragraph -->

{% link https://dev.to/aseemwangoo/flutter-web-and-hive-527c %}

<!-- wp:paragraph -->
<p>Once the user clicks on the search result, we insert the entry inside the Hive. We have a class <strong>SearchResultCommand, </strong>which exposes the <strong>tap </strong>method</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>class SearchResultCommand {
   void tap(String result, String searchTerm) {
     var article =
      _combinedInputList.where((item) =>     item.articleName==result).first;
     // INSERT IN CACHE
     _insertInCache(article, searchTerm);
     _navTo(article.articleRoute);
   }
}</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li><strong>result: </strong>is the clicked search result, e.g <strong>Hooks</strong></li><li><strong>searchTerm</strong>: the user searched term, e.g <strong>ook</strong></li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Details regarding the result are searched against the existing articles list and then inserted (<strong>ArticleModel</strong>) into the cache along with the search term (<strong>ook</strong>)</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>For inserting the data into the cache, we create a new model called <strong>CachedSearches</strong>&nbsp;</li></ul>
<!-- /wp:list -->

<!-- wp:image {"align":"center"} -->
<div class="wp-block-image"><figure class="aligncenter"><img src="https://cdn-images-1.medium.com/max/1600/1*9oHGl56AK9OA4D5JmI88NQ.png" alt=""/></figure></div>
<!-- /wp:image -->

<!-- wp:list -->
<ul><li>For inserting this data into the cache, we use <strong>Hive</strong>.</li></ul>
<!-- /wp:list -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Logic: If the item exists in cache, increase the occurence of that item, if not then insert.</p></blockquote>
<!-- /wp:quote -->

<!-- wp:list -->
<ul><li>Let’s <strong>get the opened box </strong>using</li></ul>
<!-- /wp:list -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">final _searchBox = Hive.box&lt;CachedSearches&gt;('searches');</pre>
<!-- /wp:preformatted -->

<!-- wp:list -->
<ul><li>For <strong>checking the item</strong> in the cache,</li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Note: If the item is not present, then hive returns null.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>bool checkIfExistsInCache(CachedSearches data) {
  final val = _searchBox.get(data.phrase);
  if (val == null) {
     return false;
  }
  return true;
}</code></pre>
<!-- /wp:code -->

<!-- wp:list -->
<ul><li>For data <strong>insertion</strong>,</li></ul>
<!-- /wp:list -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">_searchBox.put('key', 'value') ---> GENERIC
_searchBox.put(data.phrase, data)---> OUR CASE SPECIFIC</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Final result,</p>
<!-- /wp:paragraph -->

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

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

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

<!-- wp:heading {"level":4} -->
<h4>3. Displaying user search&nbsp;history</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For displaying search history, we use our custom paginated data table.</p>
<!-- /wp:paragraph -->

{% link https://dev.to/aseemwangoo/flutter-web-and-paginateddatatable-2hl2 %}

<!-- wp:paragraph -->
<p>Hive exposes <strong>listeners</strong> for the Hive Boxes. We utilize this listener on the box to show updates instantly.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>final _searchBox = Hive.box&lt;CachedSearches>('searches');
// FROM ABOVE
Box&lt;CachedSearches> get cacheSearchBox => _searchBox;
cacheSearchBox.listenable() ----> LISTENABLE exposed BY HIVE</code></pre>
<!-- /wp:code -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Note: listenable is present inside <strong>hive_flutter</strong></p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>We can wrap this listenable inside <a rel="noreferrer noopener" href="https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html" target="_blank"><strong>ValueListenableBuilder</strong></a></p>
<!-- /wp:paragraph -->

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

<!-- wp:paragraph -->
<p>Each time any changes are made to the box’s entries, it gets notified to us.</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>For <strong>removing an item </strong>from the cache,</li></ul>
<!-- /wp:list -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">final _searchBox = Hive.box&lt;CachedSearches>('searches');
// FROM ABOVE

Future&lt;void> clearItemInCache(CachedSearches data) async {
   await _searchBox.delete(data.phrase);
}</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>This deletes the particular phrase from the cache.</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>For <strong>removing the entire history </strong>from the cache,</li></ul>
<!-- /wp:list -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted">_searchBox.clear()</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>This removes all the entries from the cache.</p>
<!-- /wp:paragraph -->

*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 iNjzG3mkF8I %}

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Hosted URL&nbsp;: <a href="https://web.flatteredwithflutter.com/#/" rel="noreferrer noopener" target="_blank">https://web.flatteredwithflutter.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 -->