<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Yet Another Programmer's Blog]]></title><description><![CDATA[I'm building apps since the '90 and totally in 💙 with Flutter.]]></description><link>https://yapb.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 22:42:05 GMT</lastBuildDate><atom:link href="https://yapb.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Anatomy of a Flutter upgrade.]]></title><description><![CDATA[Introduction
A lot of things happen in the lifecycle of an app. Bugs get fixed and features are added. Besides the changes in functionality of the app itself, it is also important to maintain and upgrade the dependencies of the app. Once in a while y...]]></description><link>https://yapb.dev/anatomy-of-a-flutter-upgrade</link><guid isPermaLink="true">https://yapb.dev/anatomy-of-a-flutter-upgrade</guid><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Fri, 04 Jul 2025 12:17:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/cxoR55-bels/upload/9e5ef303698dd1fa91503d98effaeb77.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>A lot of things happen in the lifecycle of an app. Bugs get fixed and features are added. Besides the changes in functionality of the app itself, it is also important to maintain and upgrade the dependencies of the app. Once in a while you have to upgrade the Flutter version that you are using. Upgrading the Flutter version is what this document is all about.</p>
<h2 id="heading-upgrading-flutter">Upgrading Flutter</h2>
<p>I think that everybody who uses Flutter has seen this message once in a while:</p>
<pre><code class="lang-plaintext">┌─────────────────────────────────────────────────────────────────────────────┐
│ A new version of Flutter is available!                                      │
│                                                                             │
│ To upgrade to the latest version, run "flutter upgrade".                    │
└─────────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p>This command only updates the Flutter tooling, it does nothing for your source code. Although it looks that this is harmless, there is a caveat. Your project now contains files and folders that were generated with an older version of Flutter while it is compiled with a newer Flutter version.</p>
<p>Some examples:</p>
<ul>
<li><p>.gitignore might have changed and you might track or ignore wrong files and folders.</p>
</li>
<li><p>The platform folders might have changes. Think about a new gradle version being used by the latest version of Flutter (build.gradle =&gt; build.gradle.kts).</p>
</li>
<li><p>There might be new or changed entries in the pubspec.yaml.</p>
</li>
</ul>
<h2 id="heading-upgrading-flutter-and-your-project-step-by-step">Upgrading Flutter and your project step-by-step</h2>
<p>The solution I describe in this document is to create all files and folders with the latest version of Flutter and to merge back the necessary changes. On top of that git is used to document the upgrade step-by step. For installing an pinning specific Flutter versions to a project, I use fvm.</p>
<p>You can use this git repository alongside with this document.</p>
<p><a target="_blank" href="https://github.com/jsroest/upgrade_demo/commits/main/">https://github.com/jsroest/upgrade_demo/commits/main/</a></p>
<h2 id="heading-step-01-step-06-create-sample-app">Step 01 - Step 06: Create sample app</h2>
<p>With “Step 01” through “Step 06” I created a demo app with Flutter 3.29.3 for demonstrating the upgrade process. As these steps are not relevant for the upgrade process itself, I do not describe the details here.</p>
<h2 id="heading-step-07-start-flutter-upgrade-from-3293-to-3324">Step 07: Start flutter upgrade from 3.29.3 to 3.32.4.</h2>
<p>This is an empty commit that documents the start of the update process.</p>
<h2 id="heading-step-08-delete-all-except-git-lib-and-test">Step 08: Delete all except .git, lib and test.</h2>
<p>Delete everything except the source in the lib and test folder. Be sure to also delete all hidden files, except the “.git” folder. Add a commit to document the deletion.</p>
<h2 id="heading-step-09-create-a-new-project-with-flutter-3324">Step 09: Create a new project with Flutter 3.32.4.</h2>
<p>To minimize the manual work, it is necessary to use the same values with the “Flutter create” command as that was used in the original project.</p>
<ul>
<li><p>Match the description of the project.</p>
</li>
<li><p>Match the organization.</p>
</li>
<li><p>Match the project name.</p>
</li>
<li><p>Match all platforms used and their languages (Java/Kotlin Objective-C/Swift).</p>
</li>
<li><p>etc</p>
</li>
</ul>
<p>Make 3.32.4 global and check the version fvm is using afterwards.</p>
<ul>
<li><p><code>fvm global 3.32.4</code></p>
</li>
<li><p><code>fvm flutter --version</code></p>
</li>
</ul>
<p>Recreate all files except any files in the lib and test folder. Flutter will skip those folders if they are already present.</p>
<ul>
<li><p><code>fvm flutter create --description "Demo upgrade from flutter 3.29.3 to flutter 3.32.4" --org com.rubigo --project-name upgrade_demo --platforms android .</code></p>
</li>
<li><p><code>git add .</code></p>
</li>
</ul>
<p>Add a commit to document the newly created files.</p>
<h2 id="heading-step-10-pin-to-flutter-3324-with-fvm">Step 10: Pin to Flutter 3.32.4 with fvm.</h2>
<p>Pin the version of Flutter to use with fvm.</p>
<ul>
<li><p><code>fvm use 3.32.4</code></p>
</li>
<li><p><code>git add .</code></p>
</li>
</ul>
<p>Add a commit to document the new Flutter version to use for this project.</p>
<h2 id="heading-step-11-start-manual-merge">Step 11: Start manual merge.</h2>
<p>This is the most complicated part of the upgrade. Here you have to judge the changes you have made to the original files and you have to merge them with the newly created files and or structure.</p>
<p>I like to mark this with an empty commit, to just have a point you can rollback to if the merge fails.</p>
<p>After the empty commit you have to do the following:</p>
<ul>
<li><p>Delete all except .git (don’t forget the hidden files and folders).</p>
</li>
<li><p>Get all files and folders from Step 07. For this you have to know the git commit hash that belongs to Step 07.<br />  <code>git checkout e019a0d144027d849bfb87f664ee5f0946bccde8 -- .</code><br />  This command will copy all files and folders that are present at the moment of Step 07.</p>
</li>
<li><p>After the checkout you have to judge and merge every single file manually.</p>
<p>  I find Android Studio a good tool for that, because it’s easy to</p>
<ul>
<li><p>Rollback a file if you want to keep the newly generated one.</p>
</li>
<li><p>Revert changes per line or block.</p>
</li>
<li><p>Commit the changes per file.</p>
</li>
</ul>
</li>
</ul>
<p>    If you make a mistake with merging a specific file, you will have to restore the original from Step 07 with:<br />    <code>git checkout e019a0d144027d849bfb87f664ee5f0946bccde8 -- path/to/file</code></p>
<ul>
<li><p>I like to commit the changes per file.<br />  Be aware that it can be very confusing.</p>
<p>  Old version: Is the newly generated file.</p>
<p>  New version: Is the original file from Step 07.</p>
</li>
</ul>
<h2 id="heading-step-12-merge-completed-flutter-upgrade-to-3324-done">Step 12: Merge completed. Flutter upgrade to 3.32.4 done.</h2>
<p>I prefer an empty commit to mark the merge and upgrade as completed.</p>
<p>When you are done, you can check the upgrade by selecting the commit of Step 12 and Step 07. For that you can use e.g. Sourcetree or Fork</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751629908107/877eff10-e673-4be5-ab6e-baf922141204.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-final-words">Final words</h2>
<p>There is more work to be done after this. The code you have now may not compile at this point.</p>
<p>You might have to do one or more from these steps:</p>
<ul>
<li><p>Update/check all dependencies</p>
</li>
<li><p>Migrate abandoned/unmaintained packages</p>
</li>
<li><p>Apply new code guidelines and or lints</p>
</li>
<li><p>Reformat all your code</p>
</li>
</ul>
<p>Let me know what you think, feedback is appreciated.</p>
]]></content:encoded></item><item><title><![CDATA[Software maintenance]]></title><description><![CDATA[Introduction
We all understand that an internal combustion engine needs maintenance. The oil needs to be changed regularly, likewise the spark plugs and the timing belt. A failure to do so will most likely result in a higher consumption, lower engine...]]></description><link>https://yapb.dev/software-maintenance</link><guid isPermaLink="true">https://yapb.dev/software-maintenance</guid><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sat, 07 Jun 2025 15:07:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/V37iTrYZz2E/upload/95e05fe460c506c83b9b822640eb6cd5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>We all understand that an internal combustion engine needs maintenance. The oil needs to be changed regularly, likewise the spark plugs and the timing belt. A failure to do so will most likely result in a higher consumption, lower engine performance, and in the end complete engine failure. Maintenance is necessary, because the engine has internal moving parts that wear out during its lifetime. It just comes down to physics.</p>
<p>If we take that analogy to software, one could argue that software does not need maintenance. It does not have any moving internal parts. So why is software maintenance even a thing?</p>
<h2 id="heading-the-moving-parts-of-software">The moving parts of software</h2>
<p>With software, the “moving parts” are not on the inside of the program but on the outside.</p>
<h3 id="heading-device-os">Device OS</h3>
<p>The device OS where the software is running on gets feature and security updates. It is not certain that the software will run flawlessly on a newer OS. It can be the features in the OS that change, but it can also be that the software is actively refused by the OS. Android nowadays prevents really old software to be run. Software compiled with older SDK’s are actively refused. That minimum SDK boundary rises automatically, with every new OS release.</p>
<h3 id="heading-programming-language">Programming language</h3>
<p>The app’s programming language evolves. The language gets new features and functions, but also some functions get deprecated and are eventually removed.</p>
<h3 id="heading-libraries-used">Libraries used</h3>
<p>The libraries that the app uses are in motion. Some libraries are updated to follow the programming languages best practises and deprecation guidelines. Some libraries get new functionalities, and others are abandoned or superseded by competing libraries.</p>
<h3 id="heading-programming-tools">Programming tools</h3>
<p>The programming tools evolve. The programming tools, like Android Studio, will support a lower boundary and an upper boundary of the programming language. To load an old project, an old version of Android Studio needs to be used.</p>
<h3 id="heading-developers-os">Developers OS</h3>
<p>The operating system of the developers laptop, changes. There comes a point in time when the developer is not able to start an old version of the development tools, like Android Studio.</p>
<h3 id="heading-cpu-architecture-developers-os">CPU architecture developers OS</h3>
<p>Eventually the developer gets a new laptop with a new processor architecture. Like in the past years where Apple made the transition from x86/x64 to arm64. I must admit that the backwards compatibility provided is pretty good, but it is for a fact that old x86/x64 virtual machine images are not usable on arm64. That means, that old windows software has to be able to run on the arm version of Windows, otherwise you are out of luck.</p>
<h2 id="heading-security">Security</h2>
<p>If software maintenance is neglected, so is the security of the solution. The amount of vulnerabilities in software increases with time because of newly discovered vulnerabilities. Using unmaintained software is like driving a car without ever checking the tires.</p>
<h2 id="heading-technical-debt">Technical debt</h2>
<p>The above shows that with software maintenance, everything is connected. The software itself does not change, of course, but it’s surroundings do. When software is not maintained it’s technical debt increases with time. If software maintenance is neglected long enough it might even become irreparable. Then the software is total loss, just like a car engine that snapped it’s timing belt.</p>
<p>Be sure that your software is maintained and kept up to date.</p>
<h2 id="heading-technology-release-timeline-2013-2024">Technology Release Timeline (2013 - 2024+)</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Technology</strong></td><td><strong>2013</strong></td><td><strong>2014</strong></td><td><strong>2015</strong></td><td><strong>2016</strong></td><td><strong>2017</strong></td><td><strong>2018</strong></td><td><strong>2019</strong></td><td><strong>2020</strong></td><td><strong>2021</strong></td><td><strong>2022</strong></td><td><strong>2023</strong></td><td><strong>2024+</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>ARM64 Transition</strong></td><td>Apple A7 Chip (1st 64-bit consumer ARM CPU)</td><td></td><td>Windows 10 on ARM support</td><td></td><td>iOS drops 32-bit support</td><td>AWS Graviton (ARM in cloud)</td><td>Play Store 64-bit mandate, Surface Pro X (ARM)</td><td>Apple announces Mac transition, Apple M1 Chip released</td><td>Play Store 64-bit ONLY mandate, Apple M1 Pro/Max</td><td>Apple M2 Chip</td><td>Apple M2/M3 families, Snapdragon X Elite announced</td><td>First Snapdragon X Elite PCs (Planned)</td></tr>
<tr>
<td><strong>macOS</strong></td><td>Mavericks (10.9)</td><td>Yosemite (10.10)</td><td>El Capitan (10.11)</td><td>Sierra (10.12)</td><td>High Sierra (10.13)</td><td>Mojave (10.14)</td><td>Catalina (10.15) (Drops 32-bit app support)</td><td>Big Sur (11.0) (Native Apple Silicon support)</td><td>Monterey (12.0)</td><td>Ventura (13.0)</td><td>Sonoma (14.0)</td><td>Ongoing Releases</td></tr>
<tr>
<td><strong>Windows</strong></td><td>Windows 8.1</td><td></td><td>Windows 10</td><td></td><td></td><td></td><td></td><td></td><td>Windows 11 (Improved ARM support)</td><td></td><td></td><td></td></tr>
<tr>
<td><strong>iOS</strong></td><td>iOS 7 (1st 64-bit support)</td><td>iOS 8</td><td>iOS 9</td><td>iOS 10</td><td>iOS 11</td><td>iOS 12</td><td>iOS 13</td><td>iOS 14</td><td>iOS 15</td><td>iOS 16</td><td>iOS 17</td><td>Ongoing Releases</td></tr>
<tr>
<td><strong>Android</strong></td><td></td><td></td><td>Android 5.0 Lollipop (1st ARM64 support)</td><td>Android 6 Marshmallow</td><td>Android 7 Nougat</td><td>Android 8 Oreo</td><td>Android 9 Pie</td><td>Android 10</td><td>Android 11</td><td>Android 12</td><td>Android 13</td><td>Android 14</td></tr>
<tr>
<td><strong>Android Studio</strong></td><td></td><td>AS 1.0</td><td>AS 1.1 - 1.5</td><td>AS 2.0 - 2.2</td><td>AS 2.3 - 3.0</td><td>AS 3.1 - 3.3</td><td>AS 3.4 - 3.6</td><td>AS 4.0 - 4.1</td><td>Arctic Fox, Bumblebee</td><td>Dolphin, Electric Eel</td><td>Flamingo, Giraffe, Hedgehog</td><td>Ongoing Releases</td></tr>
<tr>
<td><strong>Dart</strong></td><td>Dart 1.0</td><td></td><td></td><td></td><td></td><td>Dart 2.0</td><td></td><td>Dart 2.10 (Null Safety Beta)</td><td>Dart 2.12 (Stable Null Safety)</td><td>Dart 2.16-2.18</td><td>Dart 3.0</td><td>Ongoing Releases</td></tr>
<tr>
<td><strong>Flutter</strong></td><td></td><td></td><td>"Sky" Demo</td><td></td><td>Alpha</td><td>Flutter 1.0</td><td></td><td>Flutter 1.2x</td><td>Flutter 2.0 (Stable Web)</td><td>Flutter 3.0 (Stable Desktop)</td><td>Flutter 3.7-3.16 (Impeller)</td><td>Ongoing Releases</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Consumer apps versus workforce apps]]></title><description><![CDATA[History
In the history of building workforce apps, there was in interesting twist.
In the beginning, computers were a thing for large companies, research facilities and universities. Consumers rarely came in touch with them. They were difficult to op...]]></description><link>https://yapb.dev/consumer-apps-versus-workforce-apps</link><guid isPermaLink="true">https://yapb.dev/consumer-apps-versus-workforce-apps</guid><category><![CDATA[Flutter]]></category><category><![CDATA[line-of-business]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Fri, 07 Feb 2025 19:27:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/c_4eaGRDSVU/upload/f41c0e476c52458dac95687c9b691ef8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-history">History</h2>
<p>In the history of building workforce apps, there was in interesting twist.</p>
<p>In the beginning, computers were a thing for large companies, research facilities and universities. Consumers rarely came in touch with them. They were difficult to operate and very, very expensive.</p>
<p>At a later point in time, consumers got interested in computers, because they got less expensive and smaller. The Personal Computer was born. Remember the IBM-XT? I remember it well because I had one; I’m that…. experienced 😁.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738942065554/7551c03c-b969-4f5a-9795-97acb01522a4.jpeg" alt class="image--center mx-auto" /></p>
<p>Computers got smaller, and smaller until they could run on batteries.</p>
<p>The Telxon PTC960-SL was the first handheld I used to program for. It had similar specs as the original IBM-XT.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738941637980/059cea37-4c41-41a0-94a7-bebf862faba4.jpeg" alt class="image--center mx-auto" /></p>
<p>It was a Microsoft DOS compatible machine, with a 21×16 line text LCD and a kind of wifi. We used this to support warehouse personnel with their daily job and to give them realtime information at their current location.</p>
<p>At a later point in time, when DOS was replaced by Windows on the desktops, the OS on these devices was replaced with WindowsCE and Windows Mobile.</p>
<p>At this exact point, the twist happened. The computer companies were shifting their focus from enterprises to consumers. There was much more to earn there, the consumer market was much bigger. Not a few big chunks of money to earn, but many, many more small chunks.</p>
<p>So in the early days, the focus was on enterprises and consumers were a side market. Now it is the other way around. The focus is now on consumers and the enterprise is a side market.</p>
<p>There is not much to complain, in the end a lot of things improved, but there are some pain points that I will explain here below.</p>
<h2 id="heading-hardware-for-the-workforce">Hardware for the workforce</h2>
<p>As devices for consumers are build in large quantities, they are in general cheaper and with better hardware specifications than the Enterprise counterparts. I remember, roughly ten or fifteen years ago, that enterprises tried to use consumer devices as a cheap alternative. The thought was simple: Buy a new one at the local shop if it fails or dropped and shattered. But in the end that didn’t work out well.</p>
<ol>
<li><p>The lifecycle of a consumer device is short and unpredictable.<br /> “No sir, that particular model is replaced by this new fancy bigger one.”</p>
</li>
<li><p>There is no way to get an SLA (service level agreement) on these devices.<br /> “How can you expect an SLA on a 300 dollar device?”</p>
</li>
<li><p>Erratic OS updates.<br /> “We promise some updates for the first year, and it depends.”</p>
</li>
<li><p>OS updates can not be scheduled.<br /> “Production is halted in hall 1 and 2 because Android &lt;X&gt; came out last night.”</p>
</li>
<li><p>Contents of OS updates is not clear.<br /> “All 300 devices are offline, because of a patch in the WiFi driver.”</p>
</li>
<li><p>No internal hardware barcode scanner.<br /> “A camera can scan a barcode too. I do this all the time with electronic banking at home".</p>
</li>
<li><p>Bluetooth connection issues with hardware barcode scanners.<br /> “Press and hold till the light flashes blue, three times.”</p>
</li>
<li><p>Simply not ruggedised enough.<br /> “Oops, sorry boss.”</p>
</li>
<li><p>How to keep your workforce of Candy Crush and Pokemon Go?<br /> “How many points did you get in the first shift?”</p>
</li>
</ol>
<p>If the number of devices was larger than a few, this became soon a nightmare to manage. Especially as time goes by and entropy kicks in.</p>
<ol>
<li><p>More brands</p>
</li>
<li><p>More models</p>
</li>
<li><p>More configurations</p>
</li>
<li><p>More firmware versions</p>
</li>
</ol>
<p>And how about security? How can you secure your business, if you have no idea which security patches are installed on your devices? Ransomware, anyone?</p>
<h2 id="heading-operating-systems-for-the-workforce">Operating Systems for the Workforce</h2>
<p>To be honest, there is only one operating system left for our Enterprise Workforce devices, and that is Android.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738945047363/c5d321a4-6e01-4b19-9827-fa70193562f3.png" alt class="image--center mx-auto" /></p>
<p>So here we are. We now have Enterprise devices with a consumer OS and tools that are mostly used to build apps for consumers.</p>
<p>This is not necessarily a bad thing. The quality of the tools, like Flutter, is extremely high, with a great developer experience. Maybe that quality would not have been reached if the market was not so big.</p>
<p>But there is a constant tension:</p>
<p>Android introduces ways to improve battery life, protect the end-user privacy and add ways to secure their monetisation with the play store.</p>
<p>Enterprise devices manufacturers seek ways keep the operating system open, productive and usable for their customers workforce.</p>
<p>This is something that you have to keep in mind when developing apps for workforces.</p>
<h2 id="heading-consumer-apps-versus-workforce-apps">Consumer apps versus Workforce apps</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><strong>Consumer apps</strong></td><td><strong>Custom workforce apps</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Owner of the device</strong></td><td>Consumer</td><td>Business</td></tr>
<tr>
<td><strong>Consumer privacy laws apply</strong></td><td>Yes</td><td>No</td></tr>
<tr>
<td><strong>End user price of app</strong></td><td>$0 - $10</td><td>Build to order</td></tr>
<tr>
<td><strong>Installations</strong></td><td>Large (&gt;10k) to very large (&gt; 1 mil)</td><td>Small (&lt;10k) to very small (&lt;10)</td></tr>
<tr>
<td><strong>Different daily apps</strong></td><td>Maybe more than 10</td><td>Mostly 1, but also 2 to 3</td></tr>
<tr>
<td><strong>Service Level Agreement</strong></td><td>None</td><td>Agreed between customer and software agency</td></tr>
<tr>
<td><strong>Incidental outage</strong></td><td>Maybe some long term impact</td><td>Direct business impact</td></tr>
<tr>
<td><strong>App updates</strong></td><td>Mandatory</td><td>Part of the SLA</td></tr>
<tr>
<td><strong>App logging</strong></td><td>Anonymous crash reporting</td><td>Detailed logging with precise data and circumstances.</td></tr>
<tr>
<td><strong>Main goal</strong></td><td>Monetising by entertaining the user</td><td>Support the workflow</td></tr>
</tbody>
</table>
</div><p>In my view, the last two factors are the key distinctions between consumer apps and workforce apps.</p>
<h3 id="heading-app-logging">App logging</h3>
<p>We use logging extensively. If a customer has a question, we can download the logging an trace step-by-step what happened. As the software is on the edge of the process, it is also the place where the user encounters issues.</p>
<p>With logging we can proof:</p>
<ol>
<li><p>Bad response times of the backend.</p>
</li>
<li><p>Issues with the network or endpoints.</p>
</li>
<li><p>User errors.</p>
</li>
<li><p>Basically always show that the issue is not ours 🫢</p>
</li>
</ol>
<h3 id="heading-main-goal">Main goal</h3>
<p>The main goal for most consumer apps is to please and monetise the consumer. Therefore they have to offer a super smooth experience where the user is always in control. Chances are great, if the user has to wait too long or too often, your app gets deleted and gets a bad review.</p>
<p>The main goal for most workforce apps is to support a workflow and to prevent mistakes. Therefore, if we have to wait for confirmation from the backend, there is not much else we can do then to wait. If we have to protect the workflow by locking the UI, then thats it, we lock it.</p>
<p>With consumer apps, the consumer is king. With workforce apps, the workflow is king. That’s basically it.</p>
]]></content:encoded></item><item><title><![CDATA[Navigator onPopPage versus onDidRemovePage, Part 2]]></title><description><![CDATA[Recap part 1.
In the previous article about this subject, I explained the limitations that I have ran into, with the default onDidRemovePage implementation. In this article I will show how I’ve overcome or better said, circumvented these limitations....]]></description><link>https://yapb.dev/navigator-onpoppage-versus-ondidremovepage-part-2</link><guid isPermaLink="true">https://yapb.dev/navigator-onpoppage-versus-ondidremovepage-part-2</guid><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 05 Feb 2025 13:52:11 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-recap-part-1">Recap part 1.</h2>
<p>In the <a target="_blank" href="https://yapb.dev/navigator-onpoppage-versus-ondidremovepage">previous</a> article about this subject, I explained the limitations that I have ran into, with the default <code>onDidRemovePage</code> implementation. In this article I will show how I’ve overcome or better said, circumvented these limitations.</p>
<p>First of all, these are limitations that I’ve found:</p>
<ol>
<li><p><code>onDidRemovePage</code> is called <strong>after</strong> the pop transition has started.</p>
</li>
<li><p>The <code>BackButton</code> in the <code>AppBar</code> calls <code>onDidRemovePage</code>.</p>
</li>
<li><p>The Android back button calls <code>onDidRemovePage</code>.</p>
</li>
<li><p>The back gesture on iOS calls <code>onDidRemovePage</code>.</p>
</li>
<li><p>Predictive back gesture on Android (hopefully) calls <code>onDidRemovePage</code><br /> I’ve haven’t tested this yet. At this time of writing this is still experimental, but I expect that it works the same.</p>
</li>
</ol>
<p>As <code>onDidRemovePage</code> is called after the fact, it is not possible at that point to prevent the pop without introducing ugly animations or other pitfalls (like the inability to use a <code>showDialog</code> at that time, while the transition happens).</p>
<p>The solution: prevent <code>onDidRemovePage</code> from being called.</p>
<h2 id="heading-backbutton">BackButton</h2>
<p>In this section, I show how a standard <code>BackButton</code> can be adjusted to fit our needs.</p>
<p>A standard <code>BackButton</code> executes this code when pressed:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BackButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">_ActionButton</span> </span>{
  ...
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> _onPressedCallback(BuildContext context) =&gt; Navigator.maybePop(context);
}
</code></pre>
<p>This causes the <code>BackButton</code> to call <code>onDidRemovePage</code>. We can override this behaviour by passing our own callback in the <code>onPressed</code> property.</p>
<pre><code class="lang-dart">BackButton(onPressed: rubigoRouter.ui.pop)
</code></pre>
<p>We can pass this to the <code>leading</code> property of an <code>AppBar</code>. But this will cause it to be shown always. That is not what we want because the standard behaviour of the <code>AppBar</code> is to hide the <code>BackButton</code> on the first page of the app.</p>
<p>This can be accomplished the same with this helper method. It also causes the <code>AppBar</code> to rebuild when the <code>ModalRoute.canPopOf</code> property changes.</p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Use this function for [AppBar.leading] to show a standard BackButton that</span></span>
<span class="hljs-comment">/// <span class="markdown">delegates onPressed to the [RubigoRouter.ui].pop function.</span></span>
Widget? rubigoBackButton(
  BuildContext context,
  RubigoRouter rubigoRouter,
) {
  <span class="hljs-keyword">return</span> ModalRoute.canPopOf(context) ?? <span class="hljs-keyword">false</span>
      ? BackButton(onPressed: rubigoRouter.ui.pop)
      : <span class="hljs-keyword">null</span>;
}
</code></pre>
<p>This is how the “fixed” <code>AppBar</code> looks like:</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">return</span> Scaffold(
    appBar: AppBar(
      leading: rubigoBackButton(context, controller.rubigoRouter),
    ...
    ),
  );
}
</code></pre>
<p>Be aware that this only fixes the <code>AppBar</code> when using a <code>BackButton</code>. The <code>AppBar</code> can also use a <code>DrawerIconButton</code> or a <code>CloseButton</code> and they may need similar fixes.</p>
<h2 id="heading-android-back-button">Android back button</h2>
<p>The Android back button can be a dedicated (hardware) button on the device, or a (software) button provided by the OS. You can catch Android back button presses by providing a <code>BackButtonDispatcher</code> to the <code>MaterialApp</code>. Here below I show it with a <code>RootBackButtonDispatcher</code>.</p>
<pre><code class="lang-dart">  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp.router(
      backButtonDispatcher: RootBackButtonDispatcher,
      ...
      builder: (context, child) {
        <span class="hljs-keyword">return</span> ...
        );
      },
    );
  }
</code></pre>
<p>The standard <code>RootBackButtonDispatcher</code> returns false when <code>didPopRoute()</code> is called, to indicate that the pop was not handled.</p>
<p>We have to provide our own implementation of a <code>RootBackButtonDispatcher</code>, to handle back button presses.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RubigoRootBackButtonDispatcher</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">RootBackButtonDispatcher</span> </span>{
  ...

  <span class="hljs-meta">@override</span>
  Future&lt;<span class="hljs-built_in">bool</span>&gt; didPopRoute() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (currentRouteIsPage(rubigoRouter)) {
      <span class="hljs-comment">//Current route is page based route (MaterialPage or CupertinoPage)</span>
      <span class="hljs-keyword">await</span> rubigoRouter.ui.pop();
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    }
    <span class="hljs-comment">// This is for the default back button behaviour. For example to close a dialog when</span>
    <span class="hljs-comment">// the user presses the Android hardware back button.</span>
    <span class="hljs-comment">// Current route is a pageless route. super.didPopRoute() called.',</span>
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">super</span>.didPopRoute();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  }
}
</code></pre>
<p>This is the implementation of <code>currentRouteIsPage()</code>.<br />Please see this stack overflow post, answered by Rémi Rousselet:<br /><a target="_blank" href="https://stackoverflow.com/questions/50817086/how-to-check-which-the-current-route-is">https://stackoverflow.com/questions/50817086/how-to-check-which-the-current-route-is</a></p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Check if the current topmost route is a pageless route or a</span></span>
<span class="hljs-comment">/// <span class="markdown">page(full)route.</span></span>
<span class="hljs-built_in">bool</span> currentRouteIsPage(RubigoRouter rubigoRouter) {
  <span class="hljs-keyword">var</span> isPage = <span class="hljs-keyword">false</span>;
  <span class="hljs-keyword">final</span> context = rubigoRouter.navigatorKey.currentContext;
  <span class="hljs-keyword">if</span> (context == <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
  }
  <span class="hljs-comment">// https://stackoverflow.com/questions/50817086/how-to-check-which-the-current-route-is</span>
  Navigator.of(context).popUntil(
    (route) {
      <span class="hljs-keyword">if</span> (route.settings <span class="hljs-keyword">is</span> Page) {
        isPage = <span class="hljs-keyword">true</span>;
      }
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
    },
  );
  <span class="hljs-keyword">return</span> isPage;
}
</code></pre>
<h2 id="heading-back-gestures">Back gestures</h2>
<p>It is not possible to know upfront that a back gesture is going to happen because it is a user interaction. The user slides the page to the right, we can not know that upfront.</p>
<p>While it is possible to disable back gestures with a <code>PopScope</code> widget, this is probably not what you want. Popping pages by sliding the page to the right feels natural when interacting with apps on mobile devices.</p>
<p>So here we have to use the <code>onDidRemovePage</code> callback, but there are some caveats:</p>
<ol>
<li><p>onDidRemovePage is called for every <code>Page</code> that is removed from the stack.</p>
</li>
<li><p>onDidRemovePage is called when the transition has not finished yet.</p>
</li>
</ol>
<h3 id="heading-ondidremovepage-is-called-for-every-page">onDidRemovePage is called for every Page</h3>
<p>Although this might sounds obvious, this is not how, the now deprecated, <code>onPopPage</code> worked. The <code>onPopPage</code> callback was called when the user wanted to navigate back. It implicitly meant the user wanted to pop the topmost page and it was possible to block the pop by returning false.</p>
<p>The new <code>onDidRemovePage</code> is here to support back gestures, so it doesn’t provide a way to block the pop. In addition to that, it is called for every page that is removed from the stack, so also for pages that are not on the top.</p>
<p>For example, assume we replace this stack of screens:</p>
<ol>
<li><p>Page1</p>
</li>
<li><p>Page2</p>
</li>
<li><p>Page3</p>
</li>
</ol>
<p>With this stack of screens:</p>
<ol>
<li><p>Page4</p>
</li>
<li><p>Page5</p>
</li>
<li><p>Page6</p>
</li>
</ol>
<p>For this example <code>onDidRemovePage</code> is called three times:</p>
<ol>
<li><p><code>onDidRemovePage(Page3)</code></p>
</li>
<li><p><code>onDidRemovePage(Page2)</code></p>
</li>
<li><p><code>onDidRemovePage(Page1)</code></p>
</li>
</ol>
<p>The most easy solution is to ignore calls to <code>onDidRemovePage</code> when the removed page is not the one that is current on top.</p>
<h3 id="heading-ondidremovepage-is-called-when-the-transition-has-not-finished-yet">onDidRemovePage is called when the transition has not finished yet.</h3>
<p>Another challenge is that while we are informed that the topmost page has popped, the transition is still ongoing. Assume we wanted to ask the user if he is sure about his action, and we want to ask that with a standard call to <code>showDialog</code>. Chances are that you will never see that dialog.</p>
<p>Dialogs are pushed on the stack as pageless routes. Pageless routes are connected to the <code>Page</code> they are on. If you show the dialog too early, it will be pushed on the <code>Page</code> that is about to be popped.</p>
<p>Luckily there are some hooks that we can use to wait for the transition to finish.</p>
<p>The <code>NavigatorState</code> has a property <code>userGestureInProgressNotifier</code>, which tells us if the <code>onDidRemovePage</code> was caused by a back gesture and when the gesture is finished.</p>
<h3 id="heading-complete-implementation">Complete implementation</h3>
<p>For completeness this is the implementation I use:</p>
<pre><code class="lang-dart">  <span class="hljs-keyword">void</span> onDidRemovePage(Page&lt;<span class="hljs-built_in">Object?</span>&gt; page) {
    <span class="hljs-keyword">final</span> pageKey = page.key;
    <span class="hljs-keyword">if</span> (pageKey == <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> txt =
          <span class="hljs-string">'PANIC: page.key must be of type ValueKey&lt;<span class="hljs-subst">$SCREEN_ID</span>&gt;, but found '</span>
          <span class="hljs-string">'null.'</span>;
      unawaited(_logNavigation(txt));
      <span class="hljs-keyword">throw</span> UnsupportedError(txt);
    }
    <span class="hljs-keyword">if</span> (pageKey <span class="hljs-keyword">is</span>! ValueKey&lt;SCREEN_ID&gt;) {
      <span class="hljs-keyword">final</span> txt =
          <span class="hljs-string">'PANIC: page.key must be of type ValueKey&lt;<span class="hljs-subst">$SCREEN_ID</span>&gt;, but found '</span>
          <span class="hljs-string">'<span class="hljs-subst">${pageKey.runtimeType}</span>.'</span>;
      unawaited(_logNavigation(txt));
      <span class="hljs-keyword">throw</span> UnsupportedError(txt);
    }
    <span class="hljs-keyword">final</span> removedScreenId = pageKey.value;
    <span class="hljs-keyword">final</span> lastScreenId = _rubigoStackManager.screens.last.screenId;
    <span class="hljs-keyword">if</span> (removedScreenId != lastScreenId) {
      <span class="hljs-comment">// With this new event, we also receive this event when pages are removed</span>
      <span class="hljs-comment">// programmatically from the stack. Here onDidRemovePage was (probably)</span>
      <span class="hljs-comment">// initiated by the business logic, as the last page on the stack is not</span>
      <span class="hljs-comment">// the one that got removed. In this case the screenStack is already</span>
      <span class="hljs-comment">// valid.</span>
      unawaited(
        _logNavigation(
          <span class="hljs-string">'onDidRemovePage(<span class="hljs-subst">${removedScreenId.name}</span>) called. Last page is '</span>
          <span class="hljs-string">'<span class="hljs-subst">${lastScreenId.name}</span>, ignoring.'</span>,
        ),
      );
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// handle the back event.</span>
    unawaited(
      _logNavigation(
        <span class="hljs-string">'onDidRemovePage(<span class="hljs-subst">${removedScreenId.name}</span>) called.'</span>,
      ),
    );

    Future&lt;<span class="hljs-keyword">void</span>&gt; callPop() <span class="hljs-keyword">async</span> {
      <span class="hljs-comment">// This function calls ui.pop and keeps track if updateScreens is being</span>
      <span class="hljs-comment">// called, while executing ui.pop().</span>
      <span class="hljs-keyword">var</span> updateScreensIsCalled = <span class="hljs-keyword">false</span>;
      <span class="hljs-keyword">void</span> updateScreenCallback() =&gt; updateScreensIsCalled = <span class="hljs-keyword">true</span>;
      _rubigoStackManager.updateScreensCallBack.add(updateScreenCallback);
      <span class="hljs-keyword">await</span> ui.pop();
      <span class="hljs-keyword">if</span> (!updateScreensIsCalled) {
        <span class="hljs-keyword">await</span> _rubigoStackManager.updateScreens();
      }
      _rubigoStackManager.updateScreensCallBack.remove(updateScreenCallback);
    }

    <span class="hljs-keyword">final</span> navState = _navigatorKey.currentState;
    <span class="hljs-keyword">if</span> (navState == <span class="hljs-keyword">null</span>) {
      <span class="hljs-comment">// We cannot continue if we cannot access Flutter's navigator.</span>
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// Remove the last page, so the screens is the same as Flutter expects.</span>
    <span class="hljs-comment">// Just in case the widget tree rebuilds for some reason.</span>
    <span class="hljs-comment">// Note: this will not inform the listeners, this is intended behavior.</span>
    screens.removeLast();

    Future&lt;<span class="hljs-keyword">void</span>&gt; gestureCallback() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> inProgress = navState.userGestureInProgress;
      <span class="hljs-keyword">if</span> (!inProgress) {
        navState.userGestureInProgressNotifier.removeListener(gestureCallback);
        <span class="hljs-keyword">await</span> callPop();
      }
    }

    <span class="hljs-keyword">if</span> (navState.userGestureInProgress) {
      <span class="hljs-comment">// We have to wait for the gesture to finish. Otherwise pageless routes,</span>
      <span class="hljs-comment">// that might have been added in mayPop (like showDialog), are popped</span>
      <span class="hljs-comment">// together with the page. That is how Navigator 2.0 works, nothing we</span>
      <span class="hljs-comment">// can about that. Wait for the gesture to complete and the perform the</span>
      <span class="hljs-comment">// pop().</span>
      navState.userGestureInProgressNotifier.addListener(gestureCallback);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">final</span> warningText = <span class="hljs-string">'''
RubigoRouter warning.
"onDidRemovePage called" for <span class="hljs-subst">${removedScreenId.name}</span>, but the source was not a userGesture. 
The cause is most likely that Navigator.maybePop(context) was (indirectly) called.
This can happen when:
- A regular BackButton was used to pop this page. Solution: Use a rubigoBackButton in the AppBar.
- The MaterialApp.backButtonDispatcher was not a RubigoRootBackButtonDispatcher.
- The pop was not caught by a RubigoBackGesture widget.
'''</span>;
    unawaited(_logNavigation(warningText));

    <span class="hljs-comment">// This is a workaround for the following exception:</span>
    <span class="hljs-comment">// Unhandled Exception: 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 4931 pos 12: '!_debugLocked': is not true.</span>
    <span class="hljs-comment">// Which can happen if a showDialog (or other pageless route) was pushed in</span>
    <span class="hljs-comment">// the mayPop callback. This is a workaround and should not end up in</span>
    <span class="hljs-comment">// production. Read the warning here above.</span>
    Future.delayed(<span class="hljs-built_in">Duration</span>.zero, callPop);
  }
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Navigator onPopPage versus onDidRemovePage]]></title><description><![CDATA[During the development of my RubigoRouter package I ran into some limitations of the Navigator object that I explain here below.
The Flutter Navigator has a mechanism to inform the app about a back navigation event.
Historically the onPopPage callbac...]]></description><link>https://yapb.dev/navigator-onpoppage-versus-ondidremovepage</link><guid isPermaLink="true">https://yapb.dev/navigator-onpoppage-versus-ondidremovepage</guid><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Thu, 23 Jan 2025 20:14:12 GMT</pubDate><content:encoded><![CDATA[<p>During the development of my <a target="_blank" href="https://github.com/jsroest/rubigo_router">RubigoRouter</a> package I ran into some limitations of the <code>Navigator</code> object that I explain here below.</p>
<p>The Flutter Navigator has a mechanism to inform the app about a back navigation event.</p>
<p>Historically the <code>onPopPage</code> callback was used for this, but with the new <a target="_blank" href="https://docs.flutter.dev/platform-integration/android/predictive-back">predictive-back gesture</a> or the iOS-style back swipe, a new <code>onDidRemovePage</code> callback is introduced. Below, we can compare the two signatures.</p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Signature for the [Navigator.onPopPage] callback.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// This callback must call [Route.didPop] on the specified route and must</span></span>
<span class="hljs-comment">/// <span class="markdown">properly update the pages list the next time it is passed into</span></span>
<span class="hljs-comment">/// <span class="markdown">[Navigator.pages] so that it no longer includes the corresponding [Page].</span></span>
<span class="hljs-comment">/// <span class="markdown">(Otherwise, the page will be interpreted as a new page to show when the</span></span>
<span class="hljs-comment">/// <span class="markdown">[Navigator.pages] list is next updated.)</span></span>
<span class="hljs-keyword">typedef</span> PopPageCallback = <span class="hljs-built_in">bool</span> <span class="hljs-built_in">Function</span>(Route&lt;<span class="hljs-built_in">dynamic</span>&gt; route, <span class="hljs-built_in">dynamic</span> result);

<span class="hljs-comment">/// <span class="markdown">Signature for the [Navigator.onDidRemovePage] callback.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// This must properly update the pages list the next time it is passed into</span></span>
<span class="hljs-comment">/// <span class="markdown">[Navigator.pages] so that it no longer includes the input <span class="hljs-code">`page`</span>.</span></span>
<span class="hljs-comment">/// <span class="markdown">(Otherwise, the page will be interpreted as a new page to show when the</span></span>
<span class="hljs-comment">/// <span class="markdown">[Navigator.pages] list is next updated.)</span></span>
<span class="hljs-keyword">typedef</span> DidRemovePageCallback = <span class="hljs-keyword">void</span> <span class="hljs-built_in">Function</span>(Page&lt;<span class="hljs-built_in">Object?</span>&gt; page);
</code></pre>
<p>The differences between the two are:</p>
<ul>
<li><p><code>onPopPage</code></p>
<ul>
<li><p>This callback is called before the pop is executed. With the <code>boolean</code> return value we can instruct the Navigator whether or not to actually perform the pop. (In my old code, I always returned false and then handled the pop in my own code, informing Flutter if the stack did change.)</p>
</li>
<li><p>This callback is called only for the topmost route.</p>
</li>
</ul>
</li>
<li><p><code>onDidRemovePage</code></p>
<ul>
<li><p>This callback is called after the pop transition has started. There is no way to prevent the pop.</p>
</li>
<li><p>This callback is called individually for each and every page that leaves the stack. We can have the same behaviour as <code>onPopPage</code> if we check if the provided page parameter corresponds to the current topmost page. Although I’m a bit concerned about race conditions..</p>
</li>
</ul>
</li>
</ul>
<p>I have seen some problems in the past with <code>onPopPage</code> and using back gestures, where the animation does not finish and the two pages get stuck. But this is something I haven’t investigated further. Also because <code>onPopPage</code> is deprecated after v3.16.0-17.0.pre.</p>
<h2 id="heading-disable-back-gestures-and-back-button">Disable back gestures and back button</h2>
<p>If you know in advance that the page can not be popped, you can use the <code>PopScope</code> widget.</p>
<pre><code class="lang-dart">  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> PopScope(
      canPop: <span class="hljs-keyword">false</span>,
      child: Scaffold(),
    );
  }
}
</code></pre>
<p><strong>Downside:</strong> This will disable the back-gestures, but a standard <code>AppBar</code> still shows a clickable <code>BackButton</code> that does nothing, and that is confusing.</p>
<h2 id="heading-disable-back-gestures-but-respond-to-back-button">Disable back-gestures but respond to back button</h2>
<p>If you want to disable the back gestures, but still want to respond to <code>BackButton</code> events, you can also use the <code>PopScope</code> widget, but with the <code>onPopInvokedWithResult</code> property.</p>
<pre><code class="lang-dart">  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> PopScope(
      canPop: <span class="hljs-keyword">false</span>,
      onPopInvokedWithResult:(didPop, result) =&gt; controller.didPop(),
      child: Scaffold(),
    );
  }
}
</code></pre>
<p><strong>Downside:</strong> we have to do this trickery on each page. It is also a shame that we lose support for back-gestures.</p>
<h2 id="heading-what-if-we-are-stubborn-and-want-it-all">What if we are stubborn and want it all?</h2>
<p>What if I want the following:</p>
<ul>
<li><p>Have back-gestures enabled.</p>
</li>
<li><p>A working <code>BackButton</code>.</p>
</li>
<li><p>No widgets like `PopScope` to do some magic.</p>
</li>
<li><p>Use the new <code>onDidRemovePage</code> callback.</p>
</li>
<li><p>Alter the page stack to my liking (like undo the pop) after onDidRemovePage has been called.</p>
</li>
</ul>
<h3 id="heading-with-the-back-gesture-it-works-surprisingly-well">With the back-gesture it works surprisingly well.</h3>
<p>Here I made a video, where I show a few times a successful back-gesture, and then a few times a cancelled back-gesture. I swiped the page to 2/3 to the right and then let go.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/MNOsZaGpMWI">https://youtu.be/MNOsZaGpMWI</a></div>
<p> </p>
<p>The navigator animates nicely and naturally back to the original screen.</p>
<h3 id="heading-with-the-backbutton-it-works-not-that-well">With the BackButton it works not that well.</h3>
<p>Here I made a video, where I show a few times a successful back button event, and a few times a cancelled back button event.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/wiDYKkpgAnM">https://youtu.be/wiDYKkpgAnM</a></div>
<p> </p>
<p>The navigator quickly animates back and forth between the pages. This looks really terrible.</p>
<p>It seems we need both <code>onPopPage</code> and <code>onDidRemovePage</code>, but one callback for the back button and the other for back-gestures.</p>
<ul>
<li><p><strong>Back button</strong><br />  callback: <strong>onPopPage</strong><br />  We return false to inform the <code>Navigator</code> we handle the pop ourselves. The transition animation is therefore not started yet. If we want to honour the pop we can inform the <code>Navigator</code> about the new screen stack by calling <code>notifyListeners</code>, otherwise we do nothing.</p>
</li>
<li><p><strong>Back-gesture</strong></p>
<p>  <strong>callback: onDidRemovePage</strong><br />  We can cancel the event by just notifying the <code>Navigator</code> about the ‘new’ screen stack, by calling <code>notifyListeners</code>.</p>
</li>
</ul>
<p>You can find the package that I am working on here, with a working sample.<br /><a target="_blank" href="https://github.com/jsroest/rubigo_router">https://github.com/jsroest/rubigo_router</a></p>
<p>Let me know what you think!</p>
<p>Sander</p>
<p>»As of the time of this writing, the stable version of Flutter is V3.27.3«</p>
]]></content:encoded></item><item><title><![CDATA[Distributing your apps by APK]]></title><description><![CDATA[In my experience with Line of Bussiness apps, apps are not distributed by the Google Play store, but by an Enterprise Mobility Management platform like SOTI MobiControl.
This means that you most likely will distribute your app as an APK and not as an...]]></description><link>https://yapb.dev/distributing-your-apps-by-apk</link><guid isPermaLink="true">https://yapb.dev/distributing-your-apps-by-apk</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Fri, 09 Jun 2023 10:13:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/W9f-PrkRqk0/upload/93e8c177aa397cb88735dddbe48f364b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my experience with Line of Bussiness apps, apps are not distributed by the Google Play store, but by an Enterprise Mobility Management platform like <a target="_blank" href="https://soti.net/solutions/mobile-device-management/">SOTI MobiControl</a>.</p>
<p>This means that you most likely will distribute your app as an APK and not as an "App Bundle".</p>
<p>In this post, I will discuss two pitfalls that you might encounter:</p>
<ul>
<li><p><strong>versionCode</strong>: the version number on which Android decides which version of the APK is newer.</p>
</li>
<li><p><strong>keystore</strong>: each app is signed by a cryptographic key and that key has to be the same to allow app upgrades.</p>
</li>
</ul>
<h3 id="heading-versioncode">versionCode</h3>
<p>The versionCode is what Android uses to determine the version of an app.</p>
<p>Let's consider this part of the <code>pubspec.yaml</code> file that is present in any Flutter app.</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">version:</span> <span class="hljs-number">1.0</span><span class="hljs-number">.0</span><span class="hljs-string">+1</span>
</code></pre>
<p>In the sample here above, 1.0.0 is the versionName and 1 is the versionCode. The versionName is a String and is only used for display purposes. What counts for Android is the versionCode, which is an integer. A higher number means a newer version and a lower number means an older version.</p>
<p>Normally it is not allowed to downgrade an app on Android. The only solution to that is to deinstall the app first which, depending on the setup, can happen automatically. A deinstall causes all local data and local configuration to get lost. This can be quite dramatic in Enterprise environments if the employees did not upload their work first.</p>
<p>So in an Enterprise Environment, when you deploy a new version, the versionCode of the new app must be higher than any released older versions.</p>
<p>This might sound like a trivial step, you just have to change the +1 in the sample above to +2, but there is a catch.</p>
<p><strong>It is not 100% guaranteed that the versionCode that you specify in the</strong> <code>pubspec.yaml</code> <strong>file is the number that ends up in the generated APK.</strong></p>
<p>Assume this build of a Flutter app, which creates a <a target="_blank" href="https://docs.flutter.dev/deployment/android#what-is-a-fat-apk">'fat APK'</a>.</p>
<pre><code class="lang-plaintext">% flutter build apk
Running Gradle task 'assembleRelease'...                           72.5s
✓  Built build/app/outputs/flutter-apk/app-release.apk (22.1MB).
</code></pre>
<p>Let's examine the versionCode of the generated app-release.apk.</p>
<pre><code class="lang-plaintext">% aapt dump badging ./build/app/outputs/flutter-apk/app-release.apk           
package: name='com.example.demo' versionCode='1' versionName='1.0.0' compileSdkVersion='33' compileSdkVersionCodename='13'
</code></pre>
<p>Here the versionCode = 1, exactly what we expected with <code>version: 1.0.0+1</code> in the <code>pubspec.yaml</code> file.</p>
<p>Now consider this build of a Flutter app, which creates an APK for each platform (ABI). You might want to do this to shrink the size of the APK and because you think it might not hurt if you only target ARM64 devices.</p>
<pre><code class="lang-plaintext">% flutter build apk --split-per-abi
Running Gradle task 'assembleRelease'...                           15.3s
✓  Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (7.7MB).
✓  Built build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (8.0MB).
✓  Built build/app/outputs/flutter-apk/app-x86_64-release.apk (8.1MB).
</code></pre>
<p>Let's examine the versionCode of app-arm64-v8a-release.apk.</p>
<pre><code class="lang-plaintext">% aapt dump badging ./build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
package: name='com.dalosy.warehouse_app' versionCode='2001' versionName='1.0.0' compileSdkVersion='33' compileSdkVersionCodename='13'
</code></pre>
<p>Here the versionCode = 2001, this is not what we expected 🤯.</p>
<p>This is of course by design, see the following issue: <a target="_blank" href="https://github.com/flutter/flutter/issues/39817">flutter build apk --split-per-abi generates apk with wrong version code</a>.</p>
<p>In this issue the following is referenced: <a target="_blank" href="https://developer.android.com/build/configure-apk-splits#configure-APK-versions">https://developer.android.com/build/configure-apk-splits#configure-APK-versions</a></p>
<p>Here we can read the following:</p>
<blockquote>
<p>Because the Google Play Store doesn't allow multiple APKs for the same app that all have the same version information, you need to ensure that each APK has a unique versionCode before you upload to the Play Store.</p>
</blockquote>
<p>In the same article, a solution is discussed where the versionCode on each platform is incremented with a multiple of 1000 and so overcomes the Play Stores' limitation.</p>
<p>This is exactly how it is implemented in the toolchain of Flutter. Because of the open nature of Flutter, you can find the code that does this yourself.</p>
<p>In the file <a target="_blank" href="https://github.com/flutter/flutter/blob/3.10.4/packages/flutter_tools/gradle/flutter.gradle"><code>flutter.gradle</code></a> you can find on row 897 the following code that makes this happen:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">if</span> (shouldSplitPerAbi()) {
    variant.outputs.each { output -&gt;
        <span class="hljs-comment">// Assigns the new version code to versionCodeOverride, which changes the version code</span>
        <span class="hljs-comment">// for only the output APK, not for the variant itself. Skipping this step simply</span>
        <span class="hljs-comment">// causes Gradle to use the value of variant.versionCode for the APK.</span>
        <span class="hljs-comment">// For more, see https://developer.android.com/studio/build/configure-apk-splits</span>
        def abiVersionCode = ABI_VERSION.<span class="hljs-keyword">get</span>(output.getFilter(OutputFile.ABI))
        <span class="hljs-keyword">if</span> (abiVersionCode != <span class="hljs-literal">null</span>) {
            output.versionCodeOverride =
                abiVersionCode * <span class="hljs-number">1000</span> + variant.versionCode
        }
    }
}
</code></pre>
<p>So, what does that mean, for us, the Enterprise Line of Business app programmers?</p>
<ol>
<li><p>Keep using the same kind of APK for each of your existing projects. Do not switch from an "APK per abi" to a fat APK as this might result in unexpected app uninstalls and data loss for your customers.</p>
</li>
<li><p>Consider using fat APKs for all your projects, even though the APK has an increased size. Only this type of APK has the expected original versionCode.</p>
</li>
<li><p>Using "fat APKs" also prevents unexpected reinstalls while installing a debug version on a device that has a release build on it. Debug builds always use the same original versionCode as fat APKs.</p>
</li>
</ol>
<h3 id="heading-keystore">Keystore</h3>
<p>Each Android app is signed with a cryptographic key.</p>
<p>On large projects, with a lot of developers, or on high-profile apps, you do not want to store the cryptographic key with your source files. In that case, it is better to have the app signed by a CI/CD pipeline.</p>
<p>But if you are developing an app that is not distributed by the Play Store and is not a high-profile app, setting up a CI/CD pipeline is overkill and complicates the workflow for the developer without bringing reasonable benefits.</p>
<p>And if the release APK is signed with a different key than the debug APK, it will not be possible to install a debug version over a release version. The installed version is then uninstalled before the debug version is installed. This causes the loss of data that you might want to keep for your debugging session.</p>
<p>Therefore I choose the approach as explained <a target="_blank" href="https://docs.flutter.dev/deployment/android#create-an-upload-keystore">here</a> in the Flutter documentation.</p>
<ol>
<li><p>Create a keystore with <code>keytool</code> and place that in <code>./android/keystore</code>.</p>
</li>
<li><p>Create a <code>key.properties</code> file in <code>./android</code> .</p>
</li>
<li><p>Change the <code>build.gradle</code> file so the local keystore is used.</p>
</li>
</ol>
<p>This solution will <strong>not</strong> work cross-platform, due to the different path conventions between Windows, macOS, and Linux.</p>
<p>Therefore I changed the <code>build.gradle</code> a bit to make it work cross-platform too.</p>
<pre><code class="lang-plaintext">./flutter_app/android/key.properties
./flutter_app/android/keystore/upload-keystore.jks
./flutter_app/android/app/src/build.gradle
</code></pre>
<p>Sample key.properties:</p>
<pre><code class="lang-plaintext">storePassword=comedown-twiddle-hoax
keyPassword=comedown-twiddle-hoax
keyAlias=upload
storeFileWindows=..\\keystore\\upload-keystore.jks
storeFileLinux=../keystore/upload-keystore.jks
storeFileMac=../keystore/upload-keystore.jks
</code></pre>
<p>Here you see the different paths for Windows, Linux and macOS.</p>
<p>Sample build.gradle:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> org.apache.tools.ant.taskdefs.condition.Os
....
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file(<span class="hljs-string">'key.properties'</span>)
<span class="hljs-keyword">if</span> (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> new GradleException(<span class="hljs-string">"key.properties file not found, please define keystore file and password in key.properties"</span>)
}

def propertiesStoreFilename
<span class="hljs-keyword">if</span> (Os.isFamily(Os.FAMILY_WINDOWS)) {
    propertiesStoreFilename = file(keystoreProperties[<span class="hljs-string">'storeFileWindows'</span>])
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Os.isFamily(Os.FAMILY_MAC)) {
    propertiesStoreFilename = file(keystoreProperties[<span class="hljs-string">'storeFileMac'</span>])
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Os.isFamily(Os.FAMILY_UNIX)) {
    propertiesStoreFilename = file(keystoreProperties[<span class="hljs-string">'storeFileLinux'</span>])
} <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> new GradleException(<span class="hljs-string">"Unsupported OS Family: <span class="hljs-subst">${Os.os}</span>"</span>)
}

def propertiesStorePassword = keystoreProperties[<span class="hljs-string">'storePassword'</span>]
def propertiesKeyAlias = keystoreProperties[<span class="hljs-string">'keyAlias'</span>]
def propertiesKeyPassword = keystoreProperties[<span class="hljs-string">'keyPassword'</span>]
....

android {
....
    signingConfigs {
        debug {
            storeFile file(propertiesStoreFilename)
            storePassword propertiesStorePassword
            keyAlias propertiesKeyAlias
            keyPassword propertiesKeyPassword
        }
        release {
            storeFile file(propertiesStoreFilename)
            storePassword propertiesStorePassword
            keyAlias propertiesKeyAlias
            keyPassword propertiesKeyPassword
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled <span class="hljs-literal">true</span>
        }
        debug {
            signingConfig signingConfigs.debug
        }
    }
}
</code></pre>
<p>Now your APK will be signed with a local keystore, independent from the platform you are developing on.</p>
<p>I hope you had a fun read, let me know what you think! 💙</p>
]]></content:encoded></item><item><title><![CDATA[13 Tips when working with .arb files for localization]]></title><description><![CDATA[Since Flutter version 1.22, Flutter has built-in support for localization.
This works pretty well and you can find the official documentation here:
Internationalizing Flutter apps
I have some tips and tricks that I would like to share with you.
TIP 1...]]></description><link>https://yapb.dev/tips-and-tricks-13-tips-when-working-with-arb-files-for-localization</link><guid isPermaLink="true">https://yapb.dev/tips-and-tricks-13-tips-when-working-with-arb-files-for-localization</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[localization]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sat, 21 May 2022 10:47:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/EgwhIBec0Ck/upload/v1653129778959/oY2-FV2XW.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Since Flutter version 1.22, Flutter has built-in support for localization.</p>
<p>This works pretty well and you can find the official documentation here:</p>
<p><a target="_blank" href="https://docs.flutter.dev/development/accessibility-and-localization/internationalization">Internationalizing Flutter apps</a></p>
<p>I have some tips and tricks that I would like to share with you.</p>
<h3 id="heading-tip-1-know-your-options">TIP 1: Know your options</h3>
<p>If you want to know all the options there are for localization, it is a good idea to run this command and examine the output.</p>
<pre><code class="lang-console">flutter gen-l10n -h
</code></pre>
<p>There are more configurable options than there are mentioned in the official documentation. For your convenience, this is the actual output:</p>
<pre><code class="lang-console">johannesroest@WS-SW103 app % fvm flutter gen-l10n -h                                                                                         
Generate localizations for the current project.

Global options:
-h, --help                  Print this usage information.
-v, --verbose               Noisy logging, including all shell commands executed.
                            If used with "--help", shows hidden options. If used with "flutter doctor", shows additional diagnostic information. (Use "-vv" to force verbose logging in those cases.)
-d, --device-id             Target device id or name (prefixes allowed).
    --version               Reports the version of this tool.
    --suppress-analytics    Suppress analytics reporting when this command runs.

Usage: flutter gen-l10n [arguments]
-h, --help                                                      Print this usage information.
    --arb-dir                                                   The directory where the template and translated arb files are located.
                                                                (defaults to "lib/l10n")
    --output-dir                                                The directory where the generated localization classes will be written if the synthetic-package flag is set to false.

                                                                If output-dir is specified and the synthetic-package flag is enabled, this option will be ignored by the tool.

                                                                The app must import the file specified in the "--output-localization-file" option from this directory. If unspecified, this defaults to the same directory as the input directory specified in "--arb-dir".
    --template-arb-file                                         The template arb file that will be used as the basis for generating the Dart localization and messages files.
                                                                (defaults to "app_en.arb")
    --output-localization-file                                  The filename for the output localization and localizations delegate classes.
                                                                (defaults to "app_localizations.dart")
    --untranslated-messages-file                                The location of a file that describes the localization messages have not been translated yet. Using this option will create a JSON file at the target location, in the following format:

                                                                    "locale": ["message_1", "message_2" ... "message_n"]

                                                                If this option is not specified, a summary of the messages that have not been translated will be printed on the command line.
    --output-class                                              The Dart class name to use for the output localization and localizations delegate classes.
                                                                (defaults to "AppLocalizations")
    --preferred-supported-locales=&lt;locale&gt;                      The list of preferred supported locales for the application. By default, the tool will generate the supported locales list in alphabetical order. Use this flag if you would like to default to a different locale. For example, pass in "en_US" if you would like your app to default to American English on devices that support it. Pass this option multiple times to define multiple items.
    --header                                                    The header to prepend to the generated Dart localizations files. This option takes in a string.

                                                                For example, pass in "/// All localized files." if you would like this string prepended to the generated Dart file.

                                                                Alternatively, see the "--header-file" option to pass in a text file for longer headers.
    --header-file                                               The header to prepend to the generated Dart localizations files. The value of this option is the name of the file that contains the header text which will be inserted at the top of each generated Dart file.

                                                                Alternatively, see the "--header" option to pass in a string for a simpler header.

                                                                This file should be placed in the directory specified in "--arb-dir".
    --[no-]use-deferred-loading                                 Whether to generate the Dart localization file with locales imported as deferred, allowing for lazy loading of each locale in Flutter web.

                                                                This can reduce a web app’s initial startup time by decreasing the size of the JavaScript bundle. When this flag is set to true, the messages for a particular locale are only downloaded and loaded by the Flutter app as they are needed. For projects with a lot of different locales and many localization strings, it can be an performance improvement to have deferred loading. For projects with a small number of locales, the difference is negligible, and might slow down the start up compared to bundling the localizations with the rest of the application.

                                                                This flag does not affect other platforms such as mobile or desktop.
    --gen-inputs-and-outputs-list=&lt;path-to-output-directory&gt;    When specified, the tool generates a JSON file containing the tool's inputs and outputs named gen_l10n_inputs_and_outputs.json.

                                                                This can be useful for keeping track of which files of the Flutter project were used when generating the latest set of localizations. For example, the Flutter tool's build system uses this file to keep track of when to call gen_l10n during hot reload.

                                                                The value of this option is the directory where the JSON file will be generated.

                                                                When null, the JSON file will not be generated.
    --[no-]synthetic-package                                    Determines whether or not the generated output files will be generated as a synthetic package or at a specified directory in the Flutter project.

                                                                This flag is set to true by default.

                                                                When synthetic-package is set to false, it will generate the localizations files in the directory specified by arb-dir by default.

                                                                If output-dir is specified, files will be generated there.
                                                                (defaults to on)
    --project-dir=&lt;absolute/path/to/flutter/project&gt;            When specified, the tool uses the path passed into this option as the directory of the root Flutter project.

                                                                When null, the relative path to the present working directory will be used.
    --[no-]required-resource-attributes                         Requires all resource ids to contain a corresponding resource attribute.

                                                                By default, simple messages will not require metadata, but it is highly recommended as this provides context for the meaning of a message to readers.

                                                                Resource attributes are still required for plural messages.
    --[no-]nullable-getter                                      Whether or not the localizations class getter is nullable.

                                                                By default, this value is set to true so that Localizations.of(context) returns a nullable value for backwards compatibility. If this value is set to true, then a null check is performed on the returned value of Localizations.of(context), removing the need for null checking in user code.
</code></pre>
<h3 id="heading-tip-2-use-l10nyaml">TIP 2: Use l10n.yaml</h3>
<p>I like to store the options in the l10n.yaml file and check it into version control.</p>
<p>This is the configuration I use:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">arb-dir:</span> <span class="hljs-string">lib/l10n/arb</span>
<span class="hljs-attr">output-dir:</span> <span class="hljs-string">lib/l10n/generated</span>
<span class="hljs-attr">template-arb-file:</span> <span class="hljs-string">intl_nl.arb</span>
<span class="hljs-attr">output-localization-file:</span> <span class="hljs-string">l10n.dart</span>
<span class="hljs-attr">output-class:</span> <span class="hljs-string">S</span>
<span class="hljs-attr">synthetic-package:</span> <span class="hljs-literal">false</span>
<span class="hljs-attr">nullable-getter:</span> <span class="hljs-literal">false</span>
<span class="hljs-comment">#untranslated-messages-file: l10n_errors.txt</span>
</code></pre>
<h3 id="heading-tip-3-do-not-use-a-synthetic-package">TIP 3: Do not use a synthetic package</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">synthetic-package:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>I would like to set this value to false. So I can actually see and 'check in' the code that is generated. </p>
<p>This isn't fail-safe, as I sometimes have to flip this value to true for one compilation and back to false to fix a build error. I do not get this error a lot (it might be fixed in the meantime). I will update this post when I get this error again.</p>
<h3 id="heading-tip-4-configure-the-folders-for-the-source-and-target-files">TIP 4: Configure the folders for the source and target files</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">arb-dir:</span> <span class="hljs-string">lib/l10n/arb</span>
<span class="hljs-attr">output-dir:</span> <span class="hljs-string">lib/l10n/generated</span>
</code></pre>
<p>These are the folders where the localization files are stored.
The arb-dir is the folder where the tool can find the 'source' arb files.
The output-dir is the folder where the tool places the generated dart files.</p>
<p>Keep in mind, that the output-dir is only used when synthetic-package is set to false.</p>
<h3 id="heading-tip-5-name-the-output-localization-file">TIP 5: Name the output localization file</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">output-localization-file:</span> <span class="hljs-string">l10n.dart</span>
</code></pre>
<p>This defines the file that you will have to import in each dart file where you want to use localization. It is good to use something memorable. For me, this name works great with code completion when typing import 'l10n'.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:count_app/l10n/generated/l10n.dart'</span>;
</code></pre>
<h3 id="heading-tip-6-choose-a-short-name-for-the-localization-class">TIP 6: Choose a short name for the localization class</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">output-class:</span> <span class="hljs-string">S</span>
</code></pre>
<p>By default, the class name for the generated localizations is named 'AppLocalizations'. While this name is accurate, I think it's way too long if you need it in many locations in your source code.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// A bit too long </span>
AppLocalizations.of(context)
<span class="hljs-comment">// This look better </span>
S.of(context)
</code></pre>
<h3 id="heading-tip-7-dont-generate-nullable-getters">TIP 7: Don't generate nullable getters</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">nullable-getter:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>Unless you have some code that has to be compatible with nullable by default, I recommend setting this value to false. Otherwise, you will have to do a null check on each call to <code>S.of(context)!</code>, which is cumbersome and looks ugly.</p>
<h3 id="heading-tip-8-specify-the-template-arb-file">TIP 8: Specify the template arb file</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">template-arb-file:</span> <span class="hljs-string">intl_nl.arb</span>
</code></pre>
<p>If you use placeholders in your translations, you need to specify the type (string, int etc) and the name of that placeholder. This information is not needed in every arb file, but only in the 'template' arb file.</p>
<pre><code class="lang-json">  <span class="hljs-string">"s100LoggingOnToBranch"</span>: <span class="hljs-string">"Je gaat je aanmelden bij vestiging **{branchId}**."</span>,
  <span class="hljs-string">"@s100LoggingOnToBranch"</span>: {
    <span class="hljs-attr">"placeholders"</span>: {
      <span class="hljs-attr">"branchId"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"String"</span>
      }
    }
  },
</code></pre>
<h3 id="heading-tip-9-translate-per-page-and-number-your-pages">TIP 9: Translate per page and number your pages</h3>
<p>I like to number my pages and translate per page. Although this gives some duplicated translation values, I like the flexibility that it gives.</p>
<p>A sample of page names are:</p>
<ul>
<li>s100_sign_in_ax_page.dart</li>
<li>s200_main_menu_page.dart</li>
<li>s300_select_document_page.dart</li>
<li>s310_download_documents_page.dart</li>
<li>s320_upload_documents_page.dart</li>
</ul>
<p>I use this numbering, to group my translations. All translations for pages start with 'sxxx'. This makes refactoring also a bit easier. If I want to delete a page, I can easily delete the corresponding translations as well. Or if I want to change a translation I can easily see on which page that translation is (supposed to be) used.</p>
<p>It also narrows down the translations to choose from while working on a specific page, as all the translations on that page will start with the same identifier, e.g. s100*.</p>
<h3 id="heading-tip-10-regenerate-the-translations-while-programming">TIP 10: Regenerate the translations while programming</h3>
<p>Flutter does support 'hot reload' when adding or changing localizations while debugging. This is awesome, but the code is not generated on each change when there is no debug session running.</p>
<p>You can force the code to be regenerated with this command:</p>
<pre><code class="lang-console">flutter gen-l10n
</code></pre>
<p>This way you can easily add translations and enjoy code completion while programming.</p>
<h3 id="heading-tip-11-get-an-overview-of-missing-translations">TIP 11: Get an overview of missing translations</h3>
<p>Uncommenting the line <code>#untranslated-messages-file</code> and running <code>flutter gen-l10n</code>, will help you to find translations that are not yet translated for all languages.</p>
<pre><code class="lang-yaml"><span class="hljs-comment">#untranslated-messages-file: l10n_errors.txt</span>
</code></pre>
<h3 id="heading-tip-12-sort-the-arb-files-alphabetically-with-arbutils">TIP 12: Sort the arb files alphabetically with arb_utils</h3>
<p>You can only compare arb files comfortably if the order of the translations in each arb file is the same. It is really cumbersome and error-prone to manage the order of translations by hand.</p>
<p>Luckily there is a package that you can use to sort the arb files. It also keeps the placeholder lines in the template arb file together with the corresponding translation line.</p>
<p><a target="_blank" href="https://pub.dev/packages/arb_utils">https://pub.dev/packages/arb_utils</a></p>
<p>This command will sort the contents of an arb file alphabetically.</p>
<pre><code class="lang-console">dart pub global run arb_utils:sort lib/l10n/arb/intl_nl.arb;
</code></pre>
<p>The result will be a sorted arb file 🎉</p>
<pre><code class="lang-json">  <span class="hljs-string">"s110DialogPincodeErrorTitle"</span>: <span class="hljs-string">"Waarschuwing"</span>,
  <span class="hljs-string">"s110LoggingOnToBranch"</span>: <span class="hljs-string">"Je gaat je aanmelden bij vestiging **{branchId}**."</span>,
  <span class="hljs-string">"@s110LoggingOnToBranch"</span>: {
    <span class="hljs-attr">"placeholders"</span>: {
      <span class="hljs-attr">"branchId"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"String"</span>
      }
    }
  },
  <span class="hljs-string">"s110SubTitle"</span>: <span class="hljs-string">"Meld je aan"</span>,
</code></pre>
<h3 id="heading-tip-13-script-it">TIP 13: Script it!</h3>
<p>To support my workflow, I have added a script to my Flutter project in a <code>/scripts</code> folder. Although this script is for macOS, the intentions are clear and obvious, so it would not be too difficult to change this for a Windows environment.</p>
<p>This script sorts all my arb files, and after that, it calls flutter gen-l10n to generate the dart translation classes.</p>
<p>Now it is super easy to sort the arb files and generate the dart code by just executing a simple script.</p>
<pre><code class="lang-zsh"><span class="hljs-meta">#!/bin/zsh</span>
<span class="hljs-built_in">pushd</span> ..;
dart pub global run arb_utils:sort lib/l10n/arb/intl_fr.arb;
dart pub global run arb_utils:sort lib/l10n/arb/intl_nl.arb;
fvm flutter gen-l10n
<span class="hljs-built_in">popd</span>;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[How to parse fixed-length data and why you should avoid 'String.substring']]></title><description><![CDATA[Introduction
This article is about exchanging data with a fixed-length data format. It will tell you about the pros and cons of this data format. It shows and demonstrates an implementation in Dart that is easy to read and maintain. The implementatio...]]></description><link>https://yapb.dev/tips-and-tricks-how-to-parse-fixed-length-data-and-why-you-should-avoid-string-substring</link><guid isPermaLink="true">https://yapb.dev/tips-and-tricks-how-to-parse-fixed-length-data-and-why-you-should-avoid-string-substring</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 19 Jan 2022 09:55:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/io0ZLYbu31s/upload/v1642584738530/IcgAqTqCW.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction">Introduction</h3>
<p>This article is about exchanging data with a fixed-length data format. It will tell you about the pros and cons of this data format. It shows and demonstrates an implementation in Dart that is easy to read and maintain. The implementation supports characters that consist of more than one <strong>code unit</strong> like e.g. emojis. It will also show that the standard functions like <code>String.length</code> and <code>String.substring</code> may fail on emojis.</p>
<h3 id="heading-pros-and-cons-of-the-fixed-length-data-format">Pros and Cons of the fixed-length data format</h3>
<p>Data can be interchanged between systems in many different formats. The most well known format nowadays is  <strong><em>json</em></strong>, but other popular formats are <strong><em>xml</em></strong>, <strong><em>csv</em></strong> and <strong><em>fixed-length</em></strong>.</p>
<p>This article is about the <strong><em>fixed-length</em></strong> data format. It has some advantages over the other ones.</p>
<ul>
<li>No need to load all data into memory before the data can be used. This is especially useful when importing large datasets. The data can be read and processed in chunks.</li>
<li>No need to use escape characters, like you have to do with csv files. With csv files, there is always a problem when you want to use the character in the data that is also used to separate the values.</li>
</ul>
<p>Of course, there are also downsides to using a fixed-length data format.</p>
<ul>
<li>The sender and the receiver have to agree on the order of values and length of each value.</li>
<li>Each field has to be padded with trailing spaces or leading zeroes.</li>
<li>Each field is (obviously) fixed in length. An increase in length would need work on both the source and destination.</li>
</ul>
<h3 id="heading-data-definition">Data definition</h3>
<p>It is important to document the format, so the source and destination are both aware of the format used. A simple document could look like this:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Field</td><td>Type</td><td>Length</td><td>Padding</td></tr>
</thead>
<tbody>
<tr>
<td>first_name</td><td>char</td><td>10</td><td>Right with spaces</td></tr>
<tr>
<td>last_name</td><td>char</td><td>10</td><td>Right with spaces</td></tr>
<tr>
<td>age</td><td>integer</td><td>3</td><td>Left with zeroes</td></tr>
<tr>
<td>city</td><td>char</td><td>15</td><td>Right with spaces</td></tr>
<tr>
<td>country</td><td>char</td><td>20</td><td>Right with spaces</td></tr>
</tbody>
</table>
</div><h3 id="heading-sample-data">Sample data</h3>
<pre><code class="lang-text">----------------------------------------------------------
1234567890123456789012312345678901234512345678901234567890
Sander    Roest     049Rotterdam      The Netherlands     
Sandra    Roest     042Rotterdam      The Netherlands     
Jeffrey   Roest     009Rotterdam      The Netherlands     
Lucas     Roest     007Rotterdam      The Netherlands     
----------------------------------------------------------
</code></pre>
<h3 id="heading-implementation-in-dart">Implementation in Dart</h3>
<p>Writing the code to parse fixed-length data seems like an easy job. At first, it looks like you just have to substring all the fields out of the data. This is in fact true, but it might get messy and difficult to maintain when the data definition changes.</p>
<p>Another issue to consider is that the <code>String.length</code> and <code>String.substring</code> functions might not work in the way you think.</p>
<p>The <code>String</code> class works with <strong><em>code units</em></strong>. This means that you will get the length of a string in <strong><em>code units</em></strong> and not characters.</p>
<p>You can read all about it in this excellent post <a target="_blank" href="https://medium.com/dartlang/dart-string-manipulation-done-right-5abd0668ba3e">Dart string manipulation done right 👉</a>.</p>
<p>To overcome both problems, you can use this helper class:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FixedLengthParser</span> </span>{
  FixedLengthParser(<span class="hljs-built_in">String</span> value) : _characters = value.characters;

  <span class="hljs-keyword">final</span> Characters _characters;
  <span class="hljs-keyword">var</span> _index = <span class="hljs-number">0</span>;

  <span class="hljs-built_in">String</span> getByLength(<span class="hljs-built_in">int</span> length) {
    <span class="hljs-keyword">var</span> value = _characters.getRange(_index, _index + length);
    _index += length;
    <span class="hljs-keyword">return</span> value.string.trim();
  }
}
</code></pre>
<p>The usage of this class makes it easy to match the code with the data definition. If the length of a field changes, you will have to change it only in one place.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> parser = FixedLengthParser(line);
<span class="hljs-keyword">final</span> firstName = parser.getByLength(<span class="hljs-number">10</span>);
<span class="hljs-keyword">final</span> lastName = parser.getByLength(<span class="hljs-number">10</span>);
<span class="hljs-keyword">final</span> age = parser.getByLength(<span class="hljs-number">3</span>);
<span class="hljs-keyword">final</span> city = parser.getByLength(<span class="hljs-number">15</span>);
<span class="hljs-keyword">final</span> country = parser.getByLength(<span class="hljs-number">20</span>);
</code></pre>
<h3 id="heading-dartpad-sample">Dartpad sample</h3>
<p>With the dartpad sample you will be able to:</p>
<ul>
<li>Test the <code>FixedLengthParser</code> class (renamed to <code>FixedLengthParserCharacters</code>)</li>
<li>Observe that String.substring fails on Emojis</li>
<li>See a fun usage of the new <a target="_blank" href="https://medium.com/dartlang/dart-2-15-7e7a598e508a">constructor-tear-off</a> functionality in Dart to use the same code with a working (characters) and a failing (string) implementation.</li>
</ul>
<p><a target="_blank" href="https://dartpad.dev/?id=f4e6612c9f3ef3b5eb3e439923fe8a46">https://dartpad.dev/?id=f4e6612c9f3ef3b5eb3e439923fe8a46</a></p>
<p>The output looks like this where you can see that the <code>String</code> implementation fails on emojis.</p>
<pre><code class="lang-text">Parse the data using characters:
-------------------------------------------------
Firstname (10): 1234567890
Lastname (10): 1234567890
Age (3): 123
City (15): 123456789012345
Country (20): 12345678901234567890

Firstname (6): Sander
Lastname (5): Roest
Age (3): 049
City (9): Rotterdam
Country (15): The Netherlands

Firstname (10): 🥇🥇🥇🥇🥇🥇🥇🥇🥇🥇
Lastname (10): 🥈🥈🥈🥈🥈🥈🥈🥈🥈🥈
Age (3): 🎂🎂🎂
City (15): 🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘🏘
Country (20): 🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱🇳🇱

Parse the data using string (faulty):
-------------------------------------
Firstname (10): 1234567890
Lastname (10): 1234567890
Age (3): 123
City (15): 123456789012345
Country (20): 12345678901234567890

Firstname (6): Sander
Lastname (5): Roest
Age (3): 049
City (9): Rotterdam
Country (15): The Netherlands

Firstname (5): 🥇🥇🥇🥇🥇
Lastname (5): 🥇🥇🥇🥇🥇
Age (2): 🥈�
City (8): �🥈🥈🥈🥈🥈🥈🥈
Country (10): 🥈🎂🎂🎂🏘🏘🏘🏘🏘🏘
</code></pre>
<p>Happy parsing!</p>
]]></content:encoded></item><item><title><![CDATA[Customize and extend the Theme in Flutter]]></title><description><![CDATA[Intro
Theming your Flutter app can be a challenge. This post will try to help you with the following:

Provide sensible theme defaults for both light-mode and dark-mode.
Customize the default theme.
Extend the default theme.

This post comes with a  ...]]></description><link>https://yapb.dev/tips-and-tricks-customize-and-extend-the-theme-in-flutter</link><guid isPermaLink="true">https://yapb.dev/tips-and-tricks-customize-and-extend-the-theme-in-flutter</guid><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sat, 08 Jan 2022 13:02:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/OV44gxH71DU/upload/v1641642729772/ejMy_MF_n.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-intro">Intro</h3>
<p>Theming your Flutter app can be a challenge. This post will try to help you with the following:</p>
<ol>
<li>Provide sensible theme defaults for both light-mode and dark-mode.</li>
<li>Customize the default theme.</li>
<li>Extend the default theme.</li>
</ol>
<p>This post comes with a  <a target="_blank" href="https://github.com/jsroest/0220108-yapb_theme-rubigo">sample app</a> on Github. You might want to check it out because the code snippets in this post will make a lot more sense in the full context of the sample app.</p>
<p>Sample app:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1641647937826/9kyOKbCnB.gif" alt="file.gif" /></p>
<h3 id="heading-provide-sensible-theme-defaults">Provide sensible theme defaults</h3>
<p>If you want a quick start, I recommend using  <a target="_blank" href="https://twitter.com/RydMike">Mike Rydstrom's</a>  <a target="_blank" href="https://pub.dev/packages/flex_color_scheme">FlexColorScheme package</a>.</p>
<p>This truly amazing package will give you sensible defaults for light-mode and dark-mode by providing at least one color. </p>
<p><code>app_theme.dart</code></p>
<pre><code class="lang-dart">ThemeData <span class="hljs-keyword">get</span> _theme =&gt; _flexColorSchemeFactory(
  colors: FlexSchemeColor.from(
    primary: _basePrimaryColor,
    secondary: _baseSecondaryColor,
    error: _baseErrorColor,
  ),
  visualDensity: FlexColorScheme.comfortablePlatformDensity,
).toTheme
</code></pre>
<p>With that package initializing the theme in your will be as simple as:</p>
<p><code>main.dart</code></p>
<pre><code class="lang-dart">MaterialApp(
  title: <span class="hljs-string">'Flutter Demo'</span>,
  theme: <span class="hljs-keyword">const</span> AppThemeDataLight().theme,
  darkTheme: <span class="hljs-keyword">const</span> AppThemeDataDark().theme,
   home: <span class="hljs-keyword">const</span> MyHomePage(title: <span class="hljs-string">'Flutter Demo Home Page'</span>),
);
</code></pre>
<h3 id="heading-customize-the-default-theme">Customize the default theme</h3>
<p>If you want to change the default theme, you can use the copyWith method on the ThemeData class. This method allows you to swap some parts of the theme with your own.</p>
<p><code>app_theme.dart</code></p>
<pre><code class="lang-dart">ThemeData <span class="hljs-keyword">get</span> _theme =&gt; _flexColorSchemeFactory(
  colors: FlexSchemeColor.from(
    primary: _basePrimaryColor,
    secondary: _baseSecondaryColor,
    error: _baseErrorColor,
  ),
  visualDensity: FlexColorScheme.comfortablePlatformDensity,
).toTheme.copyWith(
  elevatedButtonTheme: _elevatedButtonThemeData(),
  outlinedButtonTheme: _outlinedButtonThemeData(),
  textButtonTheme: _textButtonThemeData(),
);
</code></pre>
<h3 id="heading-extend-the-default-theme">Extend the default theme</h3>
<p>Sometimes you want to add custom colors and properties to the theme. With an  <a target="_blank" href="https://stackoverflow.com/questions/49172746/is-it-possible-extend-themedata-in-flutter">extension method</a>  on ThemeData, this worked out pretty simple.</p>
<p><code>app_theme.dart</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> AppTheme <span class="hljs-keyword">on</span> ThemeData {
  AppThemeData <span class="hljs-keyword">get</span> appTheme =&gt; brightness == Brightness.dark
      ? <span class="hljs-keyword">const</span> AppThemeDataDark()
      : <span class="hljs-keyword">const</span> AppThemeDataLight();
}
</code></pre>
<p>With this extension method, getting custom colors and properties becomes really easy and intuitive.</p>
<p>'custom_button.dart'</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_riverpod/flutter_riverpod.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:yapb_theme/classes/app_theme.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ConsumerWidget</span> </span>{
  <span class="hljs-keyword">const</span> CustomButton(
    <span class="hljs-keyword">this</span>.text, {
    Key? key,
    <span class="hljs-keyword">this</span>.onPressed,
  }) : <span class="hljs-keyword">super</span>(key: key);
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> text;
  <span class="hljs-keyword">final</span> VoidCallback? onPressed;

  <span class="hljs-meta">@override</span>
  Widget build(context, ref) {
    <span class="hljs-keyword">return</span> OutlinedButton.icon(
      style: OutlinedButton.styleFrom(
        minimumSize: <span class="hljs-keyword">const</span> Size(<span class="hljs-number">48</span>, <span class="hljs-number">48</span>),
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">8</span>),
        side: Theme.of(context).appTheme.customButtonBorderSide,
      ),
      label: Text(
        text,
        style: TextStyle(
          fontWeight: FontWeight.w400,
          color: Theme.of(context).appTheme.customButtonColor,
        ),
      ),
      icon: Icon(
        Icons.flag,
        color: Theme.of(context).appTheme.customButtonColor,
      ),
      onPressed: onPressed,
    );
  }
}
</code></pre>
<h3 id="heading-about-the-sample-app">About the sample app</h3>
<p>In the sample app I have used the techniques from other posts in this blog. Key elements in this sample app are:</p>
<ol>
<li><a target="_blank" href="https://yapb.dev/mono-repositories-set-up-a-mono-repository">Set up a mono repository</a> <a target="_blank" href="https://github.com/jsroest/0000000-create_flutter_mono_repo-rubigo">with this dart script</a></li>
<li><a target="_blank" href="https://yapb.dev/replace-getter-and-setter-blocks-with-a-oneliner-when-using-changenotifier">Replace getter and setter blocks with a oneliner when using ChangeNotifier.</a></li>
<li><a target="_blank" href="https://pub.dev/packages?q=flutter_riverpod">Flutter Riverpod</a></li>
<li><a target="_blank" href="https://pub.dev/packages/flex_color_scheme">Flex color scheme</a>  </li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Replace getter and setter blocks with a oneliner when using ChangeNotifier.]]></title><description><![CDATA[When dart became ‘non-nullable by default’, an interesting keyword was added to the dart language: ‘late’.
Although it might not be so obvious at first glance, this keyword makes it possible to simplify getter and setter blocks when using a ChangeNot...]]></description><link>https://yapb.dev/replace-getter-and-setter-blocks-with-a-oneliner-when-using-changenotifier</link><guid isPermaLink="true">https://yapb.dev/replace-getter-and-setter-blocks-with-a-oneliner-when-using-changenotifier</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 11 Aug 2021 14:52:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/82TpEld0_e4/upload/v1641151203969/m-3w6q0bH.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When dart became ‘non-nullable by default’, an interesting keyword was added to the dart language: <strong>‘late’</strong>.</p>
<p>Although it might not be so obvious at first glance, this keyword makes it possible to simplify getter and setter blocks when using a ChangeNotifier to update values on the screen.</p>
<p>Normally a getter and setter block does look like this.</p>
<pre><code><span class="hljs-keyword">int</span> _counter = <span class="hljs-number">0</span>;

<span class="hljs-keyword">int</span> <span class="hljs-keyword">get</span> counter =&gt; _counter;

<span class="hljs-function"><span class="hljs-keyword">set</span> <span class="hljs-title">counter</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span></span>)</span> {
  <span class="hljs-keyword">if</span> (_counter != <span class="hljs-keyword">value</span>) {
    _counter = <span class="hljs-keyword">value</span>;
    notifyListeners();
  }
}
</code></pre><p>This code is not complicated. It consists of:</p>
<ul>
<li>a private backing variable</li>
<li>a public getter and setter</li>
<li>some logic that tests if the value has changed and calls ‘notifyListeners’ when needed</li>
</ul>
<p>There are also some drawbacks:</p>
<ul>
<li>You can access the backing variable directly (by accident).</li>
<li>It is easy to make mistakes like checking or assigning the wrong backing variable, especially when you have more than 3 pairs of getters and setters.</li>
<li>The code is boring (error-prone) and takes up too much space.</li>
<li>Don’t repeat yourself (DRY) principle is not respected.</li>
</ul>
<p>Now with the ‘late’ keyword, we can replace the code above with this one-liner:</p>
<pre><code><span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> counter = Property(<span class="hljs-number">0</span>, notifyListeners);
</code></pre><p>Without the ‘late’ keyword this would not have been possible, because it is not allowed to access member functions (in this case <code>notifyListeners</code>) in an initializer. With the late keyword, initialization is deferred until the property is first referenced.</p>
<p>The implementation of the Property class is simple and solves all drawbacks of a regular getter and setter block.</p>
<pre><code><span class="hljs-keyword">class</span> <span class="hljs-title">Property</span>&lt;<span class="hljs-title">T</span>&gt; {
  Property(T initialValue, <span class="hljs-keyword">this</span>.notifyListeners) {
    _value = initialValue;
  }

  late T _value;
  <span class="hljs-function">final <span class="hljs-keyword">void</span> <span class="hljs-title">Function</span>(<span class="hljs-params"></span>) notifyListeners</span>;

  T <span class="hljs-keyword">get</span> <span class="hljs-keyword">value</span> =&gt; _value;

  <span class="hljs-function"><span class="hljs-keyword">set</span> <span class="hljs-title">value</span>(<span class="hljs-params">T <span class="hljs-keyword">value</span></span>)</span> {
    <span class="hljs-keyword">if</span> (_value != <span class="hljs-keyword">value</span>) {
      _value = <span class="hljs-keyword">value</span>;
      notifyListeners();
    }
  }
}
</code></pre><p>This makes it possible to reduce the business logic of the default Flutter counter app to these few lines:</p>
<pre><code>class MainController extends ChangeNotifier {
  late final counter <span class="hljs-operator">=</span> Property<span class="hljs-operator">&lt;</span><span class="hljs-keyword">int</span><span class="hljs-operator">&gt;</span>(<span class="hljs-number">0</span>, notifyListeners);

  void incrementCounter() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> counter.<span class="hljs-built_in">value</span>+<span class="hljs-operator">+</span>;
}
</code></pre><div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/jsroest/pen/poWKLaG">https://codepen.io/jsroest/pen/poWKLaG</a></div>
<p>Source code to test this out can be found here:<br /><a target="_blank" href="https://github.com/jsroest/property_for_changenotifier">https://github.com/jsroest/property_for_changenotifier</a></p>
]]></content:encoded></item><item><title><![CDATA[Add a package to a mono repository]]></title><description><![CDATA[In this part, we are going to add a package to an existing mono-repo.
Open a terminal and go to the packages folder.
Go to the packages folder
cd ~/repos/0000000-demo_mono_repo_app-rubigo/packages
Create a Flutter package
fvm flutter create -t packag...]]></description><link>https://yapb.dev/mono-repositories-add-a-package-to-a-mono-repository</link><guid isPermaLink="true">https://yapb.dev/mono-repositories-add-a-package-to-a-mono-repository</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Android Studio]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sun, 07 Mar 2021 10:44:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/YU-OA2TvQRQ/upload/v1641150307603/qUQhX0gW2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this part, we are going to add a package to an existing mono-repo.</p>
<p>Open a terminal and go to the packages folder.</p>
<h3 id="heading-go-to-the-packages-folder">Go to the packages folder</h3>
<pre><code>cd <span class="hljs-operator">~</span><span class="hljs-operator">/</span>repos<span class="hljs-operator">/</span>0000000<span class="hljs-operator">-</span>demo_mono_repo_app<span class="hljs-operator">-</span>rubigo<span class="hljs-operator">/</span>packages
</code></pre><h3 id="heading-create-a-flutter-package">Create a Flutter package</h3>
<pre><code>fvm flutter create <span class="hljs-operator">-</span>t package <span class="hljs-operator">-</span><span class="hljs-operator">-</span>org com.example <span class="hljs-operator">-</span><span class="hljs-operator">-</span>project<span class="hljs-operator">-</span>name my_package ./my_package
</code></pre><h3 id="heading-openrestart-android-studio">Open/restart Android Studio</h3>
<p>Android studio might not detect the new folder. Restarting Android Studio fixes that.</p>
<h3 id="heading-commit-package">Commit package</h3>
<p>From now on we will use Android Studio to review and commit files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946013868/ixxq65HDl.png" alt />Choose “Commit”</p>
<h3 id="heading-add-package-as-a-module">Add package as a module</h3>
<p>Android Studio =&gt; File -&gt; Project Structure =&gt; Modules =&gt; + =&gt; Import Module</p>
<p>Navigate to my_package folder and choose “Open”. Be aware that the initial folder that Android Studio suggests might not be part of your current project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946016121/S-BEadBVI.png" alt />Select “Create module from existing sources”, choose “Next”<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946018819/eWXitIK1r.png" alt />Choose “Next”<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946021366/OjEHHGfQP.png" alt />Choose “Next”<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946023972/PN8jAIA3X-.png" alt />Choose “OK”</p>
<h3 id="heading-commit-add-a-package-as-a-module">Commit add a package as a module</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946027138/VFj4QAzSY.png" alt />Choose “Commit”</p>
<h3 id="heading-add-package-as-a-dependency">Add package as a dependency</h3>
<p>Open demo/mono/repo/app/pubspec.yaml and add my_package, and run pub get.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946028799/Cz0W7c3Fu.png" alt /></p>
<h3 id="heading-commit-add-a-package-as-a-dependency">Commit add a package as a dependency</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946031511/qqUoqEzsJ.png" alt />Choose “Commit”</p>
<h3 id="heading-change-maindart-to-use-package">Change main.dart to use package</h3>
<p>If you change the line _counter++ to the sample here below, the demo app will use the package to add the values. Hot reload also works while you make changes to the implementation in my_package, which is awesome.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946033565/uBjBY1sYx.png" alt /></p>
<h3 id="heading-commit-changes-to-maindart">Commit changes to main.dart</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946036561/eScmZr2Ar.png" alt />Choose “Commit”</p>
<h3 id="heading-done">Done</h3>
<p>These are all the steps that are needed to add a package to the mono-repo.</p>
]]></content:encoded></item><item><title><![CDATA[Add a plugin to a mono repository]]></title><description><![CDATA[In this part, we are going to add a plugin to an existing mono-repo.
Open a terminal and go to the packages folder.
Go to the packages folder
cd ~/repos/0000000-demo_mono_repo_app-rubigo/packages
Create a Flutter plugin
fvm flutter create -t plugin -...]]></description><link>https://yapb.dev/mono-repositories-add-a-plugin-to-a-mono-repository</link><guid isPermaLink="true">https://yapb.dev/mono-repositories-add-a-plugin-to-a-mono-repository</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Android Studio]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sun, 07 Mar 2021 10:40:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/YU-OA2TvQRQ/upload/v1641149919792/sV8shkmdw.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this part, we are going to add a plugin to an existing mono-repo.</p>
<p>Open a terminal and go to the packages folder.</p>
<h3 id="heading-go-to-the-packages-folder">Go to the packages folder</h3>
<pre><code>cd <span class="hljs-operator">~</span><span class="hljs-operator">/</span>repos<span class="hljs-operator">/</span>0000000<span class="hljs-operator">-</span>demo_mono_repo_app<span class="hljs-operator">-</span>rubigo<span class="hljs-operator">/</span>packages
</code></pre><h3 id="heading-create-a-flutter-plugin">Create a Flutter plugin</h3>
<pre><code>fvm flutter create <span class="hljs-operator">-</span>t plugin <span class="hljs-operator">-</span><span class="hljs-operator">-</span>platforms ios,android <span class="hljs-operator">-</span><span class="hljs-operator">-</span>org com.example <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ios<span class="hljs-operator">-</span>language objc <span class="hljs-operator">-</span><span class="hljs-operator">-</span>android<span class="hljs-operator">-</span>language java <span class="hljs-operator">-</span><span class="hljs-operator">-</span>project<span class="hljs-operator">-</span>name my_plugin ./my_plugin
</code></pre><h3 id="heading-openrestart-android-studio">Open/restart Android Studio</h3>
<p>Android studio might not detect the new folder. Restarting Android Studio fixes that.</p>
<h3 id="heading-commit-plugin">Commit plugin</h3>
<p>We will use Android Studio to review and commit files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946042989/jOO8qDyBu.png" alt />Choose Commit</p>
<h3 id="heading-add-plugin-as-a-module">Add plugin as a module</h3>
<p>Android Studio =&gt; File -&gt; Project Structure =&gt; Modules =&gt; + =&gt; Import Module</p>
<p>Navigate to my_plugin folder and choose “Open”. Be aware that the initial folder that Android Studio suggests might not be part of your current project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946045563/PZ-oRNTTc.png" alt />Select “Create module from existing sources”, choose “Next”<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946048390/5_E_3j7-0.png" alt />Deselect all lines, except the one with Flutter, choose “Next”<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946051320/zRz46Wm4s.png" alt />Deselect all, choose “Finish”</p>
<h3 id="heading-commit-add-plugin-as-a-module">Commit add plugin as a module</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946054231/jKc4wTWzZ.png" alt />Choose “Commit”</p>
<h3 id="heading-add-plugin-as-a-dependency">Add plugin as a dependency</h3>
<p>Open demo_mono_repo_app/pubspec.yaml and add my_plugin, and run pub get.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946056112/3wW9HSiHw.png" alt /></p>
<h3 id="heading-commit-add-plugin-as-a-dependency">Commit add plugin as a dependency</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946058744/UyKNUVIPk.png" alt />Choose “Commit”</p>
<h3 id="heading-change-maindart-to-use-plugin">Change main.dart to use plugin</h3>
<p>With some small changes, we can call MyPlugin.platformVersion, which gets information about the current OS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946060367/4oySH7A66L.png" alt /><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946061742/epVITZf0t.png" alt /></p>
<h3 id="heading-commit-changes-to-maindart">Commit changes to main.dart</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946064350/FZbLradVR6.png" alt />Choose “Commit”</p>
<h3 id="heading-done">Done</h3>
<p>These are all the steps that are needed to add a plugin to the mono-repo.</p>
<p>The app final app can be downloaded from:<br /><a target="_blank" href="https://github.com/jsroest/0000000-demo_mono_repo_app-rubigo">https://github.com/jsroest/0000000-demo_mono_repo_app-rubigo</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946067427/WK_ezs1M5.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946069921/bBxadq3g-.png" alt /></p>
]]></content:encoded></item><item><title><![CDATA[Set up a mono repository]]></title><description><![CDATA[Intro
I was intrigued by a blog post from Rémi Rousselet with the title Getting started: Creating your Flutter project. In this post, Rémi does an excellent job in explaining how to set up a Flutter project with the right rules, like enabling strong-...]]></description><link>https://yapb.dev/mono-repositories-set-up-a-mono-repository</link><guid isPermaLink="true">https://yapb.dev/mono-repositories-set-up-a-mono-repository</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Android Studio]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sun, 07 Mar 2021 10:36:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/YU-OA2TvQRQ/upload/v1641129369612/E_K7tuVvh.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-intro">Intro</h3>
<p>I was intrigued by a blog post from <a target="_blank" href="https://twitter.com/remi_rousselet">Rémi Rousselet</a> with the title <a target="_blank" href="https://dash-overflow.net/articles/getting_started/">Getting started: Creating your Flutter project</a>. In this post, Rémi does an excellent job in explaining how to set up a Flutter project with the right rules, like enabling strong-mode, set up the linter, and disable warnings for generated files. A great way to start…. except for one thing, that I could not grasp fully. Rémi advises using the ‘/packages’ folder convention. Although I did understand the idea behind it, I could not find the right workflow to set this up. It always felt a bit like hacking and abusing the IDE, in my case Android Studio, until it worked.</p>
<p>In this blog post, I write about the workflow that I have found to set this up the right way, and with consistent results. ‘Consistent result’ you might think…. what? This is because Android Studio does more than just opening the folder when you open a Flutter project for the first time. It does search and index the whole tree and makes assumptions about how you probably want to use the project. It uses the data and file contents (like .gitignore) in your project folder, to guess the right settings. This means that if you do things in (a slightly) different order, the result is a bit different.</p>
<p>This blog post will help you to set up your Flutter project consistently and logically. You will have Flutter app project, with the possibility to extract your reusable code in separate packages (or plugins). With, of course, the ability to reuse these packages easily in other projects. You could even put these packages later on at pub-dev, and just update the reference in pubspec.yaml to use the published version.</p>
<p>There is also a companion repository on github to automate this process. You can read about it here: <a class="post-section-overview" href="#heading-make-all-these-steps-a-one-liner">Make all these steps a one-liner</a></p>
<p>I use <a target="_blank" href="https://pub.dev/packages/fvm">Flutter Version Management</a> (FVM) by <a target="_blank" href="https://twitter.com/LeoAFarias">Leo Farias</a>. If you have multiple Flutter projects that you have to support, this tool is mandatory. It keeps track of the Flutter version you have used for a specific project and it makes switching between these versions a breeze. You will never have to wait again for downloading the Flutter files when you switch to another project that uses another version of Flutter. It will improve your life as a developer.</p>
<h3 id="heading-fvm">FVM</h3>
<p>Make sure you have <a target="_blank" href="https://pub.dev/packages/fvm">FVM</a> installed and applied the setup of paragraph “Set Global Version”.</p>
<h3 id="heading-create-project-folder">Create project folder</h3>
<pre><code>mkdir 0000000<span class="hljs-operator">-</span>demo_mono_repo_app<span class="hljs-operator">-</span>rubigo
</code></pre><p>I use the following convention for the project folder:</p>
<ul>
<li>0000000<br />The customer id filled up with zeros to seven positions. Companies might change their name, but their id (as used in the ERP or financial system) does not change.</li>
<li>–<br />A separator between customer id and app name.</li>
<li>demo_mono_repo_app<br />This is the name of the app, only lowercase and underscores allowed</li>
<li>–<br />A separator between app name and the company name</li>
<li>rubigo<br />This is the name of the company, this makes it easy to search for a project in a folder</li>
</ul>
<h3 id="heading-go-to-the-project-folder">Go to the project folder</h3>
<pre><code>cd  0000000<span class="hljs-operator">-</span>demo_mono_repo_app<span class="hljs-operator">-</span>rubigo
</code></pre><h3 id="heading-initialize-git">Initialize git</h3>
<pre><code>git <span class="hljs-keyword">init</span>
</code></pre><p><strong>Caveat 1</strong><br />You can also enable git via Android Studio:<br />Menu =&gt; VCS =&gt; Enable Version Control Integration…</p>
<p>But this does create a .git folder inside the Flutter project. This is not what we want. The goal is to have a mono-repo, which means multiple packages in one git repository. That’s why we have to do the git init in the top project folder</p>
<h3 id="heading-create-packages-folder">Create packages folder</h3>
<pre><code><span class="hljs-keyword">mkdir</span> packages
</code></pre><h3 id="heading-change-to-packages-folder">Change to packages folder</h3>
<pre><code> <span class="hljs-built_in">cd</span> packages
</code></pre><h3 id="heading-create-app-project">Create app project</h3>
<pre><code>fvm flutter create <span class="hljs-operator">-</span><span class="hljs-operator">-</span>org rubigo <span class="hljs-operator">-</span><span class="hljs-operator">-</span>project<span class="hljs-operator">-</span>name demo_mono_repo_app <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ios<span class="hljs-operator">-</span>language objc <span class="hljs-operator">-</span><span class="hljs-operator">-</span>android<span class="hljs-operator">-</span>language java app
</code></pre><ul>
<li><strong>fvm</strong><br />This is the Flutter Version Management program.</li>
<li><strong>flutter</strong><br />Because of FVM’s “Set Global Version”, the selected Flutter version will be used to execute the command.</li>
<li><strong>create</strong><br />This instructs Flutter to create a new app project</li>
<li><strong>–org com.rubigo</strong><br />The company to use. This is used (with the project name) for Android’s application id and iOS’s product bundle identifier.</li>
<li><strong>–project-name demo_mono_repo_app</strong><br />This is the name of the app, only lowercase and underscores are allowed. I keep this identical to the name of the app in the project folder name.</li>
<li><strong>–ios-language objc</strong><br />It’s my preference to use objective c as the language for the iOS runner. Choose as you like.</li>
<li><strong>–android-language java</strong><br />It’s my preference to use java as the language for the Android runner. Choose as you like.</li>
<li><strong>app</strong><br />This is the name of the folder for the app project that is created inside the packages folder. If it’s always ‘app’ across projects then it’s always easy to find.</li>
</ul>
<h3 id="heading-change-to-app-folder">Change to app folder</h3>
<pre><code><span class="hljs-built_in">cd</span> app
</code></pre><h3 id="heading-commit-gitignore">Commit .gitignore</h3>
<pre><code>git add .gitignore
git commit <span class="hljs-operator">-</span>m <span class="hljs-string">"[app] Initial commit"</span>
</code></pre><p>Because we will edit the gitignore file, we will commit it first to have a complete history correct.</p>
<p>Change .gitignore for IntelliJ IDEA</p>
<pre><code># IntelliJ related
-*.iml
-*.ipr
-*.iws
-.idea/
+.idea<span class="hljs-comment">/**/</span>workspace.xml
+.idea<span class="hljs-comment">/**/</span>tasks.xml
+.idea<span class="hljs-comment">/**/</span><span class="hljs-keyword">usage</span>.<span class="hljs-keyword">statistics</span>.xml
+.idea<span class="hljs-comment">/**/</span>dictionaries
+.idea<span class="hljs-comment">/**/</span>shelf
+.idea<span class="hljs-comment">/**/</span>libraries
</code></pre><p>The new .gitignore allows sharing of modules and run configurations between developers.</p>
<h3 id="heading-commit-gitignore">Commit .gitignore</h3>
<pre><code><span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">add</span> <span class="hljs-selector-class">.gitignore</span>
<span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">commit</span> <span class="hljs-selector-tag">-m</span> "<span class="hljs-selector-attr">[app]</span> <span class="hljs-selector-tag">Change</span> <span class="hljs-selector-class">.gitignore</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">include</span> <span class="hljs-selector-tag">important</span> <span class="hljs-selector-class">.idea</span> <span class="hljs-selector-tag">files</span>"
</code></pre><p>The changes are committed.</p>
<h3 id="heading-commit-flutter-project">Commit Flutter project</h3>
<pre><code>git <span class="hljs-keyword">add</span> -A
git <span class="hljs-keyword">commit</span> -m "[app] Initial flutter project"
</code></pre><p>Now the freshly created Flutter project is committed.</p>
<h3 id="heading-init-fvm-for-project">Init fvm for project</h3>
<pre><code><span class="hljs-attribute">fvm</span> use <span class="hljs-number">1</span>.<span class="hljs-number">22</span>.<span class="hljs-number">6</span>
</code></pre><p>This adds the .fvm folder to the project. This folder contains a file with the flutter version to use and a symlink to the selected Flutter version.</p>
<h3 id="heading-change-gitignore-for-fvm">Change .gitignore for FVM</h3>
<pre><code><span class="hljs-comment"># FVM related</span>
/.fvm/flutter_sdk
</code></pre><p>These lines are needed to explicitly exclude the flutter_sdk folder from git. Otherwise, you have the risk of adding a complete Flutter SDK to your repository.</p>
<h3 id="heading-change-demomonorepoappiml">Change demo_mono_repo_app.iml</h3>
<pre><code><span class="hljs-operator">&lt;</span>content url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$"</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>sourceFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/lib"</span> isTestSource<span class="hljs-operator">=</span><span class="hljs-string">"false"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>sourceFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/test"</span> isTestSource<span class="hljs-operator">=</span><span class="hljs-string">"true"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>excludeFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/.dart_tool"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>excludeFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/.fvm/flutter_sdk"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>excludeFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/.idea"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>excludeFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/.pub"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
 <span class="hljs-operator">&lt;</span>excludeFolder url<span class="hljs-operator">=</span><span class="hljs-string">"file://$MODULE_DIR$/build"</span><span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>content<span class="hljs-operator">&gt;</span>
</code></pre><p>This excludes the flutter_sdk path in Android Studio. Otherwise, search results will also include results in the Flutter SDK.</p>
<p><strong>Caveat 2</strong></p>
<p>If you try to add this line via the Android Studio menus File=&gt;Project Structure=&gt;Modules, Android Studio will bite you. During startup, all folders are indexed, including the .fvm/flutter_sdk folder. You will end up with countless excluded folders from the SDK folder in the demo_mono_repo_app.iml file.</p>
<h3 id="heading-commit-fvm-changes">Commit FVM changes</h3>
<pre><code>git <span class="hljs-keyword">add</span> -A
git <span class="hljs-keyword">commit</span> -m "[app] Init FVM 1.22.6"
</code></pre><h3 id="heading-remove-androidiml-from-modulesxml">Remove android.iml from modules.xml</h3>
<pre><code><span class="hljs-operator">&lt;</span>modules<span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">&lt;</span>module
  fileurl<span class="hljs-operator">=</span><span class="hljs-string">"file://$PROJECT_DIR$/demo_mono_repo_app.iml"</span>
  filepath<span class="hljs-operator">=</span><span class="hljs-string">"$PROJECT_DIR$/demo_mono_repo_app.iml"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span> <span class="hljs-operator">&lt;</span>module
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span>  fileurl<span class="hljs-operator">=</span><span class="hljs-string">"file://$PROJECT_DIR$/android/demo_mono_repo_app_android.iml"</span>
  <span class="hljs-operator">-</span><span class="hljs-operator">-</span>  filepath<span class="hljs-operator">=</span><span class="hljs-string">"$PROJECT_DIR$/android/demo_mono_repo_app_android.iml"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>modules<span class="hljs-operator">&gt;</span>
</code></pre><p>Remove the demo_mono_repo_app_android.iml module in the file .idea/modules.xml. It is not needed.</p>
<h3 id="heading-commit-modulesxml">Commit modules.xml</h3>
<pre><code>git <span class="hljs-keyword">add</span> -A
git <span class="hljs-keyword">commit</span> -m "[app] Remove demo_mono_repo_app_android.iml from modules.xml"
</code></pre><h3 id="heading-mono-repo-setup-is-finished">Mono repo setup is finished</h3>
<p>This is it, the flutter mono repo is successfully set up.</p>
<h3 id="heading-make-all-these-steps-a-one-liner">Make all these steps a one-liner</h3>
<p>If you want to make these steps above a one-liner you can use this dart cli project: <a target="_blank" href="https://github.com/jsroest/create_flutter_mono_repo">https://github.com/jsroest/create_flutter_mono_repo</a></p>
<pre><code>dart create_flutter_mono_repo.dart <span class="hljs-operator">-</span><span class="hljs-operator">-</span>root<span class="hljs-operator">-</span>path <span class="hljs-operator">/</span><span class="hljs-operator">&lt;</span>your repos folder<span class="hljs-operator">&gt;</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span>customer<span class="hljs-operator">-</span>id 0000000 <span class="hljs-operator">-</span><span class="hljs-operator">-</span>project<span class="hljs-operator">-</span>name demo_mono_repo_app <span class="hljs-operator">-</span><span class="hljs-operator">-</span>customer<span class="hljs-operator">-</span>name rubigo <span class="hljs-operator">-</span><span class="hljs-operator">-</span>org com.example <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ios<span class="hljs-operator">-</span>language objc <span class="hljs-operator">-</span><span class="hljs-operator">-</span>android<span class="hljs-operator">-</span>language java <span class="hljs-operator">-</span><span class="hljs-operator">-</span>flutter<span class="hljs-operator">-</span>version <span class="hljs-number">1.22</span><span class="hljs-number">.6</span>
</code></pre><h3 id="heading-always-open-the-app-folder">Always open the app folder</h3>
<p>Remember to always open the app folder in Android Studio if you want to work on your Flutter app or one of the local packages that you will add later. The app folder will contain links to the other packages so that you can edit all your Flutter code from one place.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946074581/nULpzyKk8.png" alt /></p>
]]></content:encoded></item><item><title><![CDATA[Demo Proof of delivery]]></title><description><![CDATA[TL;DR;
This year (2020) we delivered our first Line Of Business app that's built with Flutter to one of our customers.
The launch of the app was a success. The app went live without issues and the app was accepted by the users with ease.
We thought t...]]></description><link>https://yapb.dev/demo-proof-of-delivery</link><guid isPermaLink="true">https://yapb.dev/demo-proof-of-delivery</guid><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Mon, 27 Jul 2020 12:40:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/WOn90Iui_08/upload/v1641121918924/dMhC7vAXo.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-tldr">TL;DR;</h3>
<p>This year (2020) we delivered our first Line Of Business app that's built with Flutter to one of our customers.</p>
<p>The launch of the app was a success. The app went live without issues and the app was accepted by the users with ease.</p>
<p>We thought this app is a great way to demonstrate our app craftmanship of Line of Business Apps to a broader audience. That is why we decided to put a demo version of this app in the play store, app store, and on the web.</p>
<p>You can download the app for your device, or even try it out in your browser.</p>
<p><a target="_blank" href="https://apps.apple.com/us/app/demo-proof-of-delivery/id1512951904?mt=8"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946095832/i9nkuN2_c.png" alt /></a></p>
<p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.dalosy.demo_proof_of_delivery"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946097077/3yyRLGlSq.png" alt /></a></p>
<p><a target="_blank" href="https://dalosy-projecten-bv.github.io/demo-proof-of-delivery-pwa/#/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946098358/kqzAw9wpt.png" alt /></a></p>
<p><a target="_blank" href="https://jsroest.github.io/yapb_assets/202007_demo_proof_of_delivery/barcodes.pdf">Sample barcode sheet (pdf)</a></p>
<h3 id="heading-the-creation-of-the-demo-app">The creation of the demo app</h3>
<p>Although the customer was using the app successfully in production, it was not ready yet to serve as a demo app.</p>
<p>We needed to make the ‘demo’ experience better:</p>
<ul>
<li><a class="post-section-overview" href="#heading-replace-customers-logo-and-icons">Replace customer’s logos and icons</a></li>
<li><a class="post-section-overview" href="#heading-add-ios-and-web-as-a-target">Add iOS and Web as a target</a></li>
<li><a class="post-section-overview" href="#heading-internationalize-the-app">Internationalize the app</a></li>
<li><a class="post-section-overview" href="#heading-use-the-camera-as-a-barcode-scanner">Use the camera as a barcode scanner</a></li>
<li><a class="post-section-overview" href="#heading-mock-the-communication-with-the-backend">Mock the communication with the backend</a></li>
<li><a class="post-section-overview" href="#heading-final-words-and-screenshots">Final words and screenshots</a></li>
</ul>
<h3 id="heading-replace-customers-logo-and-icons">Replace customer’s logo and icons</h3>
<p>This was not the most fun part. Normally these assets are delivered to us by a designer, but now I had to figure it out by myself (again). Nothing too hard, but tedious as I remembered how it was in the past.</p>
<h3 id="heading-add-ios-and-web-as-a-target">Add iOS and Web as a target</h3>
<p>Now the fun started. Support for iOS was already there, or at least for the most part. Adding Web support was as easy as switching to Flutter’s beta branch.</p>
<p>The only major part that had to be rewritten for the web was the datastore. We like to have the power of SQL in our line of business apps, but SQL support on the web is just not there, or at least not in the way we like it. Because this application is not too difficult, we reworked the datastore from SQLite tables to hive tables. This took roughly 2 days but was straightforward and easy.</p>
<h3 id="heading-internationalize-the-app">Internationalize the app</h3>
<p>The original app was only in Dutch. To internationalize the app we used <a target="_blank" href="https://localizely.com/">localizely</a>. Localizely is an ide plugin that uses arb files for input and generates code that follows the same pattern to localize apps as the rest of the Flutter framework. Any literal string in the code can be easily extracted to the arb files by pressing ALT-ENTER.</p>
<p>After that, I created a Google sheet where I could import the English arb file (which is a JSON formatted file) and machine translate the texts to other languages. With the help of some Google sheet plugins, the result can be extracted as arb/JSON files again. This was a bit sensitive to errors, but it got the job done. For larger projects, I would recommend a more professional workflow than Google sheets.</p>
<h3 id="heading-use-the-camera-as-a-barcode-scanner">Use the camera as a barcode scanner</h3>
<p>The original app was designed for a <a target="_blank" href="https://www.zebra.com/us/en/products/mobile-computers/handheld/tc52-tc57-series-touch-computer.html">Zebra TC57</a> touch computer.</p>
<p>This is a ruggedized Android device with a dedicated barcode scanner. The barcode scanner of this device performs excellently.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946099628/HOAbLRo6A.jpeg" alt /></p>
<p>On other devices, we wanted to use the camera as a fallback option. This is where things got a bit complicated.</p>
<p>The opensource ZXING barcode scan library is abandoned. Any plugin based on that is not future-proof.</p>
<p>Commercial libraries are performing very well, but are very expensive and have a subscription model. You will have to pay for the number of users per year. This is far from ideal for a demo app.</p>
<p>We ended up implementing the barcode recognition with Google’s ML-Kit. At this time ML-Kit is still in beta, but there is a working Flutter sample from the firebase team. We used this sample as a starting point.</p>
<p>On Zebra devices, we normally enable ‘picklist mode’. When in picklist mode, the crosshairs projected on the target (red laser-lines) determine the barcode that is scanned. This makes it easier to scan the right barcode when more barcodes are visible at the same time. For example when barcodes are printed on a sheet of paper.</p>
<p>To make it easier for the user to scan the right barcode with the camera, the user can suspend the recognition by touching the screen You can then position the barcode under the square. When you release your finger, the barcode recognition is active and barcodes are scanned. A green square indicates that the recognition is suspended, a red square indicates that the recognition is active.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946101395/DCd4JMvjri.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946103321/aMp-CugE4.png" alt /></p>
<p>Because every type of phone has different camera characteristics like resolution and autofocus, the barcode scanning performance can be tweaked in the settings of the app.</p>
<p>I think that iPhone users will be most satisfied with the barcode scanning performance.</p>
<h3 id="heading-mock-the-communication-with-the-backend">Mock the communication with the backend</h3>
<p>The production app communicates with a backend to exchange data. Of course, the demo app has to be able to function without a backend. Therefore there is a setting ‘Demo mode’ in the app. For the demo app, this setting is enabled by default. When enabled, the communication functions inside the app are replaced with default reasonable responses. That’s why you can test the app without a backend.</p>
<h3 id="heading-final-words-and-screenshots">Final words and screenshots</h3>
<p>We had a really good time, building this app in Flutter. I had great plans of explaining every detail of it on this blog. Unfortunately, COVID-19 made this impossible. Thankfully not because of illness, but because of lack of time.</p>
<p>Thank you all for reading and trying out the demo.</p>
<p>For questions about the app, you can contact <a target="_blank" href="mailto:jsroest@gmail.com">me</a>.<br />If you want a quotation for your Line Of Business app, contact us <a target="_blank" href="mailto:development@dalosy.com">here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946078940/AzF8d3XJP.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946080484/bfyyFx63E.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946082158/_XInLWZEA.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946083836/94pU9weNR.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946085507/X_dOlYV0H.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946087255/pop9AkuJh.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946089042/3PHdM3Tz4.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946091054/Mqv2a9TPj.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946092712/jmstNXKKd.png" alt />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946094406/HdMzkbjqv.png" alt /></p>
]]></content:encoded></item><item><title><![CDATA[Logging]]></title><description><![CDATA[The importance and usage of logging is for Line Of Business apps different than for consumer apps.
Consumer apps

Collect application crashes/warnings
Narrow down problems to specific manufacturers, hardware, os versions
Get insights on how much the ...]]></description><link>https://yapb.dev/logging</link><guid isPermaLink="true">https://yapb.dev/logging</guid><category><![CDATA[Flutter]]></category><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 04 Mar 2020 10:49:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640720499659/KzcwsN_CG.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The importance and usage of logging is for Line Of Business apps different than for consumer apps.</p>
<h4 id="heading-consumer-apps">Consumer apps</h4>
<ul>
<li>Collect application crashes/warnings</li>
<li>Narrow down problems to specific manufacturers, hardware, os versions</li>
<li>Get insights on how much the app is installed/used across the globe</li>
<li>Get insights on how the app is used</li>
<li>Get insights on how the monetization of the app can be improved</li>
</ul>
<h4 id="heading-line-of-business-apps">Line Of Business apps</h4>
<ul>
<li>Collect application crashes/warnings</li>
<li>Narrow down issues with backend services</li>
<li>Narrow down issues reported by individual users</li>
<li>Enterprises do not like Line Of Business apps that leak info to Google, Apple, or other Application Monitoring or Tracking Software</li>
</ul>
<p>On the other hand, logging for Line Of Business apps is quite simple:</p>
<ul>
<li>Logging is written to local persistent storage (text file or database)</li>
<li>Add logging to strategic points in the app, so you can follow the usage of the app</li>
<li>Each log line has a timestamp, loglevel, message and stacktrace (if available)</li>
<li>When the max amount of logging is reached, delete old logging</li>
<li>When a problem is reported, download the logging with an EMM and analyze it</li>
</ul>
<h4 id="heading-async-challenges">Async challenges</h4>
<p>The async nature of Flutter does make logging a bit more complicated than it was in the synchronous world.</p>
<p>We want to log things as soon as the application starts, but to store the logging, we are dependant on an async filesystem/database that needs time to initialize.</p>
<p>To be able to log events before it can be written, I split the logging service into two parts:</p>
<ul>
<li>Logging service</li>
<li>Logging service file (persistent part)</li>
</ul>
<p>The logging service is the first service that is started. This means that all other services can log messages. As long as the ‘Logging service file’ is not started, the ‘Logging service’ keeps the logging in memory.</p>
<p>As soon as the ‘Logging service file’ is started, it registers its ‘writer’ function in the Logging service. As soon as this is registered, the cached logging is purged to this ‘writer’ function. From now on, the ‘logging service’ uses this ‘writer’ function directly instead of its memory cache.</p>
<p>Initialization of the LoggingServiceFile and registering its writer function:</p>
<pre><code>  Future<span class="hljs-operator">&lt;</span>void<span class="hljs-operator">&gt;</span> onStart() async {
    await Provider.of&lt;VersionService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;SoundService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;ConfigurationBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;ConfigurationLoader<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;LoggingServiceFile<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    Provider.of&lt;LoggingServiceFile<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).connectLogWriter();
    await Provider.of&lt;DatawedgeService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Navigator.pushReplacement(context, TimeRegistrationPage.Route);
  }
</code></pre><p>The implementation of LoggingService:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-string">'package:enum_to_string/enum_to_string.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;

<span class="hljs-keyword">enum</span> <span class="hljs-title">LogLevel</span> {
  debug,
  information,
  warning,
  <span class="hljs-function"><span class="hljs-keyword">error</span>,
  <span class="hljs-title">critical</span>,
}

<span class="hljs-title">class</span> <span class="hljs-title">LoggingService</span> </span>{
  LoggingService() {
    _start();
  }

  final <span class="hljs-keyword">int</span> _maxCacheRows <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;

  <span class="hljs-keyword">var</span> _cacheLog <span class="hljs-operator">=</span> List<span class="hljs-operator">&lt;</span>Logging<span class="hljs-operator">&gt;</span>();

  void _start() {
    log(LogLevel.information, <span class="hljs-string">"$runtimeType started"</span>);
  }

  void Function(Logging logging) _writer;

  void setWriter(void Function(Logging logging) writer) {
    _writer <span class="hljs-operator">=</span> writer;
    _cacheLog.forEach((a) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> _writer(a));
  }

  void log(LogLevel logLevel, String message, {String stackTrace <span class="hljs-operator">=</span> <span class="hljs-string">''</span>}) {
    <span class="hljs-keyword">var</span> logging <span class="hljs-operator">=</span> Logging(
      id: null,
      timestamp: DateTime.now().toUtc().toIso8601String(),
      logLevel: logLevel,
      message: message,
      stacktrace: stackTrace,
    );
    debugPrint(logging.toString());
    <span class="hljs-keyword">if</span> (_writer <span class="hljs-operator">=</span><span class="hljs-operator">=</span> null) {
      _cacheLog.add(logging);
      <span class="hljs-keyword">if</span> (_cacheLog.<span class="hljs-built_in">length</span> <span class="hljs-operator">&gt;</span> _maxCacheRows) {
        _cacheLog.removeRange(<span class="hljs-number">0</span>, _cacheLog.<span class="hljs-built_in">length</span> <span class="hljs-operator">-</span> _maxCacheRows);
      }
    } <span class="hljs-keyword">else</span> {
      _writer(logging);
    }
  }
}

class Logging {
  Logging({
    <span class="hljs-built_in">this</span>.id,
    <span class="hljs-built_in">this</span>.timestamp,
    <span class="hljs-built_in">this</span>.logLevel,
    <span class="hljs-built_in">this</span>.message,
    <span class="hljs-built_in">this</span>.stacktrace <span class="hljs-operator">=</span> <span class="hljs-string">''</span>,
  });

  final <span class="hljs-keyword">int</span> id;
  final String timestamp;
  final LogLevel logLevel;
  final String message;
  final String stacktrace;

  @<span class="hljs-keyword">override</span>
  String toString() {
    <span class="hljs-keyword">var</span> result <span class="hljs-operator">=</span> List<span class="hljs-operator">&lt;</span>String<span class="hljs-operator">&gt;</span>();
    result.add(
        <span class="hljs-string">'[LOGGING] ${EnumToString.parse(logLevel)} $timestamp Message: $message'</span>);
    <span class="hljs-keyword">if</span> (stacktrace.isNotEmpty) {
      result.add(<span class="hljs-string">'[LOGGING StackTrace: $stacktrace]'</span>);
    }
    <span class="hljs-keyword">return</span> result.join(<span class="hljs-string">'\n'</span>);
  }
}
</code></pre><p>The implementation of LoggingServiceFile:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/cupertino.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:intl/intl.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:app/services/hive/configuration_box_service.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:app/services/logging_service.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path_provider/path_provider.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path/path.dart'</span>;

const String externalLogFilename <span class="hljs-operator">=</span> <span class="hljs-string">"cc600_log.txt"</span>;
const String externalBackupLogFilename <span class="hljs-operator">=</span> <span class="hljs-string">"cc600_log_1.txt"</span>;

class LoggingServiceFile {
  LoggingServiceFile(<span class="hljs-built_in">this</span>._log, <span class="hljs-built_in">this</span>._configurationBoxService) {
    _start();
  }

  final LoggingService _log;
  final ConfigurationBoxService _configurationBoxService;

  File _logFile;
  String _logFilename;
  File _backupLogFile;
  String _backupLogFilename;

  void _start() {
    _log.log(LogLevel.information, <span class="hljs-string">"$runtimeType started"</span>);
  }

  Future<span class="hljs-operator">&lt;</span>void<span class="hljs-operator">&gt;</span> init() async {
    <span class="hljs-keyword">var</span> directory <span class="hljs-operator">=</span> await getExternalStorageDirectory();
    _log.log(LogLevel.information, <span class="hljs-string">'External path reported: ${directory.path}'</span>);
    _logFilename <span class="hljs-operator">=</span> join(directory.path, externalLogFilename);
    _logFile <span class="hljs-operator">=</span> File(_logFilename);
    _backupLogFilename <span class="hljs-operator">=</span> join(directory.path, externalBackupLogFilename);
    _backupLogFile <span class="hljs-operator">=</span> File(_backupLogFilename);
  }

  void connectLogWriter() {
    _log.setWriter(_writer);
  }

  void _writer(Logging logging) {
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>_configurationBoxService.logging) {
      <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">if</span> (_logFile.existsSync() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> _logFile.lengthSync() <span class="hljs-operator">&gt;</span> <span class="hljs-number">1000000</span>) {
        <span class="hljs-keyword">if</span> (_backupLogFile.existsSync()) {
          _backupLogFile.deleteSync();
        }
        _backupLogFile <span class="hljs-operator">=</span> _logFile.renameSync(_backupLogFilename);
        _logFile <span class="hljs-operator">=</span> File(_logFilename);
      }
      _logFile.writeAsStringSync(
        <span class="hljs-string">'${DateFormat('</span>dd<span class="hljs-operator">-</span>MM<span class="hljs-operator">-</span>yyyy HH:mm:ss<span class="hljs-string">').format(DateTime.now())} ${logging.message}\r\n'</span>,
        mode: FileMode.append,
        flush: <span class="hljs-literal">true</span>,
      );
    } <span class="hljs-keyword">catch</span> (e) {
      debugPrint(<span class="hljs-string">'error while writing to log ${e.toString()}'</span>);
    }
  }
}
</code></pre><p>Note that the LoggingServiceFile could be easily exchanged with a LoggingServiceDatabase. Only the signature of the writer function has to be the same.</p>
]]></content:encoded></item><item><title><![CDATA[Async initialization of services during startup]]></title><description><![CDATA[Startup phases:

Bootstrapping the native platform code
Bootstrapping the Flutter code
Normal application state where user interaction is allowed

Bootstrapping the native platform code
The first thing that happens when a Flutter app starts, is the b...]]></description><link>https://yapb.dev/line-of-business-apps-async-initialization-of-services-during-startup</link><guid isPermaLink="true">https://yapb.dev/line-of-business-apps-async-initialization-of-services-during-startup</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 04 Mar 2020 09:36:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640719979584/kLYAfozUN.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Startup phases:</p>
<ol>
<li>Bootstrapping the native platform code</li>
<li>Bootstrapping the Flutter code</li>
<li>Normal application state where user interaction is allowed</li>
</ol>
<h4 id="heading-bootstrapping-the-native-platform-code">Bootstrapping the native platform code</h4>
<p>The first thing that happens when a Flutter app starts, is the bootstrapping of the native platform code. This piece is responsible for showing the splash screen and creating a container where the Flutter app can live. As soon as everything is ready, the main function in the /lib folder is executed. This main function is responsible for bootstrapping the Flutter code.</p>
<h4 id="heading-bootstrapping-the-flutter-code">Bootstrapping the Flutter code</h4>
<p>The bootstrapping of the Flutter code is where things might get complicated due to the async nature of Flutter and the usage of Provider to provide services and state. The reason for this is:</p>
<ol>
<li>When using Provider for services and state, these services and state are part of the widget tree.</li>
<li>Some services are initialized asynchronously. For example Sqflite and all other services that are using method channels to access the underlying platform.</li>
<li>The widget tree must build to have a user interface</li>
<li>The building of the user interface depends on services and state defined in step 1</li>
</ol>
<p>To complicate things even further, some services are dependant on other services and the order of initialization of the services might matter.</p>
<p>One might try to initialize all the services before <code>runApp(MyApp())</code> is called.</p>
<pre><code><span class="hljs-function">Future&lt;<span class="hljs-keyword">void</span>&gt; <span class="hljs-title">main</span>(<span class="hljs-params"></span>) <span class="hljs-keyword">async</span></span> {
   <span class="hljs-keyword">await</span> initAllServicesAndState();
   runApp(MyApp());
}
</code></pre><p>This has some major downsides:</p>
<ul>
<li>There is no widget tree yet, getting the services into the widget tree from this point is hacky.</li>
<li>If something fails during initialization and you want the user to decide how to recover, there is no easy way to show some UI.</li>
</ul>
<p>Another approach is to initially build a user interface that is not dependant on any services or state. The only responsibility of this page is to initialize things. In my project, this is called AppStartPage.</p>
<h4 id="heading-appstartpage">AppStartPage</h4>
<p>I decided to take the following approach:</p>
<ol>
<li>Let Provider create the services and states</li>
<li>Set AppStartPage as ‘home’ in the MaterialApp</li>
<li>Let AppStartPage initialize the services</li>
<li>When initialization is done, replace the AppStartPage with the HomePage of your app</li>
</ol>
<pre><code>class AppStartPage extends StatefulWidget {
  @<span class="hljs-keyword">override</span>
  \_AppStartPageState createState() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> \_AppStartPageState();
}

class \_AppStartPageState extends State<span class="hljs-operator">&lt;</span>AppStartPage<span class="hljs-operator">&gt;</span> {
  @<span class="hljs-keyword">override</span>
  void initState() {
    <span class="hljs-built_in">super</span>.initState();
    onStart();
  }

  Future<span class="hljs-operator">&lt;</span>void<span class="hljs-operator">&gt;</span> onStart() async {
    await Provider.of&lt;VersionService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;SoundService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;ConfigurationBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;ConfigurationLoader<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Provider.of&lt;LoggingServiceFile<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    Provider.of&lt;LoggingServiceFile<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).connectLogWriter();
    await Provider.of&lt;DatawedgeService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>).init();
    await Navigator.pushReplacement(context, HomePage.Route);
  }

  @<span class="hljs-keyword">override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Container();
  }
}
</code></pre><p>In this sample, the AppStartPage is an empty container. In a real app, you might want to show a page that informs the user what is going on, like ‘Loading….’.</p>
<p>A downside is that the app now has two splash screens. The first is coming from the Android/iOS project. The second one is AppStartPage.</p>
<p>But the advantages of an AppStartPage are obvious:</p>
<ol>
<li>Everything is initialized properly and in the right order when the app starts</li>
<li>No user interaction is possible before the initialization is done, so no-undefined state.</li>
<li>When there is a problem during initialization, you can ask the user in a pop-up what to do.</li>
<li>It is super simple….</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Local datastore, SQLite]]></title><description><![CDATA[History
At the time I was programming apps for Windows CE and Windows Mobile, we used SQL CE. It integrated nicely with the Visual Studio tooling at that time and it was easy to work with. The downside of SQL CE was the likelihood of database corrupt...]]></description><link>https://yapb.dev/line-of-business-apps-local-datastore-sqlite</link><guid isPermaLink="true">https://yapb.dev/line-of-business-apps-local-datastore-sqlite</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 26 Feb 2020 09:25:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640718812772/9laJlg5kN.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4 id="heading-history">History</h4>
<p>At the time I was programming apps for Windows CE and Windows Mobile, we used SQL CE. It integrated nicely with the Visual Studio tooling at that time and it was easy to work with. The downside of SQL CE was the likelihood of database corruption. The corruption could occur if the battery was pulled, forced reboot, or if the SD card was ejected. On some hardware, the SD Card was ejected (in software) if the device went to sleep. So this was a nightmare.</p>
<p>Can you imagine what happens to customer satisfaction when he loses a whole day of work?</p>
<p>When looking for alternatives, I found SQLite. There was no compiled executable for Windows CE at that time, but I found a source, ready for compilation to Windows CE.</p>
<p>SQLite has proved its value since that day, more than 10 years ago. I never encountered a corrupt database anymore.</p>
<h4 id="heading-why-a-local-sql-datastore">Why a local SQL datastore</h4>
<p>In today’s connected world, one might ask why there is a need for a local SQL datastore on the device. There is a difference between the requirements of a consumer app and a Line of Business app.</p>
<ul>
<li><a target="_blank" href="https://yapb.dev/#network-connectivity">Network connectivity</a></li>
<li><a target="_blank" href="https://yapb.dev/#batch-applications">Batch applications</a></li>
<li><a target="_blank" href="https://yapb.dev/#speed">Speed</a></li>
<li><a target="_blank" href="https://yapb.dev/#large-datasets">Large datasets</a></li>
<li><a target="_blank" href="https://yapb.dev/#complex-queries">Complex queries</a></li>
<li><a target="_blank" href="https://yapb.dev/#data-integrity">Data integrity</a></li>
<li><a target="_blank" href="https://yapb.dev/#database-versioning">Database versioning</a></li>
<li><a target="_blank" href="https://yapb.dev/#downsides">Downsides</a></li>
</ul>
<h4 id="heading-network-connectivity">Network connectivity</h4>
<p>The work that needs to be done with Line of Business apps is not always at places with a fast or even present network connection. Think of basements, old warehouses, container yards or the countryside. A company wants to use the app reliably at 100% of the locations. Therefore, a lot of Line of Business apps are batch applications.</p>
<h4 id="heading-batch-applications">Batch applications</h4>
<p>Batch applications work in cycles. A sample of a cycle is here below:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>#</td><td>Network available</td><td>Action</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>yes</td><td>At company: download master data</td></tr>
<tr>
<td>2</td><td></td><td>Perform work (user collects data)</td></tr>
<tr>
<td>3</td><td>maybe</td><td>If connection detected, upload collected data</td></tr>
<tr>
<td>4</td><td></td><td>Goto 2 until finished</td></tr>
<tr>
<td>5</td><td>yes</td><td>At company: upload collected data</td></tr>
</tbody>
</table>
</div><h4 id="heading-speed">Speed</h4>
<p>With manual data entry, the speed of a lookup is not very important. If it took a user 8 seconds to type a number, it does not matter much if checking the number by the app takes 2 seconds.</p>
<p>But most Line of Business apps that I write, run on hardware with dedicated barcode scanners. Scanning a barcode is almost instant. Waiting 2 seconds for the app to respond is not acceptable in this use case.</p>
<p>Looking up data in an SQLite database is very fast and generally a matter of milliseconds. Even a million rows of master data is accessed without a noticeable delay.</p>
<p>This kind of speed is needed when an employee scans items in a retail shop to order at their supplier.</p>
<h4 id="heading-large-datasets">Large datasets</h4>
<p>The consequence of a batch application is that you have to download all the needed master data to the device. Sometimes this is just a few lines, but in some cases, it might be a million rows of data.</p>
<p>An approach is to download the dataset as a zip file. A zip file has the following advantages:</p>
<ul>
<li><strong>Size of the transfer</strong><br />Improves speed over slower network connections</li>
<li><strong>Detect data corruption</strong><br />When the zip files extract successfully, you have a high degree of certainty that the data is complete and not corrupted</li>
<li><strong>Have a coherent dataset</strong><br />With a zip file, you can download several files at once and be certain to have a coherent dataset.</li>
</ul>
<p>It may take a minute to (batch) insert large datasets into an SQLite database. Normally this is not an issue, as it only happens once a day. You can also automate this process in software by starting the download automatically before the workers start their day.</p>
<h4 id="heading-complex-queries">Complex queries</h4>
<p>A big advantage of using an SQLite datastore is that you can run complex queries against it.</p>
<p>An application might start simple with a few tables, relations, and statuses. But as the customer explores the power and benefits of the app in their logistical process, it is almost certain that the customer will ask for more functionality. With the power of SQL and SQLite, you can be certain that there will be no limitation on the complexity of the queries.</p>
<h4 id="heading-data-integrity">Data integrity</h4>
<p>Data integrity is as important as the integrity of the source code of the app. If the data is corrupted, the program might get in a loop, behave unexpectedly or even crash your application. The size of the database might also get out of control.</p>
<p>A good data model with the right (composite) primary keys and foreign keys will help you to keep your data valid.</p>
<h4 id="heading-database-versioning">Database versioning</h4>
<p>As the requirements of the customer changes, it will also influence your data model. It is good to know that SQLite can help you to upgrade and/or downgrade the version of your data model.</p>
<p>With the onUpgrade and onDowngrade methods, you can update or downgrade the data model. Although this is something that you have to write manually, you can take measures that the data in the tables are transformed in the right way.</p>
<h4 id="heading-downsides">Downsides</h4>
<p>Of course, there are also downsides to a local SQLite datastore:</p>
<ul>
<li><strong>Skills needed</strong><br />You need to know how to design and query SQL databases. This can be a complex and challenging subject and can be done wrong in more than a thousand ways.</li>
<li><strong>Async data access</strong><br />Every call to the database is asynchronously. Be prepared that when you navigate to a page, the data will always come later. This will make your application more complex.</li>
<li><strong>No web support</strong><br />There is no direct SQLite support for the web at this time. There are some proof of concepts, like <a target="_blank" href="https://moor.simonbinder.eu/">Moor</a> which might be useful for demo purposes. But I don’t expect that serializing the database after each update does anything good for performance and reliability.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[State management with provider]]></title><description><![CDATA[History
When I started exploring Flutter, there was (and there still is) a lot of discussion about state management. At that time, Google promoted Inherited Widget as the way to propagate information down the widget tree. I tried Inherited Widget, an...]]></description><link>https://yapb.dev/line-of-business-apps-state-management-with-provider</link><guid isPermaLink="true">https://yapb.dev/line-of-business-apps-state-management-with-provider</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Sun, 23 Feb 2020 13:44:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640635332751/Ty8ETUCjz.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4 id="heading-history">History</h4>
<p>When I started exploring Flutter, there was (and there still is) a lot of discussion about state management. At that time, Google promoted Inherited Widget as the way to propagate information down the widget tree. I tried Inherited Widget, and indeed with some extra code, you could use it to make your state accessible to your widgets. But it involved quite some boilerplate, which was not easy to read and understand. It felt overly complex and likely to introduce errors that are difficult to spot.</p>
<p>After that, I tried <a target="_blank" href="https://pub.dev/packages/get_it">get_it</a>. That was easy and simple. It looked a lot that I was used to when I was working in C#. But I was not sure. I found a lot of happy users of get_it. But there were also a lot of people calling the foundation of get_it an anti-pattern. Although I did not really understand the discussion at that time, the choice was made for me by Google. They explicitly recommended Provider in their talk <a target="_blank" href="https://www.youtube.com/watch?v=d_m5csmrf7I">Pragmatic State Management in Flutter (Google I/O’19)</a></p>
<h4 id="heading-provider">Provider</h4>
<p>With Provider, you can wrap your class with one statement into an Inherited Widget.</p>
<p>In the code below, a Provider of an S080StateService is created. When this S080StateService is created, it’s dependencies are injected via its constructor. Provider is used to find these dependencies in the tree above.</p>
<pre><code>  Provider<span class="hljs-operator">&lt;</span>S080StateService<span class="hljs-operator">&gt;</span>(
    create: (BuildContext context) {
      <span class="hljs-keyword">return</span> S080StateService(
        Provider.of&lt;LoggingService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;LadenLossenBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;ConfiguratieBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;UnloadingStateService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;CommunicatieBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;NotInteractiveCommunicationService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;NavigatorService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
      );
    },
  )
</code></pre><p>Below this provider, the instance of the S080StateService is accessible by:</p>
<pre><code><span class="hljs-keyword">var</span> state <span class="hljs-operator">=</span> Provider.of&lt;S080StateService<span class="hljs-operator">&gt;</span>(context);
</code></pre><p>The way this works is readable and easy to understand until you have a lot of services, that depends on a lot of other services.</p>
<h4 id="heading-avoid-super-nesting">Avoid super-nesting</h4>
<p>When your code is dependent on many services, the code gets easily unreadable. Every Provider creates a parent-child relationship and therefore indents your code.</p>
<p>The indentation can easily be solved by using the MultiProvider widget. When you supply a list of widgets to the MultiProvider widget, the MultiProvider will take care of the parent-child relationship at runtime.</p>
<p>In code:</p>
<ul>
<li>MultiProvider<ul>
<li>Provider1</li>
<li>Provider2</li>
<li>Provider3</li>
<li>Provider4</li>
</ul>
</li>
</ul>
<p>At runtime:</p>
<ul>
<li>MultiProvider<ul>
<li>Provider1<ul>
<li>Provider2<ul>
<li>Provider3<ul>
<li>Provider4</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>This improves readability a lot.</p>
<p>Another improvement can be made to move the list of Providers that you feed to the MultiProvider to a separate file.</p>
<pre><code>class S080UnloadingSignaturePage extends StatefulWidget {
  static get Route <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> RouteEx<span class="hljs-operator">&lt;</span>S080UnloadingSignaturePage<span class="hljs-operator">&gt;</span>(
        builder: (context) <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> S080UnloadingSignaturePage(),
      );
  @<span class="hljs-keyword">override</span>
  _S080UnloadingSignaturePageState createState() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span>
      _S080UnloadingSignaturePageState();
}

class _S080UnloadingSignaturePageState
    extends State<span class="hljs-operator">&lt;</span>S080UnloadingSignaturePage<span class="hljs-operator">&gt;</span> {
  final SignatureController _controller <span class="hljs-operator">=</span>
      SignatureController(penColor: CompanyColor.companyBlue);

  @<span class="hljs-keyword">override</span>
  Widget build(BuildContext context) {
    double bottomHeight <span class="hljs-operator">=</span> <span class="hljs-number">80.0</span>;
    <span class="hljs-keyword">return</span> MultiProvider(
<span class="hljs-number">19</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> providers: providers,
      child: Builder(builder: (BuildContext context) {
        <span class="hljs-keyword">var</span> unloadingStateService <span class="hljs-operator">=</span> Provider.of&lt;UnloadingStateService<span class="hljs-operator">&gt;</span>(context);
        <span class="hljs-keyword">var</span> state <span class="hljs-operator">=</span> Provider.of&lt;S080StateService<span class="hljs-operator">&gt;</span>(context);
        <span class="hljs-keyword">return</span> Scanner(
</code></pre><p>Here the list of providers is just one line, on line 19. The actual list of providers is in a separate file:</p>
<pre><code>List<span class="hljs-operator">&lt;</span>SingleChildWidget<span class="hljs-operator">&gt;</span> providers <span class="hljs-operator">=</span> [
  Provider<span class="hljs-operator">&lt;</span>DialogService<span class="hljs-operator">&gt;</span>(
    create: (BuildContext context) {
      <span class="hljs-keyword">return</span> DialogService(
        context,
        Provider.of&lt;ValidatorService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;DatawedgeService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;SoundService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
      );
    },
  ),
  Provider<span class="hljs-operator">&lt;</span>S080StateService<span class="hljs-operator">&gt;</span>(
    create: (BuildContext context) {
      <span class="hljs-keyword">return</span> S080StateService(
        Provider.of&lt;LoggingService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;LadenLossenBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;ConfiguratieBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;UnloadingStateService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;CommunicatieBoxService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;NotInteractiveCommunicationService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
        Provider.of&lt;NavigatorService<span class="hljs-operator">&gt;</span>(context, listen: <span class="hljs-literal">false</span>),
      );
    },
  ),
];
</code></pre><p>Although I am not convinced that this is the very best way, it gets the job done. The code is readable and understandable. And, if you make a mistake, the error message is usually so well detailed, that finding and fixing the issue is easy.</p>
]]></content:encoded></item><item><title><![CDATA[Splash screens and launch icons]]></title><description><![CDATA[Adding splash screens and launch icons is always a pain. It must be done, and, it must be done right. You only get one chance to make a first impression.
Luckily I had a UX designer that provided all the assets in the right sizes.
Because I had all a...]]></description><link>https://yapb.dev/splash-screens-and-launch-icons</link><guid isPermaLink="true">https://yapb.dev/splash-screens-and-launch-icons</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 19 Feb 2020 16:30:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640633478793/lplO4ZEQv.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Adding splash screens and launch icons is always a pain. It must be done, and, it must be done right. You only get one chance to make a first impression.</p>
<p>Luckily I had a UX designer that provided all the assets in the right sizes.</p>
<p>Because I had all assets in the right sizes, I decided to do it manually for Android and iOS and not to use a package like <a target="_blank" href="https://pub.dev/packages/flutter_launcher_icons">https://pub.dev/packages/flutter_launcher_icons</a>.</p>
<p>Splash screens and launch icons are platform-specific. Therefore I had to do the changes in the native solutions in Android Studio and Xcode.</p>
<h4 id="heading-splash-screen-on-ios">Splash screen on iOS</h4>
<p>For iOS, I choose to just adjust what the default iOS project already provided. In Assets.xcassets, there is a section LaunchImage with three empty spots with the names “1x”, “2x” and “3x”.</p>
<p>To add an image to the existing splash screen you have to add these three images, in my case these were:</p>
<pre><code><span class="hljs-attribute">ic_launcher</span>.png   (<span class="hljs-number">393</span>x<span class="hljs-number">137</span>)
<span class="hljs-attribute">ic_launcher</span>-<span class="hljs-number">1</span>.png (<span class="hljs-number">524</span>x<span class="hljs-number">182</span>)
<span class="hljs-attribute">ic_launcher</span>-<span class="hljs-number">2</span>.png (<span class="hljs-number">786</span>x<span class="hljs-number">273</span>)
</code></pre><p>Before adding the files:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946119724/66ja4fuio.png" alt="Before adding the files" /></p>
<p>After adding the files:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946121897/YGia8OBco.png" alt="After adding the files" /></p>
<p>The splash screen now looks like this:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946125074/T5dxp-egK.png" alt="Before changing the background color" /></p>
<p>After changing the background color, the splash screen looks fine:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946128498/2hkSpgnGH.png" alt="After changing the background color" /></p>
<h4 id="heading-launch-icons-on-ios">Launch icons on iOS</h4>
<p>To add the launch icons in iOS, you have to open the section AppIcon in Assets.xcassets. With a default Flutter project, it will look like this in Xcode:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946131940/o0t2Z8_UJ.png" alt="Before changing the launch icons" /></p>
<p>After carefully dragging and dropping all icons to the right places, it will look like this:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1639946135270/uOQIMZ47x.png" alt="After changing the launch icons" /></p>
<p>At last, you would like to change the CFBundleName in the info.plist to something that you want to have as text below the launch icon.</p>
<h4 id="heading-splash-screen-on-android">Splash screen on Android</h4>
<p>The process of adding a splash screen felt a bit easier on the Android side.</p>
<p>First I replaced all existing ic_launcher.png files with the ones that I received from my UX designer:</p>
<pre><code><span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>mdpi<span class="hljs-operator">/</span>ic_splash.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>hdpi<span class="hljs-operator">/</span>ic_splash.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xhdpi<span class="hljs-operator">/</span>ic_splash.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxhdpi<span class="hljs-operator">/</span>ic_splash.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxxhdpi<span class="hljs-operator">/</span>ic_splash.png
</code></pre><p>Add the file that specifies the background color of the splash screen:</p>
<pre><code><span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>values<span class="hljs-operator">/</span>colors.xml
</code></pre><pre><code><span class="hljs-meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">resources</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">color</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"splash\_background"</span>&gt;</span>#012141<span class="hljs-tag">&lt;/<span class="hljs-name">color</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">resources</span>&gt;</span>
</code></pre><p>Edit the file that defines the splash screen to include the background color and the ic_launcher image:</p>
<pre><code><span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>drawable<span class="hljs-operator">/</span>launch_background.xml
</code></pre><pre><code><span class="hljs-operator">&lt;</span>?xml version<span class="hljs-operator">=</span><span class="hljs-string">"1.0"</span> encoding<span class="hljs-operator">=</span><span class="hljs-string">"utf-8"</span>?<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">!</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span> Modify <span class="hljs-built_in">this</span> file to customize your launch splash screen <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span>layer<span class="hljs-operator">-</span>list xmlns:android<span class="hljs-operator">=</span><span class="hljs-string">"http://schemas.android.com/apk/res/android"</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>item android:drawable<span class="hljs-operator">=</span><span class="hljs-string">"@color/splash\_background"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>

    <span class="hljs-operator">&lt;</span><span class="hljs-operator">!</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span> You can insert your own image assets here <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span>item<span class="hljs-operator">&gt;</span>
        <span class="hljs-operator">&lt;</span>bitmap
            android:gravity<span class="hljs-operator">=</span><span class="hljs-string">"center"</span>
            android:src<span class="hljs-operator">=</span><span class="hljs-string">"@mipmap/ic\_splash"</span> <span class="hljs-operator">/</span><span class="hljs-operator">&gt;</span>
    <span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>item<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>layer<span class="hljs-operator">-</span>list<span class="hljs-operator">&gt;</span>
</code></pre><p>That’s it. The splash screen on Android is ready.</p>
<h4 id="heading-launch-icons-on-android">Launch icons on Android</h4>
<p>I placed all the received assets in the following folder:</p>
<pre><code><span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxxhdpi<span class="hljs-operator">/</span>ic_launcher_round.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxxhdpi<span class="hljs-operator">/</span>ic_launcher.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxhdpi<span class="hljs-operator">/</span>ic_launcher_round.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xxhdpi<span class="hljs-operator">/</span>ic_launcher.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xhdpi<span class="hljs-operator">/</span>ic_launcher_round.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>xhdpi<span class="hljs-operator">/</span>ic_launcher.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>mdpi<span class="hljs-operator">/</span>ic_launcher_round.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>mdpi<span class="hljs-operator">/</span>ic_launcher.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>hdpi<span class="hljs-operator">/</span>ic_launcher_round.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>hdpi<span class="hljs-operator">/</span>ic_launcher.png
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>anydpi<span class="hljs-operator">-</span>v26<span class="hljs-operator">/</span>ic_launcher_round.xml
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>mipmap<span class="hljs-operator">-</span>anydpi<span class="hljs-operator">-</span>v26<span class="hljs-operator">/</span>ic_launcher.xml
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>res<span class="hljs-operator">/</span>drawable<span class="hljs-operator">/</span>ic_launcher_foreground.xml
<span class="hljs-operator">&lt;</span>flutter_project<span class="hljs-operator">&gt;</span><span class="hljs-operator">/</span>android<span class="hljs-operator">/</span>app<span class="hljs-operator">/</span>src<span class="hljs-operator">/</span>main<span class="hljs-operator">/</span>ic_launcher<span class="hljs-operator">-</span>web.png
</code></pre><p>At last, you would like to change the android:label in the AndroidManifest.xml to something that you want to have as text below the launch icon.</p>
]]></content:encoded></item><item><title><![CDATA[Theming]]></title><description><![CDATA[When you receive a design from a UX designer, from for example Zeplin, everything is specified:

Colors
Fonts (Name, Size, Style)
Margins and padding of the elements

As in the Flutter counter-sample project, I started in my project with changing the...]]></description><link>https://yapb.dev/theming</link><guid isPermaLink="true">https://yapb.dev/theming</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Sander Roest]]></dc:creator><pubDate>Wed, 19 Feb 2020 10:15:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1640027810231/g46Ed1bUc7Q.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you receive a design from a UX designer, from for example <a target="_blank" href="http://zeplin.io/">Zeplin</a>, everything is specified:</p>
<ul>
<li>Colors</li>
<li>Fonts (Name, Size, Style)</li>
<li>Margins and padding of the elements</li>
</ul>
<p>As in the Flutter counter-sample project, I started in my project with changing the primary color:</p>
<pre><code><span class="hljs-selector-tag">class</span> <span class="hljs-selector-tag">MyApp</span> <span class="hljs-selector-tag">extends</span> <span class="hljs-selector-tag">StatelessWidget</span> {
  <span class="hljs-comment">// This widget is the root of your application.</span>
  <span class="hljs-variable">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-selector-tag">return</span> <span class="hljs-selector-tag">MaterialApp</span>(
      <span class="hljs-attribute">title</span>: <span class="hljs-string">'Flutter Demo'</span>,
      <span class="hljs-attribute">theme</span>: ThemeData(
        <span class="hljs-comment">// This is the theme of your application.</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// Try running your application with "flutter run". You'll see the</span>
        <span class="hljs-comment">// application has a blue toolbar. Then, without quitting the app, try</span>
        <span class="hljs-comment">// changing the primarySwatch below to Colors.green and then invoke</span>
        <span class="hljs-comment">// "hot reload" (press "r" in the console where you ran "flutter run",</span>
        <span class="hljs-comment">// or simply save your changes to "hot reload" in a Flutter IDE).</span>
        <span class="hljs-comment">// Notice that the counter didn't reset back to zero; the application</span>
        <span class="hljs-comment">// is not restarted.</span>
        <span class="hljs-attribute">primarySwatch</span>: Colors.blue,
      ),
      <span class="hljs-attribute">home</span>: MyHomePage(<span class="hljs-attribute">title</span>: <span class="hljs-string">'Flutter Demo Home Page'</span>),
    );
  }
}
</code></pre><p>But then I quickly realized that the primary color is a specific value in a range of colors, a ColorSwatch.</p>
<pre><code> <span class="hljs-string">static</span> <span class="hljs-string">const</span> <span class="hljs-string">MaterialColor</span> <span class="hljs-string">blue</span> <span class="hljs-string">=</span> <span class="hljs-string">MaterialColor(</span>
    <span class="hljs-string">_bluePrimaryValue,</span>
    <span class="hljs-string">&lt;int,</span> <span class="hljs-string">Color&gt;{</span>
       <span class="hljs-attr">50:</span> <span class="hljs-string">Color(0xFFE3F2FD),</span>
      <span class="hljs-attr">100:</span> <span class="hljs-string">Color(0xFFBBDEFB),</span>
      <span class="hljs-attr">200:</span> <span class="hljs-string">Color(0xFF90CAF9),</span>
      <span class="hljs-attr">300:</span> <span class="hljs-string">Color(0xFF64B5F6),</span>
      <span class="hljs-attr">400:</span> <span class="hljs-string">Color(0xFF42A5F5),</span>
      <span class="hljs-attr">500:</span> <span class="hljs-string">Color(_bluePrimaryValue),</span>
      <span class="hljs-attr">600:</span> <span class="hljs-string">Color(0xFF1E88E5),</span>
      <span class="hljs-attr">700:</span> <span class="hljs-string">Color(0xFF1976D2),</span>
      <span class="hljs-attr">800:</span> <span class="hljs-string">Color(0xFF1565C0),</span>
      <span class="hljs-attr">900:</span> <span class="hljs-string">Color(0xFF0D47A1),</span>
    <span class="hljs-string">},</span>
  <span class="hljs-string">);</span>
  <span class="hljs-string">static</span> <span class="hljs-string">const</span> <span class="hljs-string">int</span> <span class="hljs-string">_bluePrimaryValue</span> <span class="hljs-string">=</span> <span class="hljs-number">0xFF2196F3</span><span class="hljs-string">;</span>
</code></pre><p>Such a simple task, changing the primary color, was just not so simple anymore. How do I generate a range of colors from one specific color that was specified by the UX designer?</p>
<p>After some googling, I found the website: <a target="_blank" href="http://mcg.mbitson.com/">http://mcg.mbitson.com/</a>. Here you can delegate the burden of creating a ColorSwatch. If you specify the primary color, this website will generate the color variants for you. Great!</p>
<p>I ended up copying the code of Colors.blue and changed it with the values from that website. Together with some other specific colors, I put that into a class:</p>
<pre><code><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CompanyColor</span> </span>{
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> MaterialColor primaryColor = MaterialColor(
    _primaryColor,
    &lt;<span class="hljs-keyword">int</span>, Color&gt;{
      <span class="hljs-number">50</span>: Color(<span class="hljs-number">0xffe1e4e8</span>),
      <span class="hljs-number">100</span>: Color(<span class="hljs-number">0xffb3bcc6</span>),
      <span class="hljs-number">200</span>: Color(<span class="hljs-number">0xff8090a0</span>),
      <span class="hljs-number">300</span>: Color(<span class="hljs-number">0xff4d647a</span>),
      <span class="hljs-number">400</span>: Color(<span class="hljs-number">0xff27425e</span>),
      <span class="hljs-number">500</span>: Color(_primaryColor),
      <span class="hljs-number">600</span>: Color(<span class="hljs-number">0xff011d3b</span>),
      <span class="hljs-number">700</span>: Color(<span class="hljs-number">0xff011832</span>),
      <span class="hljs-number">800</span>: Color(<span class="hljs-number">0xff01142a</span>),
      <span class="hljs-number">900</span>: Color(<span class="hljs-number">0xff000b1c</span>),
    },
  );

  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> <span class="hljs-keyword">int</span> _primaryColor = <span class="hljs-number">0xff012141</span>;

  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyBlue = Color(<span class="hljs-number">0xff063a65</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyOrange = Color(<span class="hljs-number">0xfff98c1b</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyWhite = Color(<span class="hljs-number">0xffffffff</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyBlueLight = Color(<span class="hljs-number">0xff8fa1b7</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyBlueText = Color(<span class="hljs-number">0xff124370</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyRed = Color(<span class="hljs-number">0xffcd3631</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyGreen = Color(<span class="hljs-number">0xff008a57</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyYellow = Color(<span class="hljs-number">0xfffcc634</span>);
  <span class="hljs-built_in">static</span> <span class="hljs-keyword">const</span> Color companyGrey = Color(<span class="hljs-number">0xffe4e8ed</span>);
}
</code></pre><p>This was good. Now I had one place to put all the custom colors. But this was still not good enough. The UX designer has designed a consistent user interface, but I was repeating myself all over the place.</p>
<p>Referencing the colors by hand did not feel like applying the D.R.Y. principle. There had to be a better way. And, of course, there was: “Make a custom theme”. And the good news is that a theme is not only for colors but also for fonts, padding, etc.</p>
<p>Changing the theme is quite easy. Identify the object that you want to customize application-wide. Drill down to the Flutter source code.</p>
<p>For example for changing the cursor color:</p>
<pre><code>  <span class="hljs-comment">/// The color to use when painting the cursor.</span>
  <span class="hljs-comment">///</span>
  <span class="hljs-comment">/// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor]</span>
  <span class="hljs-comment">/// depending on [ThemeData.platform].</span>
  <span class="hljs-keyword">final</span> Color cursorColor;
</code></pre><p>You can change that in a custom theme like this:</p>
<pre><code>ThemeData companyTheme(BuildContext context) {
  <span class="hljs-keyword">var</span> theme <span class="hljs-operator">=</span> Theme.of(context);
  <span class="hljs-keyword">return</span> theme.copyWith(
    primaryColor: CompanyColor.primaryColor,
    iconTheme: theme.iconTheme.copyWith(
      color: CompanyColor.companyWhite,
    ),
    cursorColor: CompanyColor.companyOrange,
    inputDecorationTheme: theme.inputDecorationTheme.copyWith(
        contentPadding: EdgeInsets.only(bottom: <span class="hljs-number">7.0</span>, top: <span class="hljs-number">4.0</span>),
        enabledBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.underLineInputColor,
          ),
        ),
        focusedBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.underLineInputColor,
          ),
        ),
        focusedErrorBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.companyPink,
          ),
        ),
        errorBorder: UnderlineInputBorder(
          borderSide: BorderSide(
            color: CompanyColor.companyPink,
          ),
        ),
        errorStyle: TextStyle(
          color: CompanyColor.companyRed,
          fontFamily: <span class="hljs-string">'Lato-Bold'</span>,
          fontSize: <span class="hljs-number">14.0</span>,
        )),
  );
}
</code></pre><p>Apply this theme in your MaterialApp:</p>
<pre><code>  <span class="hljs-variable">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-selector-tag">return</span> <span class="hljs-selector-tag">MultiProvider</span>(
      <span class="hljs-attribute">providers</span>: providers(<span class="hljs-attribute">isZebra</span>: isZebra, <span class="hljs-attribute">scanWithCamera</span>: scanWithCamera),
      <span class="hljs-attribute">child</span>: Builder(
        <span class="hljs-attribute">builder</span>: (BuildContext context) =&gt; MaterialApp(
          <span class="hljs-attribute">localizationsDelegates</span>: [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          <span class="hljs-attribute">supportedLocales</span>: [const Locale(<span class="hljs-string">'nl'</span>)],
          <span class="hljs-attribute">title</span>: <span class="hljs-string">'Ferwerda BV'</span>,
          <span class="hljs-attribute">theme</span>: companyTheme(context),
          <span class="hljs-attribute">navigatorObservers</span>: [
            Provider.of&lt;RouteObserver&gt;(
              context,
              <span class="hljs-attribute">listen</span>: false,
            ),
          ],
          <span class="hljs-attribute">home</span>: S005StartupPage(),
        ),
      ),
    );
  }
</code></pre><p>That’s it. Now you have all theming in one place.</p>
]]></content:encoded></item></channel></rss>