<?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[Stephan E.G. Veenstra]]></title><description><![CDATA[Learning and teaching serious Flutter and Dart skills by building not-so-serious apps and games!]]></description><link>https://seg.veenstra.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 06 Sep 2026 15:41:23 GMT</lastBuildDate><atom:link href="https://seg.veenstra.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Fresh Take on Creating App Store Screenshots in Flutter]]></title><description><![CDATA[As Flutter developers, we love the efficiency of writing code once and deploying it everywhere. But when it comes to the very final step of launching or updating an app, generating App Store and Play ]]></description><link>https://seg.veenstra.dev/a-fresh-take-on-creating-app-store-screenshots-in-flutter</link><guid isPermaLink="true">https://seg.veenstra.dev/a-fresh-take-on-creating-app-store-screenshots-in-flutter</guid><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Tue, 26 May 2026 19:35:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/62a1ff516cc4647a1b004ae8/5f2f6f46-88cf-42cb-bfa0-74a08c8a73db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As Flutter developers, we love the efficiency of writing code once and deploying it everywhere. But when it comes to the very final step of launching or updating an app, generating App Store and Play Store screenshots, that efficiency suddenly feels miles away.</p>
<p>If you have ever spent hours taking manual screenshots, wrestling with complex UI test setups, or trying to configure Fastlane for multiple device sizes and localizations, you know it can be a tedious process.</p>
<p>Over the weekend, I released a new package called <a href="https://pub.dev/packages/flutter_store_screenshots"><strong>flutter_store_screenshots</strong></a> to make this workflow smooth, visual, and highly efficient.</p>
<p>But wait, aren't there already dozens of screenshot tools out there? Yes, there are. But this package takes a fundamentally different approach. Here is why it completely changes the game.</p>
<h2>The Problem with Existing Workflows</h2>
<p>Most existing screenshot tools fall into one of two categories, and both come with specific challenges:</p>
<ol>
<li><p><strong>The Beautiful Wrappers:</strong> Tools that put your screenshots inside a nice device frame with a gradient background and a catchy title. They look great, but <em>you still have to provide the raw screenshots yourself</em>. If you change a button color or a layout feature in your app, you have to retake all those base images manually.</p>
</li>
<li><p><strong>The Automation Monsters:</strong> Solutions that integrate with UI tests (like integration tests or Fastlane framing). While powerful, setting up UI tests just to snap pictures is incredibly complex. You have to maintain test scripts, handle mock data or asynchronous loading, and run them across countless device simulators and localized environments.</p>
</li>
</ol>
<p>Neither of these solutions leverages the true power of what we already do best: building UIs with Flutter.</p>
<h2>The Solution: Screenshots as Widgets</h2>
<p><strong>flutter_store_screenshots</strong> takes a completely different route. It doesn't ask you to manually take photos, and it doesn't force you into complex automated testing.</p>
<p>Instead, it treats your screenshots exactly like the rest of your app: <strong>as Flutter Widgets.</strong></p>
<p>The core idea is elegant: You create a tiny companion project (a Flutter desktop app, optimized for macOS) right next to your main mobile application. This project takes a dependency on both your actual app and the <code>flutter_store_screenshots</code> package.</p>
<p>Because you have direct access to your app's codebase, <strong>you can import and render your real app UI directly inside the screenshot builder.</strong></p>
<p>Instead of writing test scripts to navigate to a specific page, you just instantiate the Widget directly:</p>
<pre><code class="language-dart">StoreScreenshot(
  name: 'home_screen',
  builder: (context) =&gt; const HomeScreen(),
),
</code></pre>
<p>Absolute Flexibility and Control Since you are writing standard Flutter code, you have ultimate control over how the screenshots look. The package automatically handles the heavy lifting of simulating different operating systems, screen sizes, and localizations. It passes this environment data down via the <code>BuildContext</code>.</p>
<p>Want a plain, full-screen snapshot of your UI? Done. Want a highly customized, branded store asset with a custom font, background gradients, device frames, and translated marketing text? <strong>Just build it with Widgets!</strong></p>
<pre><code class="language-dart">StoreScreenshot(
  name: 'home_fancy',
  builder: (context) =&gt; MyFancyStoreFrame(
    title: mapLocalizationToText(context),
    device: Devices.ios.iPhone15ProMax,
    child: const HomeScreen(),
  ),
),
</code></pre>
<p>If you know how to build a layout in Flutter, you already know how to design your perfect App Store assets. No external design tools, no complex configurations.</p>
<h2>The Real Superpower: Instant Updates</h2>
<p>The biggest drain on a developer's time isn't creating the first set of screenshots, it's updating them when the app evolves.</p>
<p>When you change a feature, tweak your branding, or update a page design, traditional workflows require you to redo everything from scratch. With this package, <strong>your screenshots are inherently tied to your live production UI.</strong></p>
<p>If you update the <code>HomeScreen</code> in your main app, your screenshot project automatically reflects those changes. You simply run your desktop screenshot app, preview how the screens look across all configurations in real-time, and hit <strong>Export</strong>. With a single click, a fresh batch of perfect store assets is generated and saved directly to your disk.</p>
<h2>A Faster Way to Ship</h2>
<p>Generating store assets shouldn't be a bottleneck in your release cycle. By leveraging the power of Flutter’s own rendering engine and desktop capabilities, you can turn screenshot generation into a seamless, visual, and highly maintainable part of your development process.</p>
<p>Ready to streamline how you ship your apps?</p>
<p>👉 Check out <a href="https://pub.dev/packages/flutter_store_screenshots">flutter_store_screenshots on pub.dev</a> to see the full documentation, or dive straight into the <a href="https://pub.dev/packages/flutter_store_screenshots/example">GitHub example project</a> to get started.</p>
]]></content:encoded></item><item><title><![CDATA[🛑 Warning: The Hidden Costs of Using Firebase App Distribution with Android App Bundles (AAB)]]></title><description><![CDATA[If you're looking for a streamlined way to distribute your Android test builds using Firebase App Distribution (FAD) with the modern Android App Bundle (.aab) format, you might want to pause.
While the promise of using AABs via FAD—leveraging the Goo...]]></description><link>https://seg.veenstra.dev/warning-the-hidden-costs-of-using-firebase-app-distribution-with-android-app-bundles-aab</link><guid isPermaLink="true">https://seg.veenstra.dev/warning-the-hidden-costs-of-using-firebase-app-distribution-with-android-app-bundles-aab</guid><category><![CDATA[firebase app distribution]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[Android]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[aab]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Mon, 08 Dec 2025 08:00:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/lLLZkSmxe7A/upload/b19551973b6f2ab3ec06f187852be613.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're looking for a streamlined way to distribute your Android test builds using Firebase App Distribution (FAD) with the modern <strong>Android App Bundle (.aab)</strong> format, you might want to pause.</p>
<p>While the promise of using AABs via FAD—leveraging the Google Play Console's infrastructure—is appealing, the actual setup and testing experience is often frustrating, counter-intuitive, and requires far more configuration than anticipated.</p>
<p><strong>In my opinion, the increased complexity and hidden obstacles make uploading standard APKs a far superior and faster way to start testing.</strong></p>
<p>Here is a breakdown of the three major hurdles I encountered, followed by a guide on how to get it working and my final verdict.</p>
<hr />
<h2 id="heading-1-the-setup-a-deceptively-high-barrier-to-entry">1. The Setup: A Deceptively High Barrier to Entry</h2>
<p>The official documentation suggests you only need to have your app published on "at least one track" (like the Internal Test Track) to link Firebase and the Google Play Console. This proved to be insufficient.</p>
<h3 id="heading-the-reality-you-need-a-near-complete-play-store-listing">The Reality: You Need a <em>Near-Complete</em> Play Store Listing</h3>
<p>Despite having multiple builds successfully running on the Internal Test Track, I constantly ran into the infamous error:</p>
<blockquote>
<p>"1 linked app is not on a published track in Google Play"</p>
</blockquote>
<p>This error persists unless you have completed a significant portion of the tedious Google Play listing configuration, including details that seem irrelevant for private test distribution (e.g., store graphics, privacy policy links, target audience, content rating questionnaires).</p>
<ul>
<li><strong>The Cost:</strong> The "quick start" promised by FAD and AABs forces you to complete a substantial part of your production configuration just to distribute a test build. The threshold for simply beginning internal testing becomes unnecessarily high.</li>
</ul>
<hr />
<h2 id="heading-2-the-critical-misunderstanding-signing-and-service-keys">2. The Critical Misunderstanding: Signing and Service Keys</h2>
<p>When you link Firebase to the Google Play Console, the expectation is that Play will handle the generation of APKs from the AAB and sign them using the <strong>same key</strong> as your production app (the Upload Key or the Play App Signing Key).</p>
<p>This is crucial for features dependent on the app's signing certificate, such as:</p>
<ul>
<li><p><strong>Google Sign-In</strong></p>
</li>
<li><p><strong>Google Maps API</strong></p>
</li>
<li><p><strong>In-App Purchases (IAP)</strong> (Often tied to your Play Console configuration)</p>
</li>
</ul>
<h3 id="heading-the-reality-firebase-generates-its-own-signing-certificate">The Reality: Firebase Generates Its Own Signing Certificate</h3>
<p>Upon the first AAB upload to FAD, Firebase <strong>creates its own temporary signing certificate</strong> to generate the necessary APKs for App Distribution testers.</p>
<ul>
<li><p><strong>The Problem:</strong> This certificate is <strong>different</strong> from your production or developer keys. Consequently, any critical service (like Google Sign-In) configured to accept only your legitimate app signing fingerprint will immediately <strong>fail</strong> in the test build distributed via FAD.</p>
</li>
<li><p><strong>The Workaround:</strong> To make Google services work, you would have to register the temporary Firebase signing certificate fingerprint with Google Cloud Console, which is tedious and undermines the goal of seamless testing.</p>
</li>
</ul>
<blockquote>
<p><strong>Contrast:</strong> If you upload a standard <strong>APK</strong>, you can sign it yourself using your true production/developer key (provided you don't use Google Play App Signing), ensuring all your dependent services work out of the box.</p>
</blockquote>
<hr />
<h2 id="heading-3-the-end-user-experience-an-unacceptable-installation-flow">3. The End-User Experience: An Unacceptable Installation Flow</h2>
<p>After finally conquering the setup and signing issues, the final hurdle was the installation experience for the tester using the recommended <strong>App Tester</strong> app.</p>
<h3 id="heading-the-reality-developer-mode-for-everyone">The Reality: Developer Mode for Everyone</h3>
<p>When a non-technical tester attempts to install the app via the FAD App Tester, they are met with a jarring and complex installation process:</p>
<ol>
<li><p>They are prompted to go into their phone's Google Play settings.</p>
</li>
<li><p>They must press a specific item <strong>7 times</strong> to activate <strong>Developer Mode</strong>.</p>
</li>
<li><p>They must enable a specific security check to allow the installation of the test app.</p>
</li>
</ol>
<p>This kind of process is acceptable for a developer installing an app via Android Studio, but it is an <strong>absolute nightmare</strong> for non-technical internal stakeholders, marketers, or management testing a build.</p>
<ul>
<li><strong>The Cost:</strong> This significantly increases friction and requires extensive hand-holding, defeating the purpose of a simple distribution tool.</li>
</ul>
<hr />
<h2 id="heading-final-verdict-and-suggestion">💡 Final Verdict and Suggestion</h2>
<p>Firebase App Distribution is an excellent tool, but the integration with Android App Bundles is flawed and requires too much upfront investment in Play Console configuration and forces the use of a non-production signing key.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>AAB via Firebase App Distribution</strong></td><td><strong>Standard APK via Firebase App Distribution</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Setup Barrier</strong></td><td><strong>HIGH</strong> (Requires extensive Play Console config)</td><td><strong>LOW</strong> (Uploads instantly)</td></tr>
<tr>
<td><strong>Signing Key</strong></td><td><strong>New/Unique Firebase Key</strong> (Breaks services)</td><td><strong>Your Chosen Key</strong> (Services work)</td></tr>
<tr>
<td><strong>Tester UX</strong></td><td><strong>POOR</strong> (Requires Developer Mode activation)</td><td><strong>GOOD</strong> (Simple installation prompt)</td></tr>
<tr>
<td><strong>Flexibility</strong></td><td><strong>LOW</strong></td><td><strong>HIGH</strong></td></tr>
</tbody>
</table>
</div><h3 id="heading-my-suggestion-upload-signed-apks"><strong>My Suggestion: Upload Signed APKs</strong></h3>
<p>If your primary goal is fast, reliable, and flexible internal distribution for testers and stakeholders:</p>
<p><strong>Skip the AAB integration with FAD and upload your locally signed APK files directly to Firebase App Distribution.</strong></p>
<p>This approach allows you to:</p>
<ol>
<li><p>Use your actual developer or production signing key.</p>
</li>
<li><p>Avoid the onerous Google Play Console configuration requirements.</p>
</li>
<li><p>Provide a simple, non-technical installation experience for your testers.</p>
</li>
</ol>
<p>Hopefully, this information saves fellow developers the many hours of frustration I endured.</p>
]]></content:encoded></item><item><title><![CDATA[My October Challenge: Building and Releasing Minesweeper Hex]]></title><description><![CDATA[Last month, I set myself a new challenge: to build and release a small app from scratch within a month. This decision came after a realization I had while working on my previous project. That project had so many uncertainties and things left to do th...]]></description><link>https://seg.veenstra.dev/my-october-challenge-building-and-releasing-minesweeper-hex</link><guid isPermaLink="true">https://seg.veenstra.dev/my-october-challenge-building-and-releasing-minesweeper-hex</guid><category><![CDATA[hexagon]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Game Development]]></category><category><![CDATA[app development]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Minesweeper]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sat, 02 Nov 2024 14:17:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730556795861/3b2bd377-b191-43c8-adaf-ca89c2ee94d7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last month, I set myself a new challenge: to build and release a small app from scratch within a month. This decision came after a realization I had while working on my previous project. That project had so many uncertainties and things left to do that when someone asked when I expected a first release, I realized it could take a year or two, even just to make something workable. And that would be before the inevitable rounds of tweaking and refining. The thought of working on it for the next few years felt daunting and demotivating. So, I decided to take on something smaller, more contained—a project I could complete from start to finish in a limited time.</p>
<p>I chose to build a simple minesweeper game with hexagonal fields, calling it "Minesweeper Hex." Minesweeper felt ideal because it has a clear set of rules, so I could skip the step of designing a new game concept and dive straight into development.</p>
<h2 id="heading-lessons-from-the-challenge">Lessons from the Challenge</h2>
<p>This experience taught me the importance of focusing on essentials. With only a month, I realized that extra features like multiple game modes, achievements, or even in-game ads could wait until later. Instead, I focused on creating a playable version and delivering a complete experience with core gameplay.</p>
<p>The challenge also influenced how I approached the game’s architecture. Given the tight timeline, I chose to go “back to basics”—avoiding complex architecture and abstractions. I relied directly on services and resources, using mainly ChangeNotifiers instead of external state management solutions. This simple structure kept development streamlined and allowed me to focus on getting the game functional.</p>
<h2 id="heading-releasing-the-app-not-as-daunting-as-expected">Releasing the App: Not as Daunting as Expected</h2>
<p>One surprising aspect was how smoothly the release process went. Initially, I saw it as a big mountain to climb—setting up store listings, creating screenshots, and filling out forms. But once I got started, I realized it wasn’t as difficult as I’d anticipated, and in a few days, everything was ready to go live.</p>
<h2 id="heading-leveraging-ai-to-streamline-development">Leveraging AI to Streamline Development</h2>
<p>Throughout the project, AI tools like ChatGPT were incredibly useful. For example, ChatGPT helped me set up the base game logic and gave me ideas on implementing CustomPainter for the hexagons. Although the initial code needed adjustments, it was a great starting point. AI also helped me draft app store descriptions, allowing me to spend more time refining the game.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Completing "Minesweeper Hex" in just a month was a rewarding experience. The challenge of creating something from start to finish in a short period reminded me of the power of focusing on essentials. I’m excited to apply these lessons and continue taking on projects that feel achievable yet rewarding.</p>
<h3 id="heading-download-minesweeper-hex">Download Minesweeper Hex</h3>
<p>I’ve made Minesweeper Hex available on both <a target="_blank" href="https://apps.apple.com/us/app/minesweeper-hex/id6737420937">iOS</a> and <a target="_blank" href="https://play.google.com/store/apps/details?id=nl.segit.minesweeper_hex">Android</a>.</p>
]]></content:encoded></item><item><title><![CDATA[The making off SciFiContainer]]></title><description><![CDATA[At the time of writing this article I’m working on a Survival/Management App/Game using Flutter.
The game takes place in a distant future, so I want the UI of the game to resemble this. I’ve done some searching on the internet and took inspiration of...]]></description><link>https://seg.veenstra.dev/the-making-off-scificontainer</link><guid isPermaLink="true">https://seg.veenstra.dev/the-making-off-scificontainer</guid><category><![CDATA[Flutter]]></category><category><![CDATA[CustomPaint]]></category><category><![CDATA[Dart]]></category><category><![CDATA[scifi]]></category><category><![CDATA[UI]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sat, 21 Sep 2024 11:38:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726913389056/8d363c67-3ee6-4f8b-9a12-df42b2fec44a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the time of writing this article I’m working on a Survival/Management App/Game using Flutter.</p>
<p>The game takes place in a distant future, so I want the UI of the game to resemble this. I’ve done some searching on the internet and took inspiration of games and movies.</p>
<p>After playing around with some ideas, I came up with the following idea, I need 45-degree corners!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726913668383/350c22d3-3894-47bb-b876-db13f6a5c80a.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-beveledrectangleborder">BeveledRectangleBorder</h1>
<p>I already used some 45-degree angled corners in the game, like for items in the list of goals.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726914305478/5a45414e-316d-46b9-9e74-52c6a3483349.png" alt class="image--center mx-auto" /></p>
<p>For this I used <a target="_blank" href="https://api.flutter.dev/flutter/painting/BeveledRectangleBorder-class.html">BeveledRectangleBorder</a> which can easily be applied to Containers and buttons.</p>
<p>But just having beveled corners isn’t enough, I want more. So how can I do this?</p>
<h1 id="heading-custompaint">CustomPaint</h1>
<p>In short, by using <a target="_blank" href="https://api.flutter.dev/flutter/widgets/CustomPaint-class.html">CustomPaint</a> you get full control over what you draw to the screen. You can use it to make very complex UI which is great for when the build-in widgets are not sufficient.</p>
<p>Then I realized that in my 6 years of using Flutter, I never used CustomPaint. I never felt like I needed it, but I’ve seen people do amazing things with them. One of them is <a target="_blank" href="https://x.com/jogboms">Jeremiah Ogbomo</a>. He showed off some cool UI in his <a target="_blank" href="https://saturdays-are-for-flutter.vercel.app/">saturdays-are-for-flutter</a> series.</p>
<p>I’ve attended one of Jeremiah’s presentations and that made me think about using CustomPainter for my SciFiContainer.</p>
<h1 id="heading-challenges">Challenges</h1>
<p>I’ve spend about 4 or 5 evenings on creating the SciFiContainer.</p>
<p>The biggest challenge for me was coming up with a nice API as I wanted it to be quite flexible in order to use it in multiple places without having all containers looking the same.</p>
<p>The other big challenge was calculating all the points and padding. The <code>Size</code> that is passed into the CustomPainter is determined on the <code>child</code> widget. But this is not the limit in which you can paint. If you want, you can ‘paint’ outside the given size.</p>
<p>This meant a lot of different cases to take into account. Like when the indent is negative. This means that all the points on that side has to move in the opposite direction to make it fit inside the ‘bounding box‘.</p>
<p>The configurations of the corners and the borders also affect the padding inside the container as it tries to keep the content within the border.</p>
<p>Eventually I got the hang of it and was I able to make it all work as intended.</p>
<p>There are still some things that can be cleaned up to create a more ‘safe’ API but as it’s mainly intended for my own use, I don’t want to spend too much time on it.</p>
<h1 id="heading-result">Result</h1>
<p>Because quite some people on Twitter/X showed interest in this SciFiContainer I decided to share the code as a <a target="_blank" href="https://gist.github.com/SEGVeenstra/dda3861ae51964afea939475372cb605">GitHub gist</a>. But you can also view it below.</p>
<p><a target="_blank" href="https://dartpad.dev/?id=dda3861ae51964afea939475372cb605">View on DartPad</a></p>
<iframe style="width:100%;height:600px" src="https://dartpad.dev/embed-flutter.html?id=dda3861ae51964afea939475372cb605&amp;run=true"></iframe>

<p>If you like this kind of content, give me a follow on <a target="_blank" href="https://x.com/SEGVeenstra">Twitter/X</a> as most of the stuff I make will be shared there.</p>
]]></content:encoded></item><item><title><![CDATA[How I Theme My Flutter Apps: Introducing "Skinning"]]></title><description><![CDATA[Last year, I wrote two articles about theming in Flutter, sharing my frustrations with ThemeData (part 1 & part 2). While ThemeData is powerful, its extensive properties can be overwhelming, especially when many of them are rarely used. After experim...]]></description><link>https://seg.veenstra.dev/how-i-theme-my-flutter-apps-introducing-skinning</link><guid isPermaLink="true">https://seg.veenstra.dev/how-i-theme-my-flutter-apps-introducing-skinning</guid><category><![CDATA[Flutter]]></category><category><![CDATA[theme]]></category><category><![CDATA[Dart]]></category><category><![CDATA[theming]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sun, 08 Sep 2024 09:12:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/8ig2a2Axsxg/upload/7f359de1d752beec7adb4a15d20a5779.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last year, I wrote two articles about theming in Flutter, sharing my frustrations with <code>ThemeData</code> (<a target="_blank" href="https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me">part 1</a> &amp; <a target="_blank" href="https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me-part-2">part 2</a>). While <code>ThemeData</code> is powerful, its extensive properties can be overwhelming, especially when many of them are rarely used. After experimenting with a more intuitive approach, I developed a method called "Skinning." This method simplifies theming by focusing on the essential properties that change based on the device’s theme (light or dark mode) while coupling behavior and styling in custom widgets.</p>
<p>In this article, I’ll walk you through what Skinning is, why I prefer it over <code>ThemeData</code>, and how I implement it in my apps.</p>
<hr />
<h3 id="heading-the-problem-with-themedata">The Problem with ThemeData</h3>
<p>When I first started using <code>ThemeData</code>, I found it bloated with hundreds of properties that were unnecessary for my projects. I often only needed to change a handful of colors or styles between light and dark modes. Trying to manage those changes through <code>ThemeData</code> made the theming process feel overly complicated. Additionally, <code>ThemeData</code> is primarily concerned with styling individual components, but what if you want to adjust both the behavior and styling in a unified way?</p>
<h3 id="heading-my-solution-skinning">My Solution: "Skinning"</h3>
<p>Instead of directly using Flutter’s components like <code>ElevatedButton</code>, <code>AppBar</code>, or <code>Scaffold</code>, I wrap them in custom widgets that encapsulate both behavior and appearance. This way, I have full control over how my components look and function, making global changes easier and cleaner.</p>
<h4 id="heading-custom-widgets-for-unified-behavior-and-theming">Custom Widgets for Unified Behavior and Theming</h4>
<p>For instance, when I need a primary button, I don’t use an <code>ElevatedButton</code> directly. Instead, I create a <code>PrimaryButton</code> widget that wraps the <code>ElevatedButton</code> internally, allowing me to manage both behavior and styling in one place.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PrimaryButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> label;
  <span class="hljs-keyword">final</span> VoidCallback onPressed;

  <span class="hljs-keyword">const</span> PrimaryButton({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.label, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onPressed});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ElevatedButton(
      onPressed: onPressed,
      style: ButtonStyle(
        backgroundColor: WidgetStateProperty.all(context.skin.primaryColor), <span class="hljs-comment">// Using Skin for color</span>
      ),
      child: Text(label),
    );
  }
}
</code></pre>
<p>This makes it easy to update all primary buttons by just modifying the <code>PrimaryButton</code> class. If I need to switch to an <code>OutlinedButton</code>, I do it once in the <code>PrimaryButton</code> class, and the change applies app-wide.</p>
<h4 id="heading-skin-a-minimalist-alternative-to-themedata">Skin: A Minimalist Alternative to ThemeData</h4>
<p>To handle theming across light and dark modes, I use a <code>SkinProvider</code>, an <code>InheritedWidget</code> that provides two <code>Skin</code> objects: <code>lightSkin</code> and <code>darkSkin</code>. The app automatically switches between these based on the device’s theme configuration.</p>
<p>Here’s how the <code>SkinProvider</code> works:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SkinProvider</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">InheritedWidget</span> </span>{
  <span class="hljs-keyword">final</span> Skin lightSkin;
  <span class="hljs-keyword">final</span> Skin darkSkin;

  <span class="hljs-keyword">const</span> SkinProvider({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.lightSkin,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.darkSkin,
    <span class="hljs-keyword">required</span> Widget child,
  }) : <span class="hljs-keyword">super</span>(child: child);

  <span class="hljs-keyword">static</span> Skin of(BuildContext context) {
    <span class="hljs-keyword">final</span> brightness = MediaQuery.of(context).platformBrightness;
    <span class="hljs-keyword">final</span> provider = context.dependOnInheritedWidgetOfExactType&lt;SkinProvider&gt;()!;
    <span class="hljs-keyword">return</span> brightness == Brightness.dark ? provider.darkSkin : provider.lightSkin;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> updateShouldNotify(SkinProvider oldWidget) {
    <span class="hljs-keyword">return</span> lightSkin != oldWidget.lightSkin || darkSkin != oldWidget.darkSkin;
  }
}
</code></pre>
<p>And here’s a basic <code>Skin</code> class:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Skin</span> </span>{
  <span class="hljs-keyword">final</span> Color primaryColor;
  <span class="hljs-keyword">final</span> Color secondaryColor;

  Skin({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.primaryColor,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.secondaryColor,
  });
}
</code></pre>
<p>Instead of managing a large <code>ThemeData</code> object, I focus only on the properties that need to change between light and dark modes. In this example, I only define <code>primaryColor</code> and <code>secondaryColor</code> in each <code>Skin</code>, keeping it simple.</p>
<h4 id="heading-extension-method-for-easy-access">Extension Method for Easy Access</h4>
<p>To make accessing the current <code>Skin</code> easier, I use an extension method on <code>BuildContext</code>. This allows me to quickly get the active theme in any widget with <code>context.skin</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> SkinExtension <span class="hljs-keyword">on</span> BuildContext {
  Skin <span class="hljs-keyword">get</span> skin =&gt; SkinProvider.of(<span class="hljs-keyword">this</span>);
}
</code></pre>
<p>Now, in my custom widgets, I can easily access the current skin:</p>
<pre><code class="lang-dart">style: ButtonStyle(
  backgroundColor: WidgetStateProperty.all(context.skins.primaryColor),
),
</code></pre>
<p>This makes it easy to theme components based on the active light or dark mode.</p>
<hr />
<h3 id="heading-why-i-use-skinning">Why I Use Skinning</h3>
<ul>
<li><p><strong>Unified Behavior and Theming:</strong> By creating custom widgets like <code>PrimaryButton</code>, I can manage both the appearance and behavior of my UI elements in one place. This keeps my code DRY and simplifies maintenance.</p>
</li>
<li><p><strong>Automatic Theme Switching:</strong> By providing both <code>lightSkin</code> and <code>darkSkin</code> via the <code>SkinProvider</code>, my app automatically switches between the two based on the device’s light or dark mode configuration. No extra logic is needed in each component to handle the mode switching.</p>
</li>
<li><p><strong>Simplicity:</strong> Instead of dealing with the complexity of <code>ThemeData</code>, I focus on a leaner <code>Skin</code> class that includes only the properties I need. This keeps the theming logic clear and easy to manage.</p>
</li>
<li><p><strong>Flexibility:</strong> When a design change happens, like switching all primary buttons from <code>ElevatedButton</code> to <code>OutlinedButton</code>, I only need to modify the <code>PrimaryButton</code> class. This change propagates across the entire app, reducing the risk of inconsistencies.</p>
</li>
</ul>
<hr />
<h3 id="heading-conclusion">Conclusion</h3>
<p>Skinning offers a simpler, more flexible approach to theming Flutter apps. By wrapping components in custom widgets and using a <code>SkinProvider</code> to manage both <code>lightSkin</code> and <code>darkSkin</code>, I can easily switch themes based on device configuration while keeping behavior and appearance tightly coupled.</p>
<p>This method has streamlined my theming process and allowed me to focus on what really matters: the specific styles and behaviors my app needs. If you’re looking for a more intuitive way to theme your Flutter apps, Skinning might be just what you need.</p>
]]></content:encoded></item><item><title><![CDATA[Triggering Code on Flutter’s Main Isolate from a WorkManager Background Isolate]]></title><description><![CDATA[Use Case: Managing Nutrition in a Game
I'm currently developing a game where nutrition plays a vital role. The user has a nutrition level that decreases over time, whether the app is open or not. When the nutrition level drops too low, I want to noti...]]></description><link>https://seg.veenstra.dev/triggering-code-on-flutters-main-isolate-from-a-workmanager-background-isolate</link><guid isPermaLink="true">https://seg.veenstra.dev/triggering-code-on-flutters-main-isolate-from-a-workmanager-background-isolate</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[workmanager]]></category><category><![CDATA[background tasks]]></category><category><![CDATA[dart-isolates]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sun, 18 Aug 2024 12:24:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2OrOt0QyNDk/upload/73377190eb63a4828235d9b207d69372.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-use-case-managing-nutrition-in-a-game">Use Case: Managing Nutrition in a Game</h1>
<p>I'm currently developing a game where nutrition plays a vital role. The user has a nutrition level that decreases over time, whether the app is open or not. When the nutrition level drops too low, I want to notify the user, prompting them to open the app and consume food.</p>
<p>To achieve this, I need to periodically calculate the current nutrition level and send a notification if it's too low. This needs to happen even when the app is in the background or closed.</p>
<h1 id="heading-the-challenge-isolate-communication-in-flutter">The Challenge: Isolate Communication in Flutter</h1>
<p>Flutter uses isolates for concurrency, with each isolate having its own memory space. This means that isolates do not share data or instances of classes directly. The <code>workmanager</code> package allows us to run background tasks in a separate isolate. However, the challenge arises when we need to communicate between the background isolate (where the <code>workmanager</code> task runs) and the main isolate (where the UI is managed).</p>
<p>For example, if the background task runs while the app is closed, it can create its own instance of a <code>NutritionService</code> to calculate the nutrition level and send notifications. However, if the app is open, we want the main isolate to handle these calculations. The main reason for this is that the UI listens to the <code>NutritionService</code> instance on the main isolate, and using a separate instance on the background isolate won't update the UI.</p>
<h1 id="heading-solution-communicating-between-isolates">Solution: Communicating Between Isolates</h1>
<p>To handle this scenario, we can implement a communication mechanism between the background isolate and the main isolate. One approach is to use <code>SendPort</code> and <code>ReceivePort</code>, which are part of Dart's isolate communication API. Here’s a high-level overview of how this can be implemented:</p>
<ol>
<li><p><strong>Set Up a ReceivePort in the Main Isolate</strong>:</p>
<ul>
<li>The main isolate should create a <code>ReceivePort</code> and pass its <code>SendPort</code> to the background isolate when initializing the background task.</li>
</ul>
</li>
<li><p><strong>SendPort from Background Isolate</strong>:</p>
<ul>
<li>When the background task is triggered by the <code>workmanager</code>, it can send a message through the <code>SendPort</code> to the main isolate.</li>
</ul>
</li>
<li><p><strong>Handle Messages in the Main Isolate</strong>:</p>
<ul>
<li>The main isolate listens to the <code>ReceivePort</code> and triggers the necessary UI updates or service calls based on the message received from the background isolate.</li>
</ul>
</li>
<li><p><strong>Conditionally Execute Code</strong>:</p>
<ul>
<li>If the app is open, the main isolate performs the necessary calculations and UI updates. If the app is closed, the background isolate handles the notification logic independently.</li>
</ul>
</li>
</ol>
<p>Here’s a simplified code outline for this:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Main Isolate</span>
<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">final</span> receivePort = ReceivePort();
  IsolateNameServer.registerPortWithName(receivePort.sendPort, <span class="hljs-string">'main_send_port'</span>);

  receivePort.listen((message) {
    <span class="hljs-comment">// Handle message from the background isolate</span>
    <span class="hljs-keyword">if</span> (message == <span class="hljs-string">'trigger_nutrition_check'</span>) {
      <span class="hljs-comment">// Perform nutrition check on main isolate and update UI</span>
    }
  });

  runApp(MyApp());
}

<span class="hljs-comment">// Background Isolate Task</span>
<span class="hljs-keyword">void</span> backgroundTask() {
  <span class="hljs-keyword">final</span> sendPort = IsolateNameServer.lookupPortByName(<span class="hljs-string">'main_send_port'</span>);

  <span class="hljs-keyword">if</span> (sendPort != <span class="hljs-keyword">null</span>) {
    sendPort.send(<span class="hljs-string">'trigger_nutrition_check'</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// If the main isolate is not available, run the task independently</span>
    checkNutritionAndNotify();
  }
}

<span class="hljs-comment">// Example function to register background task</span>
<span class="hljs-keyword">void</span> registerBackgroundTask() {
  Workmanager().registerOneOffTask(<span class="hljs-string">'uniqueName'</span>, <span class="hljs-string">'backgroundTask'</span>, inputData: {});
}
</code></pre>
<p>This method ensures that when the app is open, the main isolate handles the nutrition check, thereby keeping the UI in sync. If the app is closed, the background isolate takes over the task and sends a notification directly.</p>
<p>By handling isolate communication this way, we can ensure that the app's state remains consistent, and the user experience is smooth, regardless of whether the app is open or running in the background.</p>
]]></content:encoded></item><item><title><![CDATA[Getting my app in the hands of testers]]></title><description><![CDATA[About 6 weeks ago I started playing around with an idea for a game.
It was not yet a very concrete idea, but more a collection of gameplay elements that I thought would go well together.
Therefore I decided to build a prototype app in which I would p...]]></description><link>https://seg.veenstra.dev/getting-my-app-in-the-hands-of-testers</link><guid isPermaLink="true">https://seg.veenstra.dev/getting-my-app-in-the-hands-of-testers</guid><category><![CDATA[codemagic]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Game Development]]></category><category><![CDATA[Firebase]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Wed, 31 Jul 2024 19:44:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/e3OUQGT9bWU/upload/35d84053dc6009622d7f7636ffe9c995.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>About 6 weeks ago I started playing around with an idea for a game.</p>
<p>It was not yet a very concrete idea, but more a collection of gameplay elements that I thought would go well together.</p>
<p>Therefore I decided to build a prototype app in which I would put these elements to the test and see if they're actually as fun as I imagined.</p>
<h1 id="heading-the-first-game-loop">The first game-loop</h1>
<p>About two weeks ago, I had finished my first full game-loop.</p>
<p>When the player starts the game, they have a nutrition count of 100%. The nutrition would lower by 3% every hour. When the nutrition reaches 0%, the game is over.</p>
<p>Players earn food by walking. Around every 1,000 steps, they are able to claim rewards, which for now would just be one of two types of food.</p>
<p>Consuming the food would refill part of the nutrition.</p>
<p>The goal is to stay alive as long as possible.</p>
<p>Because the 'game' was basically playable, I started testing myself.<br />I just started playing the game on a daily basis, and I thought it was already pretty fun.</p>
<h1 id="heading-distributing-to-testers">Distributing to testers</h1>
<p>I decided to go with the same approach I used for <a target="_blank" href="https://seg.veenstra.dev/scavenger-hunt-plus">Scavenger Hunt Plus</a>.</p>
<h2 id="heading-codemagic">CodeMagic</h2>
<p>I've been a long time fan of <a target="_blank" href="https://codemagic.io/">CodeMagic</a> and for my pet projects they are my go-to for the CI/CD. They make it really easy to set it all up with incredible guides.</p>
<p>My distribution pipeline is triggered for every Pull-Request that gets merged into my develop branch. Every time I finish a feature, a new version is automatically distributed to the testers.</p>
<p>Specially if you're new to CI/CD, then you should definitely check them out!<br />Their <a target="_blank" href="https://codemagic.io/pricing/">free-tier</a> has been more than enough for me and my pet projects over the years.</p>
<h2 id="heading-firebase-app-distribution">Firebase app distribution</h2>
<p>For Android I use <a target="_blank" href="https://firebase.google.com/docs/app-distribution">Firebase App Distribution</a>. I prefer Firebase over using the Play Store Test Tracks for the following reasons:</p>
<ol>
<li><p>Easier to switch between versions.</p>
</li>
<li><p>Easier to work with for testers.</p>
</li>
<li><p>No issues with having to increase build numbers for every release.</p>
</li>
<li><p>When you use build flavors for different environments (production, test), Google doesn't like it when you have two similar app entries in your Play Store account up to a point where they even remove your app (true story).</p>
</li>
</ol>
<p>For iOS, Firebase is not really an option. While CodeMagic makes it easy for you to setup and deploy your iOS app, the experience for your testers to actually being able to access and download the apps is horrific.</p>
<p>I did try it for this current project, but non of my testers where able to complete the setup on their devices resulting in non of them being able to download the app.</p>
<h2 id="heading-testflight">TestFlight</h2>
<p>Because Firebase didn't work out, I decided to go with TestFlight again. Unlike Google, does Apple not care about you having similar apps just for different environments.</p>
<p>TestFlight is pretty good and has similar functionality as Firebase. Users are able to download all the versions you've made available and they can easily download the app through the TestFlight app.</p>
<p>The mayor downside of TestFlight for me is that in order to make the builds available to external testers, is that it has to go to review.</p>
<p>Now the external testers review is often less strict than the review Apple does for production releases, but still you'll have to wait a bit until your testers get access to your latest build.</p>
<h2 id="heading-google-play-test-tracks">Google Play Test Tracks</h2>
<p>While I'm still early in development, I am not using Test Tracks. But I will start using them again when I get closer to releases and I need tests to be done with the production environment.</p>
<p>Using Test Tracks just gives an experience closer to the production app, as it can be downloaded through the Play Store.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>While I'm still just 'playin' around with my prototype, I'll keep using Firebase App Distribution and TestFlight to distribute the app to the testers.</p>
<p>Once I get close to a production release, I will start testing from Google Play Test Tracks as well.</p>
<p>To automate this process, I'm using CodeMagic which build and distribute a new version on every merge to my develop branch.</p>
]]></content:encoded></item><item><title><![CDATA[Daily step counter in Flutter]]></title><description><![CDATA[On the time of writing, I'm working on a Flutter game with which I try to motivate the player to walk daily. By walking they will earn resources that they'll be able to use in the game. The more they'll walk, the more resources they can earn.
I need ...]]></description><link>https://seg.veenstra.dev/daily-step-counter-in-flutter</link><guid isPermaLink="true">https://seg.veenstra.dev/daily-step-counter-in-flutter</guid><category><![CDATA[pedometer]]></category><category><![CDATA[step counter]]></category><category><![CDATA[daily step count]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[rxdart]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Mon, 01 Jul 2024 19:49:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/p5JVzahHku0/upload/05d1c1cfbee9f5daab227fbe407d3e1d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On the time of writing, I'm working on a Flutter game with which I try to motivate the player to walk daily. By walking they will earn resources that they'll be able to use in the game. The more they'll walk, the more resources they can earn.</p>
<p>I need a way to track the step count for each day, as the progression should reset at midnight.</p>
<h1 id="heading-the-tricky-requirement">The tricky requirement</h1>
<p>Just tracking the steps might seem easy, but while building this, I ran into a case that made it a bit more tricky. At least I needed some time to get to a proper solution.</p>
<p>Because the app should show the amount of steps from <strong>the current day</strong>, I need to keep track of <strong>the current day</strong>. So when the user is actively using the app and it becomes midnight, thus a new day starts, the app should reflect this so the player is not longer able to claim rewards from the previous day.</p>
<h1 id="heading-pedometer-2">Pedometer 2</h1>
<p>I started looking on pub.dev for a plugin that I could use to track steps. There are quite some plugins build by the community and most of them seem to be forks of each other, which made it really hard to find the 'right' one.</p>
<p>Eventually I went for <a target="_blank" href="https://pub.dev/packages/pedometer_2">pedometer_2</a>, simply because I liked the documentation best.</p>
<h1 id="heading-platform-differences">Platform differences</h1>
<p>It turned out that both platforms that I wish to support (iOS and Android), do not have the same capabilities.</p>
<blockquote>
<ul>
<li>Get real time step count since a date (Stream) (Only on IOS)<br />  *<em>Alternative for Android explained under</em></li>
</ul>
</blockquote>
<p>Getting real time step count since a certain date (start of today) sounded exactly what I needed, luckily there is an explanation for an alternative on Android!</p>
<h1 id="heading-permissions">Permissions</h1>
<p>To get access to the step counter information you need to get permissions from the user. I will not cover it in this article as the documentation explains it very well <a target="_blank" href="https://pub.dev/packages/pedometer_2#permissions">here</a>.</p>
<h1 id="heading-creating-the-listener">Creating the listener</h1>
<p>Ideally we would want to use the <code>.stepCountStreamFrom(...)</code> method because it allows us to specify the period. But as the documentation clearly states, this is not supported on Android.</p>
<p>Luckily, the documentation mentions an alternative solution.</p>
<blockquote>
<ul>
<li><strong><em>(Android alternative)*</em></strong>Use a combination of the*<code>getStepCount</code> and <code>stepCountStream</code>. Example in the <code>Example App</code></li>
</ul>
</blockquote>
<p>The <code>.getStepCount</code> used in the example app, supports a <code>from</code> and <code>to</code> argument, on both Android and iOS.</p>
<p>The <code>.stepCountStream</code> used in the example streams the amount of steps registered since the last system boot. When the user is actively walking, the stream will continuously emit new values.</p>
<p>By combining the two, and sprinkling a bit of <a target="_blank" href="https://pub.dev/packages/rxdart">rxdart</a> magic, we can create the stream that we need.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> stepCounterService = StepCounterService();

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StepCounterService</span> </span>{
    StepCounterService() {
        <span class="hljs-comment">// We listen to stepCountStream so we get new data</span>
        <span class="hljs-comment">// when the user is walking.</span>
        <span class="hljs-comment">// But we don't use the value as it gives us the total</span>
        <span class="hljs-comment">// steps since last device boot</span>
        Pedometer().stepCountStream.listen((totalSteps) <span class="hljs-keyword">async</span> {
            <span class="hljs-comment">// Now we determine the periode for which we wish to</span>
            <span class="hljs-comment">// get the amount of steps, which is today.</span>
            <span class="hljs-keyword">final</span> now = <span class="hljs-built_in">DateTime</span>.now();
            <span class="hljs-keyword">final</span> startOfDay = <span class="hljs-built_in">DateTime</span>(now.year, now.month, now.day);
            <span class="hljs-keyword">final</span> endOfDay = startOfDay.add(<span class="hljs-built_in">Duration</span>(days: <span class="hljs-number">1</span>));

            <span class="hljs-comment">// Now we use this information to use the getStepCount</span>
            <span class="hljs-comment">// method to get the steps for today.</span>
            <span class="hljs-keyword">final</span> stepCount = Pedometer().getStepCount(
                from: startOfDay,
                end: endOfDay,
            );

            <span class="hljs-comment">// And then we add this stepCount to the</span>
            <span class="hljs-comment">// _stepCounterController.</span>
            _stepCounterController.add(stepCount);
        });
    }

    <span class="hljs-comment">// We use a BehaviorSubject because it holds the last value</span>
    <span class="hljs-comment">// and emits it whenever a new listener subscribes.</span>
    <span class="hljs-comment">// This makes a Stream more useful to be used in the UI.</span>
    <span class="hljs-keyword">final</span> _stepCounterController = BehaviorSubject.seeded(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">final</span> Stream&lt;<span class="hljs-built_in">int</span>&gt; <span class="hljs-keyword">get</span> todaysStepCount =&gt; _stepCounterController;
}
</code></pre>
<p>Great, we now get the amount of steps for today which is updated whenever the player walks.</p>
<p>However...</p>
<h1 id="heading-its-a-new-dawn-its-a-new-day">It's a new dawn, it's a new day</h1>
<p>In the above code, we get the step count of today. But what if it is no longer today?</p>
<p>Our service initializes the stream when the app starts. It uses the current day, but when we reach midnight, we want the step count of the next day.</p>
<p>In other words, the periode should be dynamic.</p>
<h1 id="heading-more-streams">More streams!</h1>
<p>The rxdart package has some very useful utilities that we can use.</p>
<p>Let's add a dynamic way of managing the date to our service.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> stepCounterService = StepCounterService();

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StepCounterService</span> </span>{
    StepCounterService() {
        <span class="hljs-comment">// We use CombineLatestStream, also from rxdart, to combine</span>
        <span class="hljs-comment">// two streams. The listener will be called whenever</span>
        <span class="hljs-comment">// one of the streams changes.</span>
        CombineLatestStream.combine2(
            Pedometer().stepCountStream(),
            _todayController,
            (totalSteps, date) =&gt; date, <span class="hljs-comment">// We only need the date</span>
        ).listen((date) <span class="hljs-keyword">async</span> {
            <span class="hljs-keyword">final</span> startOfDay = <span class="hljs-built_in">DateTime</span>(date.year, date.month, date.day);
            <span class="hljs-keyword">final</span> endOfDay = startOfDay.add(<span class="hljs-built_in">Duration</span>(days: <span class="hljs-number">1</span>));

            <span class="hljs-keyword">final</span> stepCount = Pedometer().getStepCount(
                from: startOfDay,
                end: endOfDay,
            );
            _stepCounterController.add(stepCount);
        });
    }

    <span class="hljs-comment">// We can use this to force the date to update.</span>
    <span class="hljs-keyword">void</span> updateDate() =&gt; _todayController.add(<span class="hljs-built_in">DateTime</span>.now());

    <span class="hljs-keyword">final</span> _stepCounterController = BehaviorSubject.seeded(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">final</span> Stream&lt;<span class="hljs-built_in">int</span>&gt; <span class="hljs-keyword">get</span> todaysStepCount =&gt; _stepCounterController;

    <span class="hljs-comment">// We use this to keep track of the day</span>
    <span class="hljs-keyword">final</span> _todayController = BehaviorSubject.seeded(<span class="hljs-built_in">DateTime</span>.now());
}
</code></pre>
<p>We've now made the date for which the step count is requested, dynamic. By using <code>stepCounterService.updateDate()</code>, we're able to force the service to use the current date.</p>
<h1 id="heading-updating-the-date">Updating the date</h1>
<p>Now that we have a way to update the date, we need to determine when to call it.</p>
<p>When the app is opened, it will already initialize with the current date. So we should call our method when we reach the next day. For this we could use a background task, or even easier, a <code>Timer</code> .</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> main() {
    <span class="hljs-comment">// Schedule the first</span>
    _scheduleUpdate();
}

<span class="hljs-keyword">void</span> _scheduleUpdate() {
    <span class="hljs-comment">// Force the service to update the date.</span>
    stepCounterService.forceUpdate();

    <span class="hljs-comment">// Calculate how long it takes till tomorrow.</span>
    <span class="hljs-keyword">final</span> now = <span class="hljs-built_in">DateTime</span>.now();
    <span class="hljs-keyword">final</span> tomorrow = now.add(<span class="hljs-built_in">Duration</span>(days: <span class="hljs-number">1</span>));
    <span class="hljs-keyword">final</span> startOfTomorrow = <span class="hljs-built_in">DateTime</span>(
        tomorrow.year,
        tomorrow.month,
        tomorrow.day,
    );
    <span class="hljs-keyword">final</span> tillMidnight = now.difference(startOfTomorrow);

    <span class="hljs-comment">// Schedule this function again at the start of tomorrow.</span>
    Timer(tillMidnight, _scheduleUpdate);
}
</code></pre>
<p>Now the <code>_scheduleUpdate</code> function will be called once at app startup, and then it will schedule itself again for every next midnight.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>With some rxdart magic and some well <em>timed</em> (pun intended) function calls, we're able to overcome the platform limitations on Android and are able to get the daily step count as a stream.</p>
]]></content:encoded></item><item><title><![CDATA[Side-Project Tips for 2024]]></title><description><![CDATA[In the summer of 2022 I started working on my side-project, Scavenger Hunt Plus, an app for creating, sharing and playing Scavenger Hunts, which I plan to release early 2024.
During my time working on this project, I've learned a lot about how to 'ma...]]></description><link>https://seg.veenstra.dev/side-project-tips-for-2024</link><guid isPermaLink="true">https://seg.veenstra.dev/side-project-tips-for-2024</guid><category><![CDATA[side project]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[tips]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sat, 16 Dec 2023 14:34:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702729149883/357b1030-ddf4-461f-895f-fe44ae2f36ca.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the summer of 2022 I started working on my side-project, <a target="_blank" href="https://twitter.com/hashtag/ScavengerHuntPlus">Scavenger Hunt Plus</a>, an app for creating, sharing and playing Scavenger Hunts, which I plan to release early 2024.</p>
<p>During my time working on this project, I've learned a lot about how to 'manage' my side-project to help me to stay motivated up to a point that I'm close to releasing it.</p>
<p>In this article I will share these learnings.</p>
<h1 id="heading-1-focus">1. Focus</h1>
<p>Focus on shipping your app. Focus should be your main priority and you will see it being mentioned multiple times in this article. Every decision you make should raise the question: "How would this impact the shipping of my project?".</p>
<h1 id="heading-2-stick-to-what-you-already-know">2. Stick to what you already know</h1>
<p>It might be tempting to try some new tech, languages, tools or frameworks, but if your goal is to ship your project, you should avoid it.</p>
<p>Learning something means you will make mistakes, and mistakes cost time. Something we don't have enough of is time.</p>
<p>If you want to make progress, try to stick to the things you are already familiar with, preferably the things you use in a day-to-day basis, like during your day-job for example.</p>
<h1 id="heading-3-the-most-viable-product">3. The Most Viable Product</h1>
<p>Try to trim down your idea to the most minimal version of itself by removing features that can be added later with updates, till all you've left with is the core of your idea.</p>
<p>Focus on building that first.</p>
<p>When I started working on Scavenger Hunt Plus, the idea was to have an app that supports all kinds of geographical games, show analytical data, has different tiers, etc. I trimmed it all down to a 'simple' Scavenger Hunt app for the MVP.</p>
<h1 id="heading-4-plan-your-time">4. Plan your time</h1>
<p>Time is precious and we never have enough of it. When I started out, my intention was to work nights and weekends on my side-project.</p>
<p>Soon I realized that most of the time I planned in the weekend would be interrupted by family-related things. This would often leave me bummed out and moody because I was already mentally prepared to work on it. This would have negative impact on me and my family.</p>
<p>I decided that weekends are for family, friends, etc. So I only work on my side-project in the evenings, right after the kids are in bed. I will spend an hour or two which often allows me to do at least one 'task' while also giving me some time left to unwind before going to bed.</p>
<h1 id="heading-5-managing-your-work">5. Managing your work</h1>
<p>It sounds boring, but for me this was a great improvement on my productivity and motivation.</p>
<p>Create a list of todo's, or even better, a <a target="_blank" href="https://trello.com/">Trello</a> board. Don't worry about it being incomplete. Just put down everything that comes to mind that has to be done for your project.</p>
<p>Start out by writing 'big' tasks, like "Create a login screen", avoid going into detail too early. Once you get to a task, you start breaking it up in smaller tasks, preferably small enough that they fit inside your 'work window'.</p>
<p>For example, I can spend 1 or maybe 2 hours in the evening to work on a task. So my Tasks should be short enough to be completed in this time frame. Why?, well for the following two reasons:</p>
<ol>
<li><p><strong>Easy to start</strong><br /> Sometimes, after a long day's work, you might not feel like working on your project. If your todo's are big, it will be even harder to start.<br /> But if you have 1 hour or even half-an-hour tasks, it will make it easier to tell yourself, "Well let's at least do this small one...", that's progress baby!</p>
</li>
<li><p><strong>A sense of accomplishment.</strong><br /> Finishing a task works motivating! Just moving that task from <em>doing</em> to <em>done</em> feels awesome. I've had times that I felt tired and unmotivated to start working on my project. But after I've completed that 'easy half-an-hour task' I got kick-started with motivation and I almost always continue picking up the next one.</p>
</li>
</ol>
<h1 id="heading-6-parking-work">6. Parking work</h1>
<p>Since we're now using a todo list, Trello board, or something similar to manage our work, it will become way easier to park incidental work.</p>
<p>Let's say you're working on a task. Then you realize this task need a new service of some kind. Instead of immediately starting working on this service, just create an empty class, with some hardcoded values. Then add a task to your list for later.</p>
<p>Now you can put it out of your mind a continue working on the task you had set out to do.</p>
<h1 id="heading-7-share-with-friendsfamilycommunity">7. Share with friends/family/community</h1>
<p>You know what is a great motivator? Compliments!</p>
<p>Share your work with the community, potential users, friends, family whatever!<br />It's a great way to get compliments as well as valuable feedback for your project.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>These are my 7 tips on how to stay motivated for your side-project. I hope they will work for you too! Let me know if they do!</p>
<p>Happy programming for 2024!!!</p>
]]></content:encoded></item><item><title><![CDATA[Creating a reactive TextField in Flutter]]></title><description><![CDATA[In this article, I will show you how you can very easily create a reactive TextField in Flutter.
Motivation
A few months ago, while working on a client project, I was building multiple pages in a row which all contained some kind of form, like a logi...]]></description><link>https://seg.veenstra.dev/creating-a-reactive-textfield-in-flutter</link><guid isPermaLink="true">https://seg.veenstra.dev/creating-a-reactive-textfield-in-flutter</guid><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[Reactive Programming]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sun, 09 Jul 2023 11:23:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/vpOeXr5wmR4/upload/cf17931393eaff820349752d9961e10d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I will show you how you can very easily create a reactive TextField in Flutter.</p>
<h1 id="heading-motivation">Motivation</h1>
<p>A few months ago, while working on a client project, I was building multiple pages in a row which all contained some kind of form, like a login-, registration- and personal details form.</p>
<p>I soon got annoyed with all sorts of things related to building these forms...</p>
<ol>
<li><p>Each page had to be Stateful (<code>StatefulWidget</code>).</p>
</li>
<li><p>All the <code>TextEditingController</code>s and <code>FocusNode</code>s had to be created manually.</p>
</li>
<li><p>We had to think about disposing these <code>TextEditingController</code>s and <code>FocusNode</code>s.</p>
</li>
<li><p>The <code>TextEditingController</code>s had to be 'connected' with our state management.</p>
</li>
<li><p>Validation ended up in several places, depending on if the validation was client- or server-sided.</p>
</li>
</ol>
<p>All of this resulted in a painful amount of boilerplate code.</p>
<p>And I was not having it...</p>
<h1 id="heading-the-slider-widget">The Slider widget</h1>
<p>A bit later, I had to create a form for my <a target="_blank" href="https://twitter.com/hashtag/ScavengerHuntApp">Scavenger Hunt app</a> pet project. This form had only one <code>TextField</code>, but it also contained a <code>Slider</code>.</p>
<p>I realized the <code>Slider</code> was way simpler to work with. Its most important properties are: <code>value</code> and <code>onChanged</code>.<br />With <code>value</code> you tell the <code>Slider</code> its current value, and with <code>onChanged</code> you receive the value the user changed the slider to. No hassle of dealing with controllers.</p>
<p>So I was thinking, what if I could make a TextField that works the same way?</p>
<h1 id="heading-a-reactive-textfield-was-born">A Reactive TextField was born</h1>
<p>After playing around with some ideas, I ended up with something that can be used like this:</p>
<pre><code class="lang-dart">ReactiveTextField(
  text: state.counter.toString(),
  onChange: context.read&lt;CounterCubit&gt;().setValue,
  error: state.error,
),
</code></pre>
<p>As you can see, there are no controllers to deal with. Its properties can be directly connected with the state management solution of your choosing.</p>
<p>Below you can see its implementation.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReactiveTextField</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> ReactiveTextField({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.text,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onChange,
    <span class="hljs-keyword">this</span>.error,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> text;
  <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-built_in">Function</span>(<span class="hljs-built_in">String</span> text) onChange;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String?</span> error;

  <span class="hljs-meta">@override</span>
  State&lt;ReactiveTextField&gt; createState() =&gt; _ReactiveTextFieldState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_ReactiveTextFieldState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">ReactiveTextField</span>&gt; </span>{
  <span class="hljs-comment">// ReactiveTextField will hold onto its own TextEditingController and Focusnode,</span>
  <span class="hljs-comment">// which you otherwise had to create and maintain yourself.</span>
  <span class="hljs-keyword">final</span> _controller = TextEditingController();
  <span class="hljs-keyword">final</span> _focusNode = FocusNode();

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();

    _controller.text = widget.text;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didUpdateWidget(<span class="hljs-keyword">covariant</span> ReactiveTextField oldWidget) {
    <span class="hljs-keyword">super</span>.didUpdateWidget(oldWidget);
    <span class="hljs-comment">// This is an important part!</span>
    <span class="hljs-comment">// Without this check, there will be an infinite loop!</span>
    <span class="hljs-keyword">if</span> (widget.text != _controller.text) {
      _controller.text = widget.text;
     <span class="hljs-comment">// We remove focus, because the value has been set</span>
     <span class="hljs-comment">// from a different source.</span>
      _focusNode.unfocus();
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _controller.dispose();
    _focusNode.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-comment">// ReactiveTextField simply wraps a 'normal' TextField.</span>
    <span class="hljs-keyword">return</span> TextField(
      controller: _controller,
      focusNode: _focusNode,
      onChanged: widget.onChange,
      decoration: InputDecoration(
        errorText: widget.error,
      ),
    );
  }
}
</code></pre>
<p>As you can see, it is very simple. The most 'tricky' part here is the <code>didUpdateWidget</code> section, which is needed to prevent an infinite update loop from happening.</p>
<p>This <code>ReactiveTextField</code> widget will manage its controller so the parent can remain a <code>StatelessWidget</code>. All you have to do as the user is set the current value and listen to the updates, THAT IS IT!</p>
<h1 id="heading-full-example">Full example</h1>
<p><a target="_blank" href="https://dartpad.dev/?id=614b3f4ad618e39ee8cb3f20ab9c0801">Check out this dartpad link for a full example!</a></p>
<p>Let me know what you think!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 11: First Feature Completed]]></title><description><![CDATA[In the previous devblog I talked about decisions I made about the project that was supposed to improve progression.
Well, we're just three weeks further, and I've made way more progress than I thought!
I've even "completed" my first feature!
Scavenge...]]></description><link>https://seg.veenstra.dev/devblog-11-first-feature-completed</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-11-first-feature-completed</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[scavenger hunt]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Firebase]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Tue, 06 Jun 2023 18:58:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/fwcnv0WRsTY/upload/0a4d2b99ee034a6b34f7bd25c06a4b86.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://fluttergamedev.com/devblog-10-a-shift-of-focus">previous devblog</a> I talked about decisions I made about the project that was supposed to improve progression.</p>
<p>Well, we're just three weeks further, and I've made way more progress than I thought!</p>
<p>I've even "completed" my first feature!</p>
<h1 id="heading-scavenger-hunt-drafts">Scavenger Hunt Drafts</h1>
<p>When a user is still working on a Scavenger Hunt, they can save them to continue working on it at a later time. These Scavenger Hunts are saved as <em>drafts</em>.</p>
<p>I was able to "complete" the functionality to manage (create, update, delete) these drafts. I put "complete" in quotes because even though you can do everything, it still needs a little cleanup. More about that later.</p>
<p>Let's take a look at this feature in more detail:</p>
<h2 id="heading-drafts-overview">Drafts overview</h2>
<p>When you currently log in to the app, you will land on the 'drafts overview'.</p>
<p>Here you will see the Scavenger Hunts that you've created but have not yet published:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686123906770/5bcc330a-45a9-4321-b69e-5a4fb876214f.jpeg" alt /></p>
<p>You can use the FAB (Floating Action Button) to create a new draft or select an existing draft to edit it.</p>
<p>It still looks a little empty, but that will eventually change.</p>
<h2 id="heading-editingcreating-drafts">Editing/Creating drafts</h2>
<p>When you arrive on the 'draft detail' page, you will be presented with a simple form.</p>
<p>Here you can give your Scavenger Hunt a name, a description and you can manage the <em>locations</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686123926319/5a8e3451-895d-4c0a-a87d-200554ede5cc.jpeg" alt /></p>
<p>With the '+'-button under the <em>locations</em>-header you can add new locations. When locations are present, the screen will look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686123940645/389fd1c8-fc79-4d45-be11-2ee3d807c64b.jpeg" alt /></p>
<p>This still needs to get some styling-love, but the basics are there. When you click on them you can edit them.</p>
<h2 id="heading-locations">Locations</h2>
<p>Creating/opening a location will open a full-screen modal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686123954991/3ac74534-8c3c-476b-ac74-7e1169c110df.jpeg" alt /></p>
<p>Here you can give the location a name, set a position, set the range for the position and optionally, add an assignment!</p>
<h3 id="heading-position">Position</h3>
<p>When you create a new location, the app will try to fetch your current position (using the <a target="_blank" href="https://pub.dev/packages/geolocator">geolocator package</a>).</p>
<p>The position is drawn on a Google Map (<a target="_blank" href="https://pub.dev/packages/google_maps_flutter">google_maps_flutter</a>). You can easily change the position by tapping on the map. So technically you can create a Scavenger Hunt without leaving the house 😅.</p>
<h3 id="heading-range">Range</h3>
<p>The range will determine within what range the players will complete the position or unlock the assignment in case the location has one.</p>
<p>Adjusting the range-slider will update the map so the circle will represent the actual area.</p>
<h3 id="heading-assignment">Assignment</h3>
<p>Locations can have assignments that need to be completed to unlock the next location. For the first release, the only assignment option will be a multiple-choice question, but more will follow.</p>
<p>An assignment can be added by pressing the "+"-button. This will allow you to configure a multiple-choice question by adding and removing answers and selecting the correct one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686123969399/5bee672e-f7ce-4cf1-9613-efb40efe87cb.jpeg" alt /></p>
<p>This concludes the drafts-feature!</p>
<h1 id="heading-firebase">Firebase</h1>
<p>And that's not all! I've also replaced the in-memory repository and implemented Cloud Firestore to save the data in the cloud.</p>
<p>It has been a while since I've used firestore so I had to do a little digging into the documentation, but everything considered, using firestore was a breeze.</p>
<p>I had to do some tweaking to reduce the number of reads, and I'm still not sure if I'm fully satisfied, but for now it will do.</p>
<p>The cool thing is that firestore supports offline out of the box, although I did have to alter my code a little bit to make it work as intended since I found out that updating/setting data will only complete the <code>future</code> when the data has been synced to the cloud.</p>
<h1 id="heading-the-big-cleanup">The big cleanup</h1>
<p>The feature is fully functional but I still need to cleanup some things. Although the itch to continue adding features is enormous, I feel like I have to do this first.</p>
<p>So what do I mean by 'cleanup'? There are still a bunch of things in the UI that can be improved upon, but also the technical side can use some love.</p>
<p>For example, the loading states in the app hardly show after I switched to firestore, mainly because the SDK will update the data very rapidly, and synchronizing the data happens in the background. I can probably get rid of some of loading-states.</p>
<p>Also, some of the views are just not pretty, the drafts-overview can hold a bit more information, as well as the locations list.</p>
<p>Buttons can be pressed to save data, even though nothing changed, and data can be deleted without warning, or you can leave a page without saving. This needs to be addressed.</p>
<p>I probably also want to start integrating localization and apply hints to TextFields and give it a more finished vibe.</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>Well, I kind of explained that in the previous part. I will clean up this feature before continuing to the next, which will be adding the ability to try-out/play-test drafts!</p>
<p>Stay tuned!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 10: A shift of Focus]]></title><description><![CDATA[It's been a while since I create a new article for this devblog. In the last devlog article that I posted at the end of March, I talked about the focus of the project.
This new focus was an improvement, but it still wasn't enough. I was still not get...]]></description><link>https://seg.veenstra.dev/devblog-10-a-shift-of-focus</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-10-a-shift-of-focus</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[scavenger hunt]]></category><category><![CDATA[Devblog]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Thu, 18 May 2023 19:48:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7KLa-xLbSXA/upload/8c7c77aacccb536a6fa4675cf010a7c9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's been a while since I create a new article for this devblog. In <a target="_blank" href="https://fluttergamedev.com/devblog-9-can-we-still-be-friends">the last devlog article that I posted at the end of March</a>, I talked about the focus of the project.</p>
<p>This new focus was an improvement, but it still wasn't enough. I was still not getting any actual work done for this project. Therefore I've made yet another, drastic decision.</p>
<h1 id="heading-focus-on-building-the-app">Focus on building the app</h1>
<p>Last time I said I was still going to use Etos (my state management package) so I can develop it alongside this project.</p>
<p>This turned out to be tougher than I thought. Whenever I ran into a problem to solve I got sidetracked from the actual project and I would be spending time thinking about that particular issue, thus not making any progress.</p>
<p>For this reason, I decided a few weeks ago, that I would drop the 'learning' part from the project. This means I will focus on the development of the app.</p>
<p>I will achieve this by using things I already have experience with, instead of using new things.</p>
<h2 id="heading-bye-etos-hello-bloc">Bye Etos, Hello Bloc</h2>
<p>I've decided to replace Etos with (flutter_)bloc. I've worked with bloc professionally since the start of 2021. That same year I also migrated my very first Flutter app (Scrap Bandit) to bloc.</p>
<p>Because I've experience with it, it takes away the uncertainties I have with Etos, thus speeding up development.</p>
<h2 id="heading-firebase">Firebase</h2>
<p>In the previous article, I stated that I would be using Firebase as the backend for my application. This is still the plan, for similar reasons as mentioned above.</p>
<p>I've released apps using Firebase, I have very good experiences with it thus why not use it again?</p>
<h1 id="heading-the-projects-progress">The project's progress</h1>
<p>It's been about two or three weeks since I decided to fully focus on building the app, and I can say it already paid off.</p>
<p>In these weeks I've replaced Etos with Bloc and was able to add new features. Because you probably have no idea how far I was, let's take this moment to just show the current state of the application:</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/uafq6ZLoc1A">https://youtu.be/uafq6ZLoc1A</a></div>
<p> </p>
<p>So what do you see in the video?</p>
<h2 id="heading-drafts">Drafts</h2>
<p>I started with the functionality for creating <em>drafts</em>. Drafts are Scavenger Hunts that are still being worked on.</p>
<p>The idea is that, while a Scavenger Hunt is in draft, the user is free to make any changes to it as pleased. Once the user is satisfied and has tested the Scavenger Hunt, they can publish it.</p>
<p>In the video, you see an overview of Drafts, which can be edited by clicking on the FAB (Floating Action Button) to add a new draft or by clicking on one of the existing ones to edit them.</p>
<p>On the draft detail page you can add change the name and description of the Scavenger Hunt. At the bottom, you can add <em>locations,</em> or edit them when you already have one or more locations.</p>
<p>If you're editing an existing Scavenger Hunt, you can also delete it here.</p>
<h2 id="heading-locations">Locations</h2>
<p>A <em>location</em> is a point in the Scavenger Hunt that the players should try to find. When they get close enough they will unlock an assignment which will just be answering a question at first.</p>
<p>When you want to add or edit a location you can click the plus-button on the bottom of the detail page, or click on an existing location.</p>
<p>This will open a bottom sheet that allows you to set a name and a range (for now).</p>
<p>When you are editing a location, you will also get a delete-button.</p>
<h2 id="heading-memory-storage">Memory Storage</h2>
<p>Because it has been a while since I used Firebase I decided to first implement my repositories with memory storage. It should be relatively easy to replace them with Firebase soon.</p>
<h2 id="heading-theming">Theming</h2>
<p>I have no idea if the current style will be the final one, but it looks nicer than the default one, so I'll go with it for now.</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>I started keeping track of the features again in Trello. This helped me in my previous projects and it seems to work for this one as well.</p>
<p>The next goal is adding geolocation and Google maps to make it possible to set the position for the locations. I want to show a small map with the radius of that location.</p>
<p>I'm planning to make smaller iterations again on this devblog. I think that should be possible since I'm making more progress now.</p>
<p>Until the next!</p>
]]></content:encoded></item><item><title><![CDATA[Why theming in Flutter is confusing (to me) Part 2]]></title><description><![CDATA[In my previous article, I tried to explain why theming in Flutter confuses me.I ended the article by saying the way ThemeData is implemented doesn't make sense to me.
A few weeks have passed and I've been playing with this in my head, and within my p...]]></description><link>https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me-part-2</link><guid isPermaLink="true">https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me-part-2</guid><category><![CDATA[Flutter]]></category><category><![CDATA[theming]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sun, 23 Apr 2023 13:05:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/l3N9Q27zULw/upload/86de8842c1515e8c3fbb316ddb1f8e7b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a target="_blank" href="https://fluttergamedev.com/why-theming-in-flutter-is-confusing-to-me">previous article</a>, I tried to explain why theming in Flutter confuses me.<br />I ended the article by saying the way <code>ThemeData</code> is implemented doesn't make sense to me.</p>
<p>A few weeks have passed and I've been playing with this in my head, and within my pet project. I had some new insights that I would like to share with you in this article.</p>
<h1 id="heading-but-first-we-need-to-make-things-clear">But first, we need to make things clear</h1>
<p>During the discussion, people were saying to me that <em>Material Design</em> is a good design system and it's not confusing to them.</p>
<p>And I agree that Material Design is a good system. It's well-documented and covers all varieties of devices and screen sizes.</p>
<p>So just to be clear, this article is not about criticizing Material Design. It's about how theming is implemented in Flutter.</p>
<p>The other thing that has been mentioned is that <code>ThemeData</code> is capable of implementing any design. This is also not something that I'm claiming Flutter is not capable of. But what we have to do to achieve it, is confusing to me.</p>
<h1 id="heading-new-insights">New insights</h1>
<p>In the last couple of weeks I've played around with theming in my pet project and I also started a new project at work.</p>
<p>Thinking about theming in both of these projects led me to new thoughts that might explain why it is confusing me.</p>
<h2 id="heading-property-overload-part-2">Property overload (part 2)</h2>
<p>In the previous article, I also talked about <a target="_blank" href="https://fluttergamedev.com/why-theming-in-flutter-is-confusing-to-me#heading-property-overload">property overload</a>. I talked about all these properties you have available to you in the <code>ThemeData</code> object, even though you might only need 1% of them.</p>
<p>People commented that why this is even a problem since I could "just not use them".</p>
<p>So I started thinking about this, and why it bothers me as much as it does. And I found the reason, which I will explain to you here.</p>
<h3 id="heading-abstractions">Abstractions</h3>
<p>First, we will have to talk about <em>abstractions</em>. Using abstractions is a common practice for us software developers.</p>
<p>We often use abstractions to make other people's code more our own and more specific to our needs.</p>
<p>Let's say we're building an app for our local swimming pool and they wish to show the weather forecast for the upcoming week.</p>
<p>We might want to use some 3rd party service like <a target="_blank" href="https://openweathermap.org/">https://openweathermap.org/</a>.</p>
<p>OpenWeather gives us a wide variety of endpoints and options to get weather data. But, for our use case, we only need the forecast for the upcoming week. So therefore we can make an abstraction of this service.</p>
<p>We could end up with a <code>WeatherRepository</code>. This repository only needs a single function, <code>getWeekForecast()</code>. It will call the OpenWeather endpoint for a forecast. The location can be hardcoded since it's always for the same location, which is the one of the swimming pool.</p>
<p>We have now made an abstraction of the OpenWeather API which is easy for us to use in our application.</p>
<h3 id="heading-always-themedata">Always ThemeData</h3>
<p>In the example above I've described a common practice in software development about using abstractions to make other people's code our own.</p>
<p>So if we do it for other things, why not for theming?</p>
<p>Well, because you can't. Theming in Flutter is implemented in such a way that when you grab the <code>Theme</code> from <code>context</code>, it will always return a <code>ThemeData</code> object. And as we know, <code>ThemeData</code> contains all these fields, even though we don't need and use them.</p>
<p>In the previous article, I used the counter app as an example. Wouldn't it make the theme more clear if we could abstract away all the properties we don't use, and maybe give our texts a more describing name than <em>bodyMedium</em> and <em>titleMedium</em> in the process?</p>
<h2 id="heading-why-themedata">Why ThemeData</h2>
<p>I've also thought about the existence of <code>ThemeData</code>. Why would they use it like this, and what is its purpose?</p>
<h3 id="heading-global-theming">Global Theming</h3>
<p>I realized that this is the only way to globally style the Material Widgets without wrapping them in a custom widget.</p>
<p>For example, it prevents us from having to put the same <code>InputDecoration</code> on every <code>TextField</code>.</p>
<h3 id="heading-limitations">Limitations</h3>
<p><code>ThemeData</code> is designed against the Material Design specifications.</p>
<p>And even though this can come across as flexible, it also is limited by it. You will especially experience this when you have to implement UI that is not (fully) based on Material Design.</p>
<p>An example. Material Design defines two <em>types</em> of text fields:</p>
<p><img src="https://lh3.googleusercontent.com/FK0BIH5f5k-uZK8M6FF0Y8XDRKoOLHlOZQwkdmOHF9g9j73SkbCCLHL1WAlJqw79qZdSBKQSAJM_n41bKnyS0Fe_NljhSm3sG3QFXfFr6peSCg=s0" alt="Side by side view two types of text fields, filled and outlined" /></p>
<p>The first is called <em>Filled</em>, and the second is called <em>Outlined</em>.</p>
<p>So what if we have the following design:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682250251541/0d885007-b86f-4658-a5c2-7ccb0b26e245.png" alt class="image--center mx-auto" /></p>
<p>With <code>ThemeData</code> you can go a long way, but to get that label above the <code>TextField</code>, you will have to create a custom widget to make it easily reusable since we don't want to place the <code>Text</code> widget above every <code>TextField</code> manually, right?</p>
<p>Earlier, I mentioned that <code>ThemeData</code> was necessary to globally style Material Widgets without having to wrap them. But since we just created a custom component, let's call it <code>MyTextField</code>, we've just created a wrapper.</p>
<p>So now we could also define the styling directly in the component. And because we reuse the component, we are globally theming our app at the same time. There is no need for an external way of defining a style for this component anymore.</p>
<h3 id="heading-dark-and-light-mode">Dark and light mode</h3>
<p>Okay, the above statement was not completely true. Most modern applications support both dark and light modes. So <code>MyTextField</code> should change according to the active mode.</p>
<p>To support dark and light modes in Flutter you define two <code>ThemeData</code> objects. So technically, you would be able to create two completely different Themes that don't even look like the same app because it gives you access to every material property.</p>
<p>In practice, this is hardly the case. More often it's just a difference in colors. A white background becomes dark grey and the black text becomes white.</p>
<p>So what we need is a theme that just holds the values that change between themes, nothing more.</p>
<h3 id="heading-theme-vs-component-properties">Theme vs component properties</h3>
<p>So what if we would style our <code>MyTextField</code> directly inside the class? This makes it easier to maintain because you don't have to deal with two different <code>ThemeData</code> classes. When the design changes, it most likely should be changed in both light and dark themes anyway.</p>
<p>Now we only have to find a way to allow <code>MyTextField</code> to adapt to light and dark mode changes.</p>
<p>After taking another quick look at the design, the only properties that need to change are:</p>
<ol>
<li><p>The color of the label</p>
</li>
<li><p>The color of the Input Text</p>
</li>
<li><p>The background color of the text field</p>
</li>
</ol>
<p>So what if we could have a theme that just contains these kinds of properties?</p>
<blockquote>
<p>NOTE: We could also check for the mode in the component but this is a bit ... mehh, IMO.</p>
<p>It would also limit to only dark and light modes while you might also want to support a special theme for people suffering from colorblindness later on.</p>
</blockquote>
<h1 id="heading-explicit-theming">Explicit theming</h1>
<p>In the conclusion of the previous article, I dropped the term 'explicit theming'.</p>
<p>This is something I'm currently experimenting with in my pet project. It applies the things I mentioned in this article.</p>
<p>What I'm currently doing is breaking my app up into custom reusable components. These will ensure consistency across the app because they will contain most of the styling.</p>
<p>For the dark/light mode configurations, I'm creating a custom Theme class that only holds the properties that change when switching modes. In a sense, it is an abstraction of <code>ThemeData</code> that only gives me access to properties I use.</p>
<p>I'm still playing around with this but so far I like where it is going. If I'm a bit further with the development of my pet project and I'm still happy with it, I will dedicate an article to it.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>I have a bit more understanding of why Theming in Flutter is confusing to me. I also have a better understanding of why ThemeData is 'needed'.</p>
<p>I'm still looking for a way of theming that fits me better and therefore I'm experimenting with something I'd like to call: 'explicit theming'.</p>
]]></content:encoded></item><item><title><![CDATA[Why theming in Flutter is confusing (to me)]]></title><description><![CDATA[About two weeks ago I posted the following tweet:
https://twitter.com/SEGVeenstra/status/1639904262837739520
 
Reactions to this tweet were pretty divided. Some people agreed with me, and some people thought the theming in Flutter was just fine.
In t...]]></description><link>https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me</link><guid isPermaLink="true">https://seg.veenstra.dev/why-theming-in-flutter-is-confusing-to-me</guid><category><![CDATA[theming]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sat, 08 Apr 2023 18:21:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/l3N9Q27zULw/upload/505e819fe240fa1ee8010073f9a9550a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>About two weeks ago I posted the following tweet:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/SEGVeenstra/status/1639904262837739520">https://twitter.com/SEGVeenstra/status/1639904262837739520</a></div>
<p> </p>
<p>Reactions to this tweet were pretty divided. Some people agreed with me, and some people thought the theming in Flutter was just fine.</p>
<p>In this article, I will try to explain exactly why I think theming in Flutter is confusing.</p>
<h1 id="heading-what-is-a-theme">What is a 'theme'?</h1>
<p>If I look up the definition online, this is one of the first I find:</p>
<blockquote>
<p>A design theme is simply <strong>the overall look and feel of a website</strong>. The purpose of a design theme is to create unity across web pages through the consistent use of a color palette, fonts, and layout patterns</p>
<p>- Uizard Technologies</p>
</blockquote>
<p>Using a 'Theme' in your app creates unity and consistency across your application. When an application uses the same theme across its different screens, it lets the user know they are still inside the same application.</p>
<h1 id="heading-theming-your-flutter-app">Theming your Flutter app</h1>
<p>Flutter allows you to 'style' elements in your app through the use of <code>ThemeData</code> objects which are provided by the <code>Theme</code> widget. All the Material Widgets in Flutter reference the <code>ThemeData</code> to cherry-pick its properties to be used as their <em>default</em> styling.</p>
<p>I say <em>default</em> because the Material Widgets also allow you to <em>override</em> the style of the component themselves.</p>
<h2 id="heading-what-style-is-used-where">What style is used where?</h2>
<p>How <code>ThemeData</code> is used by the Material Widgets is the first thing that confuses me. Material Widgets sometimes access different properties from <code>ThemeData</code>, depending on whether they have been supplied or not.</p>
<p>Let's take the <code>AppBar</code> Widget as an example.<br />If we want to change the text style of the title, we have several options.</p>
<ol>
<li><p>We can set the <code>titleTextStyle</code> property on the widget itself.</p>
</li>
<li><p>If that is <code>null</code>, it will try to get the style from <code>ThemeData.appBarTheme.titleTextstyle</code>.</p>
</li>
<li><p>If that is also <code>null</code>, it will get a more generic text style from <code>ThemeData</code>, <code>ThemeData.textStyles.titleLarge</code>.<br /> But, instead of using that style's color, it will use the <code>AppBar.foregroundColor</code> property.</p>
</li>
<li><p>If the <code>AppBar.foregroundColor</code> property is <code>null</code>, it will try to use the <code>ThemeData.appBarTheme.foregroundColor</code>.</p>
</li>
<li><p>However, if that is also <code>null</code>, it will use <code>ThemeData.colorScheme.onPrimary</code> for the color. Unless the theme's <code>Brightness</code> is set to <code>Brightness.dark</code>, because then it will use <code>ThemeData.colorScheme.onSurface</code>.</p>
</li>
</ol>
<p>This is just for styling one part (the title) of one component (the AppBar).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680963077142/88277e00-a641-4f83-a55b-e54fc4078b90.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-which-style-to-use">Which style to use?</h2>
<p>The next things that confuse me are the more generic style properties in <code>ThemeData</code>.</p>
<p><code>ThemeData</code> contains a set of generic <code>TextStyle</code>s located in <code>ThemeData.textTheme</code>. It contains a bunch of old and new Material Design-defined styles for texts. If we ignore the deprecated styles and focus on the ones defined in the latest Material Design version, we have the following set of styles:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680963594301/9f50981c-36b0-4715-a994-131dd58f0912.png" alt class="image--center mx-auto" /></p>
<p>Now I ask you, do these definitions make any sense to you? Where should they be used for?</p>
<p>Take a look at the image below. It's the well-known counter app...</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680964210927/0ab84ac4-c363-4dbd-89c2-3833569b375d.png" alt class="image--center mx-auto" /></p>
<p>Can you tell me which <code>TextStyle</code>s have been used for the number and the text above it, without looking into the code. I bet you cannot.</p>
<p>It turns out the number is using <code>ThemeData.textTheme.headlineMedium</code>, and the text above it uses <code>ThemeData.textTheme.bodyMedium</code>. But why? Why these properties, why are these 'medium' sized? Why aren't they 'small' or 'large'? Why is the number a headline?</p>
<p><code>ColorScheme</code> is very similar and has the same issues, so I won't cover them here.</p>
<h2 id="heading-theming-in-multiple-places">'Theming' in multiple places</h2>
<p>If theming is about having a consistent design across your app, then we have two options for achieving that in Flutter.</p>
<p>I've always had the feeling that Flutter pushed us toward using <code>ThemeData</code>. But sometimes we also make reusable components when <code>ThemeData</code> just isn't enough, like a type of ProductCard we use on both a Search and Favorites page.</p>
<p>By using reusable components, you technically are also 'theming'. The final result is often a mix of the component's configuration and the current <code>Theme</code>.</p>
<p>I always have to think about where which configuration should go, it just isn't clear to me.</p>
<h2 id="heading-themedatas-rigid-structure">ThemeData's rigid structure</h2>
<p>The <code>ThemeData</code> class is rigid. It is not easy to add properties by <em>extending</em> the class for example. That's because when you want to use the <code>Theme</code> in your UI, the <code>Theme.of()</code> static function will return a <code>Theme</code>, meaning you will only see the properties <code>Theme</code> originally has.</p>
<p>There are some ways to work around this but they are a pain in the butt.</p>
<p><code>ThemeData</code> also has a way to add <code>ThemeExtension</code>s (<a target="_blank" href="https://api.flutter.dev/flutter/material/ThemeExtension-class.html">docs</a>). This is just a list of classes that extend the <code>ThemeExtension</code> class which can be accessed by calling <code>.extension&lt;MyThemeType&gt;()</code> on a <code>ThemeData</code> instance.</p>
<p>My issue with this is, that if I just want to add a single <code>TextStyle</code>, let's say <code>titleExtraLarge</code>, I will have to add it as an extension. So, they will not be located in the same location as the other <code>TextStyles</code>, which again makes it confusing to work with.</p>
<h2 id="heading-property-overload">Property overload</h2>
<p><code>ThemeData</code> by default gives you access to all these properties:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680966244180/786e3152-82a2-443d-8954-aa9000977856.png" alt class="image--center mx-auto" /></p>
<p>These are just the root-level properties, I even left out the deprecated properties that are still in there.</p>
<p>Let's do an extreme example. Let's think about the counter app again. Why would we need all these properties? If we want to add a dark theme, only a few of these properties will have to change.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>I've tried to explain to you why I find theming in Flutter confusing. The way it's implemented and used across the Material Widgets doesn't make any sense to me.</p>
<p>I got a bunch of reactions with good suggestions on the tweet, and I also discussed this internally with my colleagues at <a target="_blank" href="https://pinch.nl">Pinch</a> on how to improve theming.</p>
<p>Both gave me ideas on how I want to try and handle theming differently. I'm currently running with an idea that I'd like to call "explicit theming" inside my <a target="_blank" href="https://fluttergamedev.com/series/local-explorer-devblog">pet project</a>.</p>
<p>But it's too early to write something about it, so I'll let you know how that turns out.</p>
<p>Until next time!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 9: Can we still be friends?]]></title><description><![CDATA[If you follow me on Twitter, you might already know, but I'm giving up on FlutterFlow once again. While I think FlutterFlow is an amazing tool, I also think it has a long way to go. For the last couple of weeks, I kept running into hurdles that slowe...]]></description><link>https://seg.veenstra.dev/devblog-9-can-we-still-be-friends</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-9-can-we-still-be-friends</guid><category><![CDATA[Flutter]]></category><category><![CDATA[flutterflow]]></category><category><![CDATA[scavenger hunt]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Firebase]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Thu, 30 Mar 2023 08:17:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/uTFiFYeQhlI/upload/661c110b6ac80a862d6241ea3e7be8d5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you follow me on Twitter, you might already know, but I'm giving up on FlutterFlow once again. While I think FlutterFlow is an amazing tool, I also think it has a long way to go. For the last couple of weeks, I kept running into hurdles that slowed down my progression. In this article, I will give you some of the details.</p>
<p>Because I'm still struggling to find time to work on this project I decided to tune down the scope a little and made some other project decisions to make the project (hopefully) more manageable.</p>
<h1 id="heading-why-ive-dropped-flutterflow-again">Why I've dropped FlutterFlow, again.</h1>
<p>Let me start by saying FlutterFlow is an amazing tool. The way you can create UI, with a nice graphical user interface, is something I didn't know I missed this much.</p>
<p>Creating UI is smooth and fast. Just dragging widgets into place and adjusting some properties made it a very nice experience.</p>
<p>But then the UI also had to do something, for which we trigger <em>actions</em>. FlutterFlow contains a bunch of these actions to work with Firestore, navigation, local state and whatnot. If the action you are looking for doesn't exist, you can create your own.</p>
<h2 id="heading-custom-actions">Custom actions</h2>
<p>This was the first hurdle for me. FlutterFlow is very Firestore-minded, meaning you often directly <em>connect</em> a screen with a Firestore query. But I wanted to work in the local state first without having to send write-requests on every change I made.</p>
<p>So, while there are plenty of actions to use to manipulate Firestore documents, working with the local state was lacking them. I ended up writing custom actions for them but that wasn't scalable. I had to write very specific actions for every <em>DataType</em> and every form of manipulation I wished to perform.</p>
<p>Not to mention the code editor has some weird quirks that sometimes just disable the analyzer. Also, I had to mark my actions as "not compile" because it broke the project.</p>
<h2 id="heading-separated-environments">Separated environments</h2>
<p>As a Flutter developer, I'm used to having different environments for testing and production (or more). We don't want to experiment in a production environment, now do we?</p>
<p>Unfortunately, FlutterFlow has no support for this, which made me a little frightened for the future, after the initial release. How would I handle working on new versions without risking screwing up the production environment?</p>
<h2 id="heading-testing-the-app">Testing the app</h2>
<p>One of the biggest issues was not being able to test on real devices right from the editor.</p>
<p>I'm used to hookup any device to my MacBook and deploying my Flutter apps in seconds, allowing me to experience my app as close to production as possible.</p>
<p>With FlutterFlow you can run your app in the browser, but it has no access to sensors, which is kind of important for my Scavenger Hunts.</p>
<h2 id="heading-so-is-it-over">So is it over?</h2>
<p>As I said, FlutterFlow is an amazing product but has a long way to go to match the capabilities of the vanilla Flutter experience.</p>
<p>I'm considering still using FlutterFlow for designing UI components since I'm not familiar with any other design tools. Other than that, I won't be using it, and I will not give it another shot for this project. All this switching also cost me valuable time. But it was a nice learning experience.</p>
<h1 id="heading-the-future-of-the-project">The future of the project</h1>
<p>Working with FlutterFlow did give me some new insights into how I wish to approach this project. For example, in the way I want to store and handle data in the database.</p>
<h2 id="heading-scope">Scope</h2>
<p>My initial idea was to create an app that allowed the user to play different kinds of geographical games, Scavenger Hunts being one of them.</p>
<p>I decided to limit the app to just doing Scavenger Hunts. Hopefully, this will prevent me from trying to early-optimize.</p>
<h2 id="heading-tech-stack">Tech Stack</h2>
<p>I might have partially mentioned this topic scattered over the previous devblogs, but this is the tech stack I will be using.</p>
<p>So I'm back to using vanilla Flutter for this project for the earlier-mentioned reasons.I will use Firebase as the backend, for things like Auth, Storage, Firestore, etc.</p>
<p>So actually, I'm back to what I initially started with...</p>
<h2 id="heading-etos">Etos</h2>
<p>I'm still going to use <a target="_blank" href="https://pub.dev/packages/etos_flutter">Etos</a>, my self-created State Management package. I will develop Etos alongside this project to make it production ready.</p>
<h2 id="heading-theming">Theming</h2>
<p>Last week I triggered a discussion about theming in Flutter. I tweeted that I find theming in Flutter confusing. It seemed that I was not the only one.</p>
<p>While thinking about this topic, I came up with an idea, that I want to give a try in this project as well. If I like it, I will write an article about it.</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>Last week I created a new project. Thus far, I've added flavors, added Firebase Auth, and added basic App State with Etos.</p>
<p>Next, I will work on the main page with the bottom navigation.</p>
<p>Until next time!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 8: Taking another dive into FlutterFlow]]></title><description><![CDATA[In an earlier devblog I wrote about having tried FlutterFlow and why I decided to not use it for my pet project.
Well, it turns out FlutterFlow has added some very nice features last month, one of which made me reconsider FlutterFlow.
Together with a...]]></description><link>https://seg.veenstra.dev/devblog-8-taking-another-dive-into-flutterflow</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-8-taking-another-dive-into-flutterflow</guid><category><![CDATA[flutterflow]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[scavenger hunt]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Wed, 08 Mar 2023 20:33:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qZAwt2OHKaA/upload/ef816b0191f0e1e516cd08e8f4417d18.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In an <a target="_blank" href="https://fluttergamedev.com/devblog-5-getting-into-the-flutterflow">earlier devblog</a> I wrote about having tried FlutterFlow and why I decided to not use it for my pet project.</p>
<p>Well, it turns out FlutterFlow has added some very nice features last month, one of which made me reconsider FlutterFlow.</p>
<p>Together with a switch in mindset, I was willing to give FlutterFlow another chance.</p>
<h1 id="heading-what-changed">What changed?</h1>
<p>In February, FlutterFlow released a new update. The update addressed one of my major pain points.</p>
<p>The update adds the possibility to scope state to a page or component. Before the update, you could only use the global state. This became messy rather quickly when you had a lot of variables to store.</p>
<p>Having more control over your local state also allows you to be more conservative with your Firestore interactions, saving you money.</p>
<p>You can for example decide to first keep track of data locally, and only save to Firestore when the user explicitly presses save.</p>
<h1 id="heading-what-about-directus">What about Directus?</h1>
<p>Before I started using FlutterFlow again, I was working on authentication with Directus.</p>
<p>After I had set up the basic authentication flow, I started thinking about how to model my Scavenger Hunt data.</p>
<p>At that point, I concluded a document database like Firebase might be a better fit, at least for the game configurations.</p>
<p>Also realizing that I was spending a couple of hours a day for a few weeks just to get the authentication partially finished, made me see how easily Firebase was to set up.</p>
<p>Directus may still become a part of the project, but it will not be something I will focus on for now.</p>
<h1 id="heading-a-different-mindset">A different mindset</h1>
<p>In my previous attempts to use FlutterFlow, I looked at it through the eyes of a Flutter developer. I tried to solve problems the way I would try to solve them in code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678304603530/5fcea0a3-db0e-4c9a-becd-4c3920cb430c.png" alt class="image--center mx-auto" /></p>
<p>In many cases, FlutterFlow will not be able to do so. If you're a Flutter developer, and you're going to give FlutterFlow a try, try to go in with a blank slate. Try to understand how FlutterFlow wants you to solve certain problems instead of comparing them to how you'd have done it in code.</p>
<p>Your Flutter knowledge will still come in handy. The way you build UI, with widgets, will be very familiar. While FlutterFlow comes packed with a lot of predefined widgets and actions, you're able to extend FlutterFlow with custom widgets, functions and actions. These are still written in Dart, so also here your Flutter developer skills have value.</p>
<h1 id="heading-my-first-custom-action">My first custom action</h1>
<p>For my project, I had to create a way to change the order of the destinations of a Scavenger Hunt. I could not find a built-in way to do this, so I decided to give a custom action a try.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678305753857/50d52ec7-5bf2-4ea5-bafc-b9eb4fab8d03.png" alt class="image--center mx-auto" /></p>
<p>The arrow buttons on the items in the list would swap the position with the item below or above it when pressed.</p>
<p>To create a custom action, you will have to define the parameters, and optionally a return type. You can then let FlutterFlow generate a template that requires you to fill out the body.</p>
<p>In my case, the input parameters were the original list of destinations and the index of the item that would be swapped. The result would be a new list of destinations. I decided to create two separate actions, one for moving the item of the given index up, and one for moving the item down.</p>
<p>We could write this code ourselves, but we can also tell FlutterFlow to write the code for us.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678306215334/74d808f2-8d30-44a2-b89e-a747d1f910d9.png" alt class="image--center mx-auto" /></p>
<p>By describing what you want your custom action to do, FlutterFlow will provide a solution based on the parameters and return type that you defined for your action.</p>
<p>I told FlutterFlow to: "<em>Swap the item with the given index with the item below it</em>", and the code it generated was spot on, even handling the case where the index is that of the last item.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Automatic FlutterFlow imports</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'/backend/backend.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'/flutter_flow/flutter_flow_theme.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'/flutter_flow/flutter_flow_util.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'/custom_code/actions/index.dart'</span>; <span class="hljs-comment">// Imports other custom actions</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'/flutter_flow/custom_functions.dart'</span>; <span class="hljs-comment">// Imports custom functions</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-comment">// Begin custom action code</span>
<span class="hljs-comment">// DO NOT REMOVE OR MODIFY THE CODE ABOVE!</span>

Future&lt;<span class="hljs-built_in">List</span>&lt;ScavengerHuntDestinationStruct&gt;&gt; moveDestinationDown(
  <span class="hljs-built_in">List</span>&lt;ScavengerHuntDestinationStruct&gt; destinations,
  <span class="hljs-built_in">int</span> index,
) <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// swap item from given index with item below</span>

  <span class="hljs-keyword">if</span> (index == destinations.length - <span class="hljs-number">1</span>) {
    <span class="hljs-keyword">return</span> destinations;
  }
  ScavengerHuntDestinationStruct temp = destinations[index];
  destinations[index] = destinations[index + <span class="hljs-number">1</span>];
  destinations[index + <span class="hljs-number">1</span>] = temp;
  <span class="hljs-keyword">return</span> destinations;
}
</code></pre>
<p>So now we can use this action on the arrow buttons like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678306981358/23501316-c34d-4b57-9919-aa8f83af67fc.png" alt class="image--center mx-auto" /></p>
<p>The first action is the custom action. This action will return the updated list of destinations. The next action updated the Firestore document.</p>
<p>That's it! We created and used a custom action with the help of AI!</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>I'm currently focussing on the functionality that allows users to create 'drafts'. Drafts are editable Scavenger Hunts which have not yet been published.</p>
<p>Users will be able to update and test drafts until they are ready to publish.</p>
<p>So far I've got an overview of the drafts, and a way to create a new draft or edit an existing one. So far you can only add and remove destinations for a Scavenger Hunt, so next will be editing functionality.</p>
<p>Hopefully, I will be able to show a bit more about the app itself.</p>
<p>Bye bye!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 7: Going into another Directus]]></title><description><![CDATA[It's been a while since the last devblog. It was a busy couple of weeks with the holidays and the Flutter Forward event coming up.
I made a little bit of progress on the project which also involved a rather impactful decision.
But first, let me tell ...]]></description><link>https://seg.veenstra.dev/devblog-7-going-into-another-directus</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-7-going-into-another-directus</guid><category><![CDATA[directus]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[scavenger hunt]]></category><category><![CDATA[app development]]></category><category><![CDATA[flutterforward]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sun, 29 Jan 2023 10:26:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/pKeF6Tt3c08/upload/c14d5919c7cd2a490f1c6a9b6d785acf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's been a while since the <a target="_blank" href="https://fluttergamedev.com/devblog-6-swimming-against-the-current">last devblog</a>. It was a busy couple of weeks with the holidays and the <a target="_blank" href="https://flutter.dev/events/flutter-forward">Flutter Forward</a> event coming up.</p>
<p>I made a little bit of progress on the project which also involved a rather impactful decision.</p>
<p>But first, let me tell you a bit about the other stuff that happened!</p>
<h1 id="heading-flutter-forward-festival">Flutter Forward Festival</h1>
<p>The people from <a target="_blank" href="https://www.meetup.com/flutternl/">Flutter Netherlands</a>, together with <a target="_blank" href="https://pinch.nl/en/">Pinch</a> (the company I work for) organized a viewing party for the <a target="_blank" href="https://www.youtube.com/watch?v=zKQYGKAe5W8&amp;ab_channel=Flutter">Flutter Forward Live Stream</a>.</p>
<p>After watching the stream together, we would have a few presentations. I was asked to be one of the presenters which I accepted.</p>
<h2 id="heading-from-mental-model-to-app-architecture">From Mental Model to App Architecture</h2>
<p>My presentation was called "<em>From Mental Model to App Architecture</em>". I talked about how I came up with the architecture that I used for the last project I worked on, which struggles I had and what I did to overcome them.<br />So instead of a technical story, it was more about my developer experience, something I would usually write about on this website.</p>
<h2 id="heading-etos">Etos</h2>
<p>At the end of the presentation, I also made an introduction to <a target="_blank" href="https://pub.dev/packages/etos_flutter">Etos</a>.<br />Etos is the state management solution I'm working on that is intended to make applying the architecture that I've come up with, easier.</p>
<p>I talked a little bit about this, without calling it by name, in an <a target="_blank" href="https://fluttergamedev.com/devblog-2-the-scavenger-hunt#heading-state-management">earlier devblog</a>. I'm using this state management solution in this project, to see if it holds up in the real world.</p>
<p>When I'm satisfied enough, I will write an article about it.</p>
<h1 id="heading-project-progress">Project Progress</h1>
<p>As I said, it was a busy couple of weeks. However, I was able to make some progress. But first I made yet a 'drastic' change...</p>
<h2 id="heading-bye-bye-firestore-and-hello-directus">Bye Bye Firestore, and hello Directus!</h2>
<p>In <a target="_blank" href="https://fluttergamedev.com/devblog-5-getting-into-the-flutterflow#heading-firebase">devblog 5</a> I mentioned I wanted to use Firebase (Firestore) for this project.<br />After several attempts of trying to model my data in Firestore, I realized that my brain doesn't like the unstructured nature of a NoSQL database.</p>
<p>Whatever I tried, I just was never really satisfied with the outcome. It was holding up the project and I was slowly losing joy.</p>
<p>Then I took a step back, and thought about why I choose Firestore in the first place, and if it was necessary to have a NoSQL database for this project.</p>
<p>I came to realize it was not.</p>
<p>A few years ago I experimented with <a target="_blank" href="https://directus.io/">Directus</a>, I remembered being impressed by it. Directus is a headless CMS that wraps around your SQL database. It also automatically generates an API (REST and GraphQL) for you!</p>
<p>Since the first time I used it, Directus has come a long way. I read the docs and watched videos about it to see if this might be a good fit for me and my project.</p>
<p>It looked very, very, very promising!</p>
<p>So I decided to give it a shot. I created a FREE Directus project in their <a target="_blank" href="https://directus.io/pricing/">cloud service</a>. It was set up in no time at all and I could quickly try it out.</p>
<h2 id="heading-authentication">Authentication</h2>
<p>Because the free tier of the hosted instance doesn't allow for SSO services I had to make some changes in the authentication. I would no longer use FirebaseAuth for this and thus had to build this myself.</p>
<p>Luckily Directus also generates authentication endpoints that I could use. Unfortunately, I had to do a lot more work than what FirebaseAuth automatically gives you, but it was also a nice experience to learn a bit more about how authentication works under the hood.</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>I think I will first focus on being able to create a draft scavenger hunt.<br />Hopefully, this will give me a feel for how to model the data in de the database.</p>
<p>There are still a few questions I need to think about, like:</p>
<ol>
<li><p>Should it be possible to update a Scavenger Hunt after it has been published?</p>
</li>
<li><p>What is, and is not, allowed to be changed?</p>
</li>
</ol>
<p>But I will take it one step at a time.<br />First I will focus on creating the drafts.</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 6: Swimming against the current]]></title><description><![CDATA[If you've read the previous blog you know that I wanted to give a shot at using FlutterFlow.
Well, I did, and at first, it felt like this was going to be a timesaver for sure!
However, after the simple crud-like functionality, I started running into ...]]></description><link>https://seg.veenstra.dev/devblog-6-swimming-against-the-current</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-6-swimming-against-the-current</guid><category><![CDATA[flutter flow]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Fri, 23 Dec 2022 09:05:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1670576838986/JXhFzwZts.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've read the <a target="_blank" href="https://fluttergamedev.com/devblog-5-getting-into-the-flutterflow">previous blog</a> you know that I wanted to give a shot at using FlutterFlow.</p>
<p>Well, I did, and at first, it felt like this was going to be a timesaver for sure!</p>
<p>However, after the simple crud-like functionality, I started running into the limitations of the platform.</p>
<p>In this devblog I will explain the issues I ran into, and why I decided to go back to vanilla Flutter for further development of this project.</p>
<h1 id="heading-working-with-temporary-state">Working with temporary state</h1>
<p>After creating pages that showed the games, I started to work on pages that would allow users to create games of their own.</p>
<p>For this, I created a <em>Form</em> that had some <em>Textfields</em> for basic information like a name and description.</p>
<p>The next part was adding locations to a Scavenger Hunt. This is where the trouble started.</p>
<p>To show data you connect your page to a FireStore query. The main form shows the list of locations already added to the Scavenger Hunt. When the user clicks on the add button, a new form would open that would allow the user to create a new location.</p>
<p>But to update the list I would have to update the document, thus persisting the data. I wanted to wait for that to happen till the user pressed save so the user can easily decide to drop the change by not saving.</p>
<p>I found a way to work around this which was by using local state. But this felt kind of dirty because I would then be mixing the two and this is just not how I would do it if I'd code it myself.</p>
<h1 id="heading-location-picker">Location picker</h1>
<p>The key feature for adding locations to a Scavenger Hunt is picking the geographical position (latitude and longitude). Flutter Flow does have Google Maps integration but this is limited to showing one or multiple markers on a Google Map.</p>
<p>This Google Map doesn't support clicking on the map to get a location, it only allows to set actions for when a user clicks a marker.</p>
<p>I tried to work around it by adding a button that grabs the user's current location and uses that for the position of the Scavenger Hunt location.<br />This meant that the user had to get in the exact position to set the location. So when the location would be a landmark like a tower, a church or any other building, you would either get inside or set the position somewhere around the building.</p>
<p>Again, I was making compromises that I was not fond of.</p>
<h1 id="heading-subclass-datatypes">Subclass Datatypes</h1>
<p>Within Flutter Flow you have to define the collections that you are using. It's basically defining a schema for your Firestore Database.</p>
<p>Flutter Flow uses this to give you access to the right fields based on their type. So this got me thinking...</p>
<p>My initial plan is to support only Scavenger Hunts, but I'm also planning on adding different types of games.</p>
<p>So what I wanted was a collection of games that could hold different types (subtypes) of games. This is simply not possible with Flutter Flow.</p>
<p>The only way I could think of was to break things up into different collections and link them together or something. But again, this felt far from ideal and definitely not how I would do it if I would code this myself.</p>
<h1 id="heading-testing">Testing</h1>
<p>The app is all about geolocation and device orientation. Unfortunately, Flutter Flow only allows us to quickly test in the browser. It's a lot harder to get a good feeling about the functioning of the app if you cannot move it around in your hand.</p>
<p>While for the testing of the forms the web version was fine, for the sensory stuff, not so much.</p>
<p>In the original Proof of Concept, I would simply run the app on my real device and head outside to take it for a spin. I loved being able to do that.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>So, even though Flutter Flow got me started quickly, I felt like I would lose all that saved time by having to work around its limitations.</p>
<p>This means I would eventually end up with a product in the same amount of time as when I build it in vanilla Flutter, but without full control over the code.</p>
<p>I think Flutter Flow is a very cool product, but not a good fit for me, mostly because I know how to code.</p>
<p>I ended up thinking: "I would do this differently if I build this myself." quite a lot and therefore don't feel I would benefit from using Flutter Flow as much as I hoped.</p>
<p>In fact, I've already started working on the vanilla Flutter version. I think both the first PoC and the Flutter Flow experiment have given me enough insight to work on an MVP.</p>
<p>More on this in the next blog!</p>
]]></content:encoded></item><item><title><![CDATA[Devblog 5: Getting into the (Flutter)Flow]]></title><description><![CDATA[I've finally made some progress again!
But it may not be the progress you'd expect...
Last week I've decided to give a tool I've tried over a year ago another try, and I must say, thus far, I'm really impressed!
This week I've also released a separat...]]></description><link>https://seg.veenstra.dev/devblog-5-getting-into-the-flutterflow</link><guid isPermaLink="true">https://seg.veenstra.dev/devblog-5-getting-into-the-flutterflow</guid><category><![CDATA[flutterflow]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[app development]]></category><category><![CDATA[indie-hacker]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Sat, 26 Nov 2022 14:32:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/4L0UtNLyHG4/upload/v1669453739306/3LXH8UI8k.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've finally made some progress again!
But it may not be the progress you'd expect...</p>
<p>Last week I've decided to give a tool I've tried over a year ago another try, and I must say, thus far, I'm really impressed!</p>
<p>This week I've also released a separate article about my approach of creating widgets.</p>
<h1 id="heading-flutterflow">FlutterFlow</h1>
<p>If you're a Flutter developer you might have heard of a tool called <a target="_blank" href="flutterflow.io">FlutterFlow</a>. Maybe you've even given it a try.</p>
<p>About a year ago a colleague and I at <a target="_blank" href="pinch.nl">Pinch</a> took a look at it during one of our 'hackatons'. We wanted to see if we could benefit from it as an app agency.</p>
<p>Long story short; we decided it wasn't for us.</p>
<p>However, after watching a bunch of tutorial on the <a target="_blank" href="https://www.youtube.com/channel/UCDOUnLypYZFVGjB70QkR8ZQ">James NoCode</a> Youtube channel recently, I got really intrigued by how fast he was able to get stuff done. He showed some nice new features which were lacking at the time.</p>
<p>For someone like me that is creating a personal project and has almost no time to spare, this could be a game-changer.</p>
<h2 id="heading-what-is-flutterflow">What is FlutterFlow?</h2>
<p>FlutterFlow was <a target="_blank" href="https://www.youtube.com/watch?v=5j82wxzwFyI&amp;ab_channel=Flutter">introduced at Google IO</a> in May '21.</p>
<p>It's a no-code platform that allows you to build Flutter apps in a click-and-drag fashion.</p>
<p>You don't need an IDE, you just go to the web app (which itself is also build in Flutter!!!) and start building your app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669470232682/l7wE4UJkx.png" alt="Schermafbeelding 2022-11-26 om 14.43.30.png" /></p>
<h2 id="heading-why-would-i-use-flutterflow">Why would I use FlutterFlow?</h2>
<p>I can hear you think "Stephan, you know how to program, you are a wonderful full-time Flutter developer for almost two years! Why would YOU need a no-code platform".</p>
<p>And that's a fair thought! I also thought that no-code tools where for the 'normies' that don't know how to code.</p>
<p>However, some time ago I started following some so called "indie hackers" on Twitter and learned that even though most of them know how to code, they are also excessively utilizing no-code tools for their business.</p>
<p>And that's how I started feeling about this pet project as well. It's no longer just building an app. I'm planning to publish it, hopefully turn it into a success and earn some money.
It's becoming more than just an app, it's becoming a business.</p>
<p>So I decided to give FlutterFlow another try. But this time looking through the eyes of an entrepreneur instead of an app developer.</p>
<h1 id="heading-making-some-progress">Making some progress</h1>
<p>After a periode of getting side-tracked I finally was able to get some progress done.</p>
<p>Last week I started creating the project in FlutterFlow.</p>
<p>As you might have read in earlier devblogs, I was working on a prototype that used GPS and the Compass to play a hard-coded scavenger hunt.</p>
<p>The next step was managing these scavenger hunts. Since users should be able to create their own hunts, we need authentication.</p>
<h2 id="heading-firebase">Firebase</h2>
<p>FlutterFlow incorporates the use of Firebase very nicely. I think it's even harder to not use it. But since I already planned on using Firebase I wasn't worried about that.</p>
<p>Setting up the connection was amazingly easy. It's just a matter of minutes and you're good to go.</p>
<h2 id="heading-login">Login</h2>
<p>We need users to login to be able to create games. FlutterFlow comes with a bunch of example login pages that have everything set up, from creating an account, to login in with social media accounts (for which you have to do some extra configuration).</p>
<p>Within the hour I had a login/registration page and a blank homepage that the user would see after login. 🤯</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669470830774/xELI7bvb4.png" alt="Schermafbeelding 2022-11-26 om 14.53.36.png" /></p>
<h2 id="heading-listing-games">Listing games</h2>
<p>Within FlutterFlow you are able to define your collections for your <a target="_blank" href="https://firebase.google.com/docs/firestore">Firestore</a> database.
Based on your collections and configuration it can deploy the rules for the database for you!</p>
<p>Once I've had setup the collection I added a ListView to the Homepage and wired it up with my collection.</p>
<p>Once wired up I could layout the <em>ListItems</em>. FlutterFlow offers a lot of predefined components. So I just picked a card that I thought would fit with my app, wired up the fields with the data from the firebase documents and BAM! A working list of Scavenger hunts!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669471080670/fXVHdfUL3.png" alt="Schermafbeelding 2022-11-26 om 14.57.54.png" /></p>
<h2 id="heading-adding-scavenger-hunts">Adding Scavenger Hunts</h2>
<p>Just listing scavenger hunts doesn't make sense if you can add any new ones right?
Therefor I added a FAB (Floating Action Button) that would open a form for creating a new game.</p>
<p>It currently only adds part of the fields, like a name and description and you're even able to upload an image. But again, I've got everything working within an hour. Truly amazing!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669471248097/GMLTXpOrP.png" alt="Schermafbeelding 2022-11-26 om 15.00.42.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I feel like I've made so much progress in maybe just 8 hours of FlutterFlow usage. Just think about what I can accomplish when I actually get good at it!</p>
<p>So far things have been pretty straightforward but I am curious how FlutterFlow will handle more custom use-cases, like when I'm going to build the game screen which relies on both GPS and the Compass.</p>
<p>I've also not been able to deploy to a real device since it requires you to get a paid subscription to do so. Currently I don't feel the need, but I think I will need it pretty soon to be able to really test the GPS and Compass integrations.</p>
<p>We'll see!</p>
<p>Now that I use FlutterFlow I'm not planning on dropping Flutter completely since I'm still a Flutter developer.
I also think me actually knowing Flutter is super useful when using FlutterFlow since it allows for custom code and widgets. I Think it will allow me to benefit from both worlds.</p>
<h1 id="heading-how-i-build-widgets">How I build widgets</h1>
<p>Last week I've also written another article. This time I wanted to share my <em>strategy</em> for building widgets. You can find the article <a target="_blank" href="https://fluttergamedev.com/creating-widgets">here</a>.</p>
<h1 id="heading-whats-next">What's next?</h1>
<p>For the upcoming weeks I want to continue working on the creation of Scavenger Hunts. Users should be able to add destinations by setting a location with a hint, maybe a range, a picture(?) and a question for when the user is within range.</p>
<p>This means I will probably have to integrate Google maps to show the location so the user can set/adjust it.</p>
<p>Exiting!</p>
<p>So, what do you think about me using FlutterFlow? Do you think it's a good idea? Would you use it? Why/Why not?</p>
]]></content:encoded></item><item><title><![CDATA[Creating Widgets]]></title><description><![CDATA[Structuring your UI in Flutter can be tricky. For me the main reason was because both logic and UI code are written in the same language (Dart). This is something I wasn't used to coming from android native development.
In this article I will share t...]]></description><link>https://seg.veenstra.dev/creating-widgets</link><guid isPermaLink="true">https://seg.veenstra.dev/creating-widgets</guid><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Dart]]></category><category><![CDATA[UI]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Stephan E.G. Veenstra]]></dc:creator><pubDate>Mon, 21 Nov 2022 20:53:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/x9WGMWwp1NM/upload/v1669063902929/2iuloSjTO.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Structuring your UI in Flutter can be tricky. For me the main reason was because both logic and UI code are written in the same language (Dart). This is something I wasn't used to coming from android native development.</p>
<p>In this article I will share the 'guidelines' I use when I'm creating my widgets. They help me keep widgets clean and reusable.</p>
<p>This article is a more detailed version of tweet that I published a some time ago:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/SEGVeenstra/status/1583446473705803776?s=20&amp;t=mXfQdggVxpBMeADU1RfqlQ">https://twitter.com/SEGVeenstra/status/1583446473705803776?s=20&amp;t=mXfQdggVxpBMeADU1RfqlQ</a></div>
<p>So without any further ado, let's jump right in!</p>
<h1 id="heading-two-types-of-widgets">Two types of Widgets</h1>
<p>First of all, I distinguish two types of widgets. Both have different, clearly defined, responsibilities.</p>
<h2 id="heading-destinations">Destinations</h2>
<p>Destinations are widgets that represent a <em>page</em> or <em>screen</em> in your app, a widget you navigate too.
So if you find yourself using your widget with a <code>Navigator</code> call, than that's a destination:</p>
<pre><code class="lang-dart">Navigator.of(context).push(
  MaterialPageRoute(builder: (context) =&gt; <span class="hljs-keyword">const</span> HomePage()),
);
</code></pre>
<p>In this example the <code>HomePage</code> is clearly a <em>destination</em>. I will often name them something ending with Screen, or Page like TodoPage or LoginPage, but also DetailsModal and OrderConfirmationDialog I consider to be destinations.</p>
<p>Destinations are very specific to the app. They orchestrate what the screen will look like, often based on some kind of state. They contain most of the logic on <em>what</em> to show on the screen:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// build function of TodoList.dart</span>
Widget build(BuildContext context) {
  <span class="hljs-keyword">if</span>(isLoading) {
    <span class="hljs-keyword">return</span> Center(
      child: CircularProgressIndicator(),
    );
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> ListView(
      <span class="hljs-comment">// show loaded items</span>
    );
  }
}
</code></pre>
<p>In the example above you can see that based on <code>isLoading</code>, a different UI is shown.
This is what a <em>destination</em> widget does. It decides which other widgets should be used to represent the current state, which brings us to the <em>component widgets</em>.</p>
<h2 id="heading-component-widgets">Component Widgets</h2>
<p>These are basically all the other widgets that I create and are not destinations. They follow a strict set of rules.</p>
<h3 id="heading-dont-pass-domain-objects">Don't pass domain objects</h3>
<p>Unlike the destination widgets, the component widgets should be as app-independent as possible. When creating these kind of widgets, I will build them in such a way that they should be reusable in any app.
They should mainly focus on the <em>how</em> to show the information on the screen.</p>
<p>To accomplish full independence we cannot rely on the domain of the app. This makes passing domain objects as parameters <strong>forbidden</strong>. In the following example I will show you what I mean.</p>
<p>Let's say we want to create a <code>ProfileHeader</code> widget that shows the user's profile picture, name and city.
We could do that like this:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileHeader</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> User user;

  <span class="hljs-keyword">const</span> ProfileHeader({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.user});

  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        Image.network(user.profilePictureUrl),
        Text(user.name),
        Text(user.city),
      ],
    );
  }
}
</code></pre>
<p>Here we pass a <code>User</code> object and use its fields to build the widget.
This causes this widget to be strongly coupled to the <em>domain</em> of this app. Not only that, it also makes this widget less reusable!</p>
<p>Now take a look at the next approach:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileHeader</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> subtitle;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> imageUrl;

  <span class="hljs-keyword">const</span> ProfileHeader({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.subtitle,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.imageUrl,
  });

  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Column(
      children: [
        Image.network(imageUrl),
        Text(title),
        Text(subtitle),
      ],
    );
  }
}
</code></pre>
<p>In this example we pass each of the values as primitives and therefor no longer rely on the domain of the app. Now we cannot only reuse this widget in other apps, but even within the app itself we are able to reuse the <code>ProfileHeader</code> for like a company or group page!</p>
<h3 id="heading-avoid-nesting">Avoid nesting</h3>
<p>When I design my widgets I want to have them as close to the 'root' as possible. As mentioned earlier, destinations control which (component)widgets are shown so they should also tell those widgets what to display.</p>
<p>So, in able to allow for this, destinations should be able to access these widgets. When you are nesting widgets, you kind of delegate the responsibilities to the closest ancestor. However, I want to have control over them in my destination widgets.</p>
<p>So what does that look like? Imagine the following widget tree and code:</p>
<pre><code>- ProfilePage <span class="hljs-comment">// destination</span>
  - LoadingIndicator <span class="hljs-comment">// when data is loading</span>
  - ProfileContent <span class="hljs-comment">// when data has been loaded</span>
    - Header
    - Bio
    - Address
</code></pre><pre><code class="lang-dart"><span class="hljs-comment">// Our destination</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfilePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{

  Widget build(BuildContext context) =&gt;
     isLoading ? ProgressIndicator() : ProfileContent(user);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileContent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> User user;

  ProfileContent(<span class="hljs-keyword">this</span>.user);

  Widget build(BuildContext context) =&gt;
     ListView(
      children: [
        Header(user),
        Bio(user.bio),
        Address(user.address),
      ],
     );
}
</code></pre>
<p>In this example we have a <code>ProfileContent</code> widget which is shown when the <code>ProfilePage</code> has loaded the data. This <code>ProfileContent</code> widget in its turn is divided in multiple 'sections' like a <code>Header</code>, <code>Bio</code> and <code>Address</code>.
In order to pass all the data needed for these sections, we have to pass them to the <code>ProfileContent</code> widget. As you can probably tell, that could be a lot of properties.
In these cases we would often pass-in complex objects (like a User object) and let <code>ProfileContent</code> figure it out.</p>
<p>However, that's against our rules!</p>
<p>So instead of the code above, I'd do it like this:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Our destination</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfilePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{

  Widget build(BuildContext context) =&gt;
     isLoading 
      ? ProgressIndicator()
      : ProfileContent(
            header: Header(
              image: user.profileUrl,
              title: user.name,
              subtitle: user.city
            ),
            bio: Bio(
                text: user.bio,
            ),
            address: Address(
              streetName: user.address.streetName,
              houseNumber: user.address.houseNumber,
              ...
            ),
        );
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileContent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">final</span> Widget header;
  <span class="hljs-keyword">final</span> Widget bio;
  <span class="hljs-keyword">final</span> Widget address;

  ProfileContent({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.header,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.bio,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.address,
  });

  Widget build(BuildContext context) =&gt;
     ListView(
      children: [
        header,
        bio,
        address,
      ],
     );
}
</code></pre>
<p>In this second example, the <code>ProfilePage</code> is in full control over what is passed into the <code>Header</code>, <code>Bio</code>, and <code>Address</code> widgets. The way we achieved this is by having widgets as arguments for the <code>ProfileContent</code> widget.
This could look familiar since the same approach is used for the <code>Scaffold</code> widget, where you have params like <code>appbar</code> and <code>body</code>. So now the only responsibility of <code>ProfileContent</code> is laying out the widgets we pass.
This makes the widget so much more flexible and reusable.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>So that was it for now. If you find this useful, please let me know so I can share more about my approaches for writing widgets!</p>
<p>Cya!</p>
]]></content:encoded></item></channel></rss>