<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Code on the Rocks</title>
    <description>The latest articles on DEV Community by Code on the Rocks (@codeontherocks).</description>
    <link>https://dev.to/codeontherocks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F331392%2F68f35629-dbf6-42fa-be12-215156d05594.png</url>
      <title>DEV Community: Code on the Rocks</title>
      <link>https://dev.to/codeontherocks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codeontherocks"/>
    <language>en</language>
    <item>
      <title>Flutter Inception: A Free Shorebird Alternative</title>
      <dc:creator>Code on the Rocks</dc:creator>
      <pubDate>Thu, 10 Aug 2023 15:44:12 +0000</pubDate>
      <link>https://dev.to/codeontherocks/flutter-inception-a-free-shorebird-alternative-5hif</link>
      <guid>https://dev.to/codeontherocks/flutter-inception-a-free-shorebird-alternative-5hif</guid>
      <description>&lt;p&gt;There is an intimacy between web developers and their users that native app developers are jealous of. Within minutes of finding and diagnosing a software bug, web developers can publish a fix that all of their users will see. There is no laborious back-and-forth with a third-party app store or a 4-day wait for an simple app review. The web developer builds their new application and sends it to the masses.&lt;/p&gt;

&lt;p&gt;Native app developers are not so lucky. App review times on the Google Play and Apple App stores are infamously unpredictable and long, delaying what could have otherwise been immediate fixes for an indeterminate amount of time. This slow but necessary step can ultimately lead to poor reviews, lost users, and developer frustration.&lt;/p&gt;

&lt;p&gt;Flutter developers in particular are negatively impacted by these review process because they typically aim to publish their apps on all platforms. Every time they are ready to release an update, they must ask themselves, "Am I ready for two app reviews?". Many other cross-platform developers face the same soul-crushing processes but in this article, I want to present a potential solution for the Flutter crowd.&lt;/p&gt;

&lt;h1&gt;
  
  
  On the Web and in your Pocket
&lt;/h1&gt;

&lt;p&gt;Flutter is special among cross-platform frameworks because it works on Android, iOS, &lt;em&gt;and&lt;/em&gt; the web with very little modification. In fact, every Flutter web application can be downloaded as a &lt;a href="https://pub.dev/packages/pwa_install"&gt;progressive web app&lt;/a&gt; (PWA) that performs similarly to a native application. In my opinion, PWAs are a valid solution to the app review problem because they &lt;br&gt;
allows developers to publish updates at their own pace without sacrificing (too much of) the native experience. The issue with this approach, of course, is that a majority of people don't know how to download a PWA and among those that do, the willingness to do so is low.&lt;/p&gt;

&lt;p&gt;Forget the PWA experience, then. Even if users don't download your Flutter app as a PWA, they can still use it in their browser which allows them the benefit of real-time updates. While good, this solution is not perfect either because native apps are still more performant and the app stores &lt;br&gt;
provide a level of trust that the wild web does not.&lt;/p&gt;

&lt;p&gt;If not a PWA or web app, then what? Well, what if we could have both? What if we could have the native app experience 95% of the time and the web experience the other 5%? What if we could have our cake and eat it too? That would be nice.&lt;/p&gt;

&lt;p&gt;The final solution I propose is to ship your native apps with a webview fallback. When the latest version of your app has passed app store review, users will get the native experience. When the latest version of your app is still in review, users will get the web experience. From a user perspective, the difference should be subtle to the point of being unnoticeable which is leaps and bounds better than them seeing a bug. Whether or not the webview is used can be &lt;br&gt;
determined by a simple API call to your server, a Firebase Remote Config value, or an automated process that compares the current native version to the latest web version. The rest of this article will discuss the setup in more details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_a7Xx_nx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/geyxmi1w9pnr1xf7lzf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_a7Xx_nx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/geyxmi1w9pnr1xf7lzf3.png" alt="Image description" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Deployment Approach
&lt;/h1&gt;

&lt;p&gt;My personal suggestion is to take a web-first approach to app development. Web applications are faster to update, globally accessible, and many browsers include a mobile device emulator that makes testing the responsiveness of your app as easy as resizing your browser window. By making your application welcoming to web travelers, you will make it welcoming to all users, regardless of the color of their SMS messages.&lt;/p&gt;

&lt;p&gt;A specific suggestion would be to develop and test your application on a chrome browser using the built in dev tools to emulate how it would look on a mobile device. When everything looks good to go, ship it and start testing on native platforms. Taking this approach will ultimately lead to a drastically higher number of web deployments compared to Android or iOS deployments but that's fine. If you're marketing campaigns are web-focused, most of your traffic will be browser-bound anyway. &lt;/p&gt;
&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Web Application
&lt;/h2&gt;

&lt;p&gt;The first step is to create a web application that will be used as the fallback for your native app. This application should be hosted on a server that you control and should be accessible via a URL. The URL can be stored in your app as a global variable or passed to your application using dart-define. I recommend using a &lt;a href="https://firebase.google.com/docs/hosting"&gt;Firebase Hosting&lt;/a&gt; since its super easy to setup. Develop this application as you would any other Flutter application and publish it.&lt;/p&gt;
&lt;h2&gt;
  
  
  In App Webview
&lt;/h2&gt;

&lt;p&gt;To effectively implement this solution, you'll need a trusty webview package that works on both Android and iOS platforms. I prefer to use the &lt;a href="https://pub.dev/packages/flutter_inappwebview"&gt;flutter_inappwebview&lt;/a&gt; package which you can add to your pubspec.yaml using the following line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;flutter_inappwebview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^5.7.2+3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;There is a &lt;a href="https://pub.dev/packages/flutter_inappwebview/versions/6.0.0-beta.24+1"&gt;beta version&lt;/a&gt; of this plugin that also works on the web&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you've added the dependency, the next step is to create an app_webview widget with the sole purpose of displaying your web application in a webview. A small visual indicator, like a different colored status bar when the app is using the&lt;br&gt;
webview, can help you avoid being confused during development. The bare bones code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppWebview&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AppWebview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppWebview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_AppWebviewState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_AppWebviewState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppWebview&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt; &lt;span class="n"&gt;webViewKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;InAppWebViewController&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;webViewController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; 
      &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;toolbarHeight:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;InAppWebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;webViewKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;pullToRefreshController:&lt;/span&gt; &lt;span class="n"&gt;pullToRefreshController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;initialUrlRequest:&lt;/span&gt; &lt;span class="n"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="n"&gt;WebUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'APP_WEBVIEW_BASE_URL'&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
          &lt;span class="nl"&gt;onWebViewCreated:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;webViewController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webview variant of your native app doesn't need to use the router your app normally uses (ex.&lt;br&gt;
&lt;a href="https://pub.dev/packages/auto_route"&gt;auto_route&lt;/a&gt;, &lt;a href="https://pub.dev/packages/go_router"&gt;go_router&lt;/a&gt;, etc) so you can add it directly to a MaterialApp widget. The app inside &lt;br&gt;
the webview will use the router as designed.&lt;/p&gt;

&lt;p&gt;The biggest problem with this code is that your native application will think it only has a single route. In other words, the native Android and iOS apps can't see the router in the webview. One tap of the Android back button or a single right swipe on iOS will close the application completely. We don't want that.&lt;/p&gt;

&lt;p&gt;To fix this, we need to wrap the webview widget in a &lt;a href="https://api.flutter.dev/flutter/widgets/WillPopScope-class.html"&gt;WillPopScope&lt;/a&gt; widget that will detect back &lt;br&gt;
button presses and communicate them to the webview controller. If the webview can navigate backwards in its history, a back button press should do that instead of closing the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;WillPopScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onWillPop:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webViewController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canGoBack&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;goBack&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;toolbarHeight:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="c1"&gt;// ...webview code&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to Webview
&lt;/h2&gt;

&lt;p&gt;Now that we have a webview set up, we need to decide when to use that instead of the normal app. This part is up to you but I'll provide a few ideas here. Since the app stores operate at different speeds, its a good idea to create separate flags for Android and iOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase Remote Config
&lt;/h3&gt;

&lt;p&gt;Firebase Remote Config lets you create and update feature flags easily through your project's &lt;br&gt;
Firebase console. On the Remote Config tab, add two new boolean parameters (android_webview and &lt;br&gt;
ios_webview), set them to false, and publish the changes. When a platform should use the webview,&lt;br&gt;
change its corresponding flag to true and publish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pc2bOgoV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uzk6ex8du5hhnaemdu59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pc2bOgoV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uzk6ex8du5hhnaemdu59.png" alt="Image description" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the app, you can read these values using the &lt;a href="https://pub.dev/packages/firebase_remote_config"&gt;firebase_remote_config&lt;/a&gt; package. The following code sets up the remote config and &lt;br&gt;
reads the values for the android_webview and ios_webview flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseRemoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConfigSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RemoteConfigSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;fetchTimeout:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;minutes:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;minimumFetchInterval:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hours:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDefaults&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s"&gt;'android_webview'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'ios_webview'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchAndActivate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;androidWebview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'android_webview'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;iosWebview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ios_webview'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;✅ Fast to change&lt;/p&gt;

&lt;p&gt;✅ Fast to deploy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;❌ Requires Firebase project and packages&lt;/p&gt;

&lt;p&gt;❌ Not always immediate&lt;/p&gt;

&lt;h3&gt;
  
  
  Dart Endpoint
&lt;/h3&gt;

&lt;p&gt;If you're a full stack Dartists, creating a simple endpoint using &lt;a href="https://dartfrog.vgv.dev/"&gt;Dart Frog&lt;/a&gt; can be a fast way to &lt;br&gt;
add remote control to your app. Create a new endpoint that returns a boolean value for each &lt;br&gt;
platform. When the endpoint is hit, the app can read the value and decide if it should use the&lt;br&gt;
webview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;✅ Fully customizable&lt;/p&gt;

&lt;p&gt;✅ Lightweight&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;❌ Requires a new server deploy for each change &lt;/p&gt;

&lt;p&gt;❌ More setup&lt;/p&gt;

&lt;p&gt;If you'd rather your app automatically detect when it should use the webview, you can tweak this implementation slightly and store the latest version in Remote Config or on your server. Then, when your native apps start up, they can check their own version against this server version and decide if the webview is necessary.&lt;/p&gt;

&lt;p&gt;You can get the current version of your app using the &lt;a href="https://pub.dev/packages/package_info_plus"&gt;package_info_plus&lt;/a&gt; package. Add it to your pubspec.yaml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;package_info_plus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.3.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your main.dart file, you can get the current version of your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;packageInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;PackageInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPlatform&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;packageInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regardless of how you decide to set these flags, the implementation on the frontend will look similar. If the app is running on the web, it should never use the webview. If its running on native Android or iOS, it should check the relevant flag and decide what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt; &lt;span class="n"&gt;baseApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;routerConfig:&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;colorScheme:&lt;/span&gt; &lt;span class="n"&gt;ColorScheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seedColor:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deepPurple&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;useMaterial3:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kIsWeb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;baseApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;androidWebview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;remoteConfigService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;androidWebview&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;iosWebview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isIOS&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;remoteConfigService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;iosWebview&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showWebView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;androidWebview&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;iosWebview&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;kIsWeb&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;showWebView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;AppWebview&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;baseApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  What about the Guidelines?
&lt;/h1&gt;

&lt;p&gt;According to section 2.5.2 of the Apple Developer guidelines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Apps should be self-contained in their bundles, and may not read or write data outside the &lt;br&gt;
designated container area, nor may they download, install, or execute code which introduces or changes features or functionality of the app, including other apps. Educational apps designed to teach, develop, or allow students to test executable code may, in limited circumstances, download code provided that such code is not used for other purposes. Such apps must make the source code provided by the app completely viewable and editable by the user.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we're comparing the webview fallback method to &lt;a href="https://shorebird.dev/"&gt;Shorebird.dev&lt;/a&gt;, I'd argue that the webview approach is more acceptable since the contents of the users app does not change until they want it to (by updating the app through the App Store).&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;If you've ever accidentally shipped a bug to production, you know what true helplessness feels like. Regardless of how simple the fix is, you can be stuck watching users encounter the bug for hours or even days as stakeholders question you about when it will be squashed. Its a stressful time and stress is bad for your health. With the webview fallback solution I've described here, you might just get days of your life back. Most of the time your users will see the native &lt;br&gt;
experience they're used to. For the other handful of times, when a bug has crept into prod or a much needed update is standing in the TSA line, you can flip a switch and sleep easy. How can you beat that?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Butler Labs OCR in Flutter</title>
      <dc:creator>Code on the Rocks</dc:creator>
      <pubDate>Mon, 10 Jul 2023 16:05:51 +0000</pubDate>
      <link>https://dev.to/codeontherocks/butler-labs-ocr-in-flutter-242b</link>
      <guid>https://dev.to/codeontherocks/butler-labs-ocr-in-flutter-242b</guid>
      <description>&lt;p&gt;&lt;a href="https://www.butlerlabs.ai/"&gt;Butler Labs&lt;/a&gt; is an AI-powered optical character recognition (OCR) platform with models fine tuned to extract important data from a variety of commonly encountered documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver's Licenses&lt;/li&gt;
&lt;li&gt;Passports&lt;/li&gt;
&lt;li&gt;Health Insurance Cards&lt;/li&gt;
&lt;li&gt;Paystubs&lt;/li&gt;
&lt;li&gt;Invoices&lt;/li&gt;
&lt;li&gt;Receipts&lt;/li&gt;
&lt;li&gt;W9s&lt;/li&gt;
&lt;li&gt;Mortgage Statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What makes this platform even more attractive from a developer standpoint is that they provide a generous free tier of 500 scans per month and a comprehensive REST API. This means you can basically add an OCR upgrade to your app today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  API Key
&lt;/h3&gt;

&lt;p&gt;To start using the Butler Labs OCR product, you'll need two values: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An API key&lt;/li&gt;
&lt;li&gt;A model-specific queue ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After creating an account, you'll find your API key at the top of the "Settings" page. The API key is a sensitive value that enables access to your account. For that reason, you should avoid adding it to version control and instead pass it to your app using dart-define:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter run --dart-define BUTLER_API_KEY=1234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or my preferred method, dart-define-from-file. First create a config.json file in your app's asset folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "BUTLER_API_KEY": "YOUR_KEY",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run your app using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter run --dart-define-from-file=assets/config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the app, you can retrieve dart-define values using the &lt;a href="https://api.flutter.dev/flutter/dart-core/String/String.fromEnvironment.html"&gt;String.fromEnvironment()&lt;/a&gt; constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BUTLER_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Queue ID
&lt;/h3&gt;

&lt;p&gt;In this post, we'll be using the &lt;a href="https://docs.butlerlabs.ai/reference/us-drivers-license-ocr"&gt;US Driver's License model&lt;/a&gt; to extract a ton of information from images of driver's licenses. Before we can do this though, we'll need a queue ID (aka API ID) for our instance of the model. On the "Explore Models" page, locate the US Driver's License model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g3F9WV6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4h86q3bt5vjojzuza4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g3F9WV6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4h86q3bt5vjojzuza4d.png" alt="Image description" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The process for adding any model is the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on it to add it to your "My Models" page&lt;/li&gt;
&lt;li&gt;On the "My Models" page, select the new model&lt;/li&gt;
&lt;li&gt;Select the "APIs" tab&lt;/li&gt;
&lt;li&gt;Copy the queue ID at the top of the page&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Image Picker
&lt;/h3&gt;

&lt;p&gt;Users will need to either capture or upload images to your app so they can be analyzed and by far the easiest way to accomplish this is with the &lt;a href="https://pub.dev/packages/image_picker"&gt;image_picker&lt;/a&gt; package. It works seamlessly on Android, iOS, and the mobile and desktop variants of web. Add the dependency to your pubspec.yaml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies:
  image_picker: ^1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then create a simple button that users can tap to take an image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ElevatedButton(
  onPressed: () async {
    ImagePicker picker = ImagePicker();
    XFile? pickedImage = await picker.pickImage(source: ImageSource.gallery);
    if (pickedImage == null) return;
    // todo analyze image
  },
  child: const Text('Upload Image'),
),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the next step we'll send the user's image to Butler Labs and parse the result.&lt;/p&gt;

&lt;h1&gt;
  
  
  A Driver's License Example
&lt;/h1&gt;

&lt;p&gt;In this post, we'll running our tests on the sample licenses from the &lt;a href="https://en.wikipedia.org/wiki/Driver%27s_licenses_in_the_United_States"&gt;Driver's licenses in the United States&lt;/a&gt; Wikipedia page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xaeOZkI8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6vahi0g0jkdou2n8i1s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xaeOZkI8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6vahi0g0jkdou2n8i1s.png" alt="Image description" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on each one separately on the wiki and download it. Once you have an image on your computer, you can drag it onto your Android emulator to add it to the list of files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cbQ7v0Vo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg4759eol8xzcaojouj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cbQ7v0Vo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg4759eol8xzcaojouj2.png" alt="Image description" width="668" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now onto the fun part!&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Request
&lt;/h2&gt;

&lt;p&gt;After a user selects an image, we want to send it to the &lt;a href="https://docs.butlerlabs.ai/reference/extract-document"&gt;extract endpoint&lt;/a&gt; where Butler Labs will analyze it and return a JSON object with the parsed results. An example response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "documentId": "1540d070-c500-4994-a0bc-4196499c41de",
  "documentStatus": "Completed",
  "fileName": "alabama.jpeg",
  "mimeType": "image/jpeg",
  "documentType": "US Driver's License",
  "confidenceScore": "High",
  "formFields": [
    {
      "fieldName": "Document Number",
      "value": "1234567",
      "confidenceScore": "High"
    },
    {
      "fieldName": "First Name",
      "value": "CONNOR",
      "confidenceScore": "Low"
    },
    {
      "fieldName": "Last Name",
      "value": "SAMPLE",
      "confidenceScore": "High"
    },
    {
      "fieldName": "Birth Date",
      "value": "01-05-1948",
      "confidenceScore": "High"
    },
    {
      "fieldName": "Expiration Date",
      "value": "01-05-2014",
      "confidenceScore": "High"
    },
    {
      "fieldName": "Address",
      "value": "1 WONDERFUL DRIVE MONTGOMERY AL 36104-1234",
      "confidenceScore": "High"
    },
    {
      "fieldName": "Sex",
      "value": "SEXM",
      "confidenceScore": "High"
    },
    {
      "fieldName": "State",
      "value": "Alabama",
      "confidenceScore": "High"
    }
  ],
  "tables": []
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image files need to be sent to the API in a &lt;a href="https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2"&gt;Multipart request&lt;/a&gt;. Luckily the http package comes with out-of-the-box support for this so the first step is to import that and a few others:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'dart:io'; // For the HttpHeaders
import 'dart:convert'; // To decode the response from Butler Labs
import 'package:http/http.dart'; // For the MultipartRequest class
import 'package:http_parser/http_parser.dart'; // For the MediaType class
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you need to create the MultipartRequest using the extract endpoint. This is also where you'll &lt;a href="https://docs.butlerlabs.ai/reference/authentication"&gt;add your API key to the request header&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String butlerApiKey = const String.fromEnvironment('BUTLER_API_KEY');

    MultipartRequest request = MultipartRequest('POST', Uri.parse('https://app.butlerlabs.ai/api/queues/$queueId/documents'));

    request.headers.addAll({
      HttpHeaders.acceptHeader: '*/*',
      HttpHeaders.authorizationHeader: 'Bearer $butlerApiKey',
      HttpHeaders.contentTypeHeader: 'multipart/form-data',
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we need to add the image to the request. This can be done using the image's file path or its raw bytes and the option you choose will depend on the platform you're using. Files returned by ImagePicker on Flutter web will not have a path so on the web you will &lt;em&gt;need&lt;/em&gt; to use the bytes method. I've included the full code snippets for both methods below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Image Path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ElevatedButton(
  onPressed: () async {
    ImagePicker picker = ImagePicker();
    XFile? pickedImage = await picker.pickImage(source: ImageSource.gallery);
    if (pickedImage == null) return;

    String butlerApiKey = const String.fromEnvironment('BUTLER_API_KEY');
    String queueId = 'fdf1f80a-03f5-40e5-83f0-a33694318532'; // Replace with your own queueId

    MultipartRequest request = MultipartRequest('POST', Uri.parse('https://app.butlerlabs.ai/api/queues/$queueId/documents'));

    request.headers.addAll({
      HttpHeaders.acceptHeader: '*/*',
      HttpHeaders.authorizationHeader: 'Bearer $butlerApiKey',
      HttpHeaders.contentTypeHeader: 'multipart/form-data',
    });

    if (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS) {
      request.files.add(
        await MultipartFile.fromPath(
          'file',
           pickedImage.path,
          contentType: MediaType('image', 'jpeg'),
        ),
      );
    } else {
      throw Exception('Platform not supported');
    }

    StreamedResponse response = await request.send();

    ButlerResult? result;
    String value = await response.stream.transform(utf8.decoder).join();
    result = ButlerResult.fromJson(jsonDecode(value));
    debugPrint('Response stream: $value');
  },
  child: const Text('Upload Image'),
),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using the Image Bytes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ElevatedButton(
  onPressed: () async {
    ImagePicker picker = ImagePicker();
    XFile? pickedImage = await picker.pickImage(source: ImageSource.gallery);
    if (pickedImage == null) return;

    String butlerApiKey = const String.fromEnvironment('BUTLER_API_KEY');
    String queueId = 'fdf1f80a-03f5-40e5-83f0-a33694318532'; // Replace with your own queueId

    MultipartRequest request = MultipartRequest('POST', Uri.parse('https://app.butlerlabs.ai/api/queues/$queueId/documents'));

    request.headers.addAll({
      HttpHeaders.acceptHeader: '*/*',
      HttpHeaders.authorizationHeader: 'Bearer $butlerApiKey',
      HttpHeaders.contentTypeHeader: 'multipart/form-data',
    });

    Uint8List imageBytes = await pickedImage.readAsBytes();

    MultipartFile file = MultipartFile(
      'file',
      ByteStream.fromBytes(imageBytes),
      imageBytes.lengthInBytes,
      filename: 'temp.jpg',
      contentType: MediaType('image', 'jpeg'),
    );

    request.files.add(file);

    StreamedResponse response = await request.send();

    ButlerResult? result;
    String value = await response.stream.transform(utf8.decoder).join();
    result = ButlerResult.fromJson(jsonDecode(value));
    log('Response stream: $value');
  },
  child: const Text('Upload Image'),
),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The bytes method will work on all platforms so I suggest using that&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deciphering the Results
&lt;/h2&gt;

&lt;p&gt;The request may take a few seconds but soon you should have a nice, juicy JSON blob to sink your teeth into. The shape of the result is fairly consistent from model to model too, meaning you won't have to build custom deserializers if you add more models later.&lt;/p&gt;

&lt;p&gt;Each result will contain the following top-level fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document ID: Unique ID for the document you uploaded&lt;/li&gt;
&lt;li&gt;Upload ID: An ID you can use to delete the image that was uploaded&lt;/li&gt;
&lt;li&gt;Document Status: A value indicating the &lt;a href="https://docs.butlerlabs.ai/reference/get-extracted-results-queues#document-status-details"&gt;status of the document&lt;/a&gt; according to Butler Labs&lt;/li&gt;
&lt;li&gt;File Name: The name of the file that was uploaded&lt;/li&gt;
&lt;li&gt;Document Type: The type of the document, typically matching the model you used (ex. Health Insurance Card)&lt;/li&gt;
&lt;li&gt;Confidence Score: A Low/Medium/High value indicating the model's overall confidence in the parsed results. You can see the breakdown of all your organization's confidence scores on the Butler Labs dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--32uAGnA3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4d37m41uulm4g5v6nod9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--32uAGnA3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4d37m41uulm4g5v6nod9.png" alt="Image description" width="425" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beneath these top-level fields you will also find a list of "Form Fields" corresponding to the fields identified by your model (see the model's page in the Butler Docs). Each form field will contain the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Field Name&lt;/li&gt;
&lt;li&gt;Value: The value detected by the model for this field &lt;/li&gt;
&lt;li&gt;Confidence Score: A Low/Medium/High value indicating the model's confidence in this specific field&lt;/li&gt;
&lt;li&gt;Confidence Value: A value between 0 and 1 indicating the likelihood the result is correct&lt;/li&gt;
&lt;li&gt;OCR Confidence Value: A value between 0 and 1 indicating the likelihood the model found the correct field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the sample app linked below, I used the confidence scores and values to create a simple UI illustrating the model's performance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LynMOnoB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fhn6suzn3vl16n1dr0ng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LynMOnoB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fhn6suzn3vl16n1dr0ng.png" alt="Image description" width="554" height="1170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Butler Labs Package
&lt;/h2&gt;

&lt;p&gt;While the code required for this OCR product is fairly unsophisticated, you can avoid the hassle of piecing everything together yourself and use the &lt;a href="https://pub.dev/packages/butler_labs"&gt;butler_labs&lt;/a&gt; package on pub.dev. This package currently only supports the Extract document endpoint. It includes a generic ButlerResult class and models specific to each model on the Butler Labs Website so you can convert the generic result from the API into something that's more usable.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;OCR is a solved problem and Butler Labs has a product that is more than satisfactory for many common use cases. If the built-in models don't meet your needs, their platform also allows you to create your own models. You can read more about that &lt;a href="https://docs.butlerlabs.ai/reference/building-custom-form-extraction-models"&gt;here&lt;/a&gt;. Happy coding!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ocr</category>
      <category>dart</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
