<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[why not make it?]]></title><description><![CDATA[Boilerplate-free articles about UX-Engineering and Design.]]></description><link>https://whynotmake.it/</link><image><url>https://whynotmake.it/favicon.png</url><title>why not make it?</title><link>https://whynotmake.it/</link></image><generator>Ghost 5.76</generator><lastBuildDate>Thu, 15 Oct 2026 22:49:23 GMT</lastBuildDate><atom:link href="https://whynotmake.it/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Rebuilding liquid glass for Flutter]]></title><description><![CDATA[In June 2025, four days after Apple announced Liquid Glass, we published
liquid_glass_renderer. Today we're releasing 1.0.0-dev.0, the first preview
of 1.0. It's a ground-up rewrite, and as far as we know, the most visually
accurate and best-performing implementation in Flutter to date.]]></description><link>https://whynotmake.it/glass/</link><guid isPermaLink="false">6ad0809fb154600001b72a47</guid><category><![CDATA[Flutter]]></category><category><![CDATA[UX]]></category><dc:creator><![CDATA[Tim Lehmann]]></dc:creator><pubDate>Thu, 15 Oct 2026 07:51:40 GMT</pubDate><media:content url="https://whynotmake.it/content/images/2026/10/cover.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://whynotmake.it/content/images/2026/10/cover.jpg" alt="Rebuilding liquid glass for Flutter"><p>In June 2025, four days after Apple announced Liquid Glass, we published<br><code>liquid_glass_renderer</code>. It was one of the very first ways to get the effect<br>in Flutter: shader-based refraction, edges that catch the light, and shapes<br>that melt into each other when they get close, all based on widgets you can insert into your tree as you wish. Its ideas spread.<br><code>liquid_glass_widgets</code> built its rendering pipeline on our code, and the<br>renderer will soon be integrated into <code>cupertino_ui</code>.</p><p>But it never quite looked like Apple&apos;s glass. It also had a memory problem:<br>stretch a button under your finger and memory went up, and resizing a sheet<br>pushed it higher still. These spikes could get so bad, that the OS would kill your app for memory pressure.</p><p>Today we&apos;re releasing <code>liquid_glass_renderer</code> 1.0.0-dev.0, the first preview<br>of 1.0. It&apos;s a ground-up rewrite, and as far as we know, the most visually<br>accurate and best-performing liquid glass implementation in Flutter to date.<br>As opposed to the previous versions of the package, we are planning to eventually move this out of developer preview. But we need your help testing the package and reporting issues.<br><br>Along the way we built three tools: a harness that renders the same scene in SwiftUI<br>and in Flutter and diffs the two, a benchmark runner that reads a Pixel 10&apos;s<br>power rails, and a tool that predicts Impeller&apos;s render passes. This post<br>covers what those tools taught us. A few of the findings went against what we<br>expected.</p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/hero-controls.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="1620" height="1560" srcset="https://whynotmake.it/content/images/size/w600/2026/10/hero-controls.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/hero-controls.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/hero-controls.jpg 1600w, https://whynotmake.it/content/images/2026/10/hero-controls.jpg 1620w" sizes="(min-width: 720px) 720px"></figure><p><em>The example app in light and dark mode. Every screenshot in this post is<br>rendered by Flutter, at iPhone 17 Pro size.</em></p><h2 id="glass-in-two-halves">Glass in two halves</h2><p>Rendering glass involves two kinds of work, and they change at very different<br>rates.</p><p>The first is about the <strong>shape</strong>. How far is this pixel from the edge? Which<br>way does the surface tilt here, and how far should the background bend? None<br>of that changes unless the shape changes or resizes.</p><p>The second is about the <strong>background</strong>. Whatever sits behind the glass has to<br>be bent, tinted and lit, and that has to happen again every time the content<br>underneath changes, which during a scroll means every frame.</p><p>1.0 keeps the two apart. The shape work goes into a small texture we call the<br><em>matte</em>, and the renderer keeps it until a shape changes. Every frame, one<br>final shader reads the matte, samples the background through it, and adds the<br>rim highlights, the dark outline and the inner shadow. When a list scrolls<br>behind a tab bar that isn&apos;t moving, the shape work doesn&apos;t run at all.</p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/pipeline.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="2000" height="873" srcset="https://whynotmake.it/content/images/size/w600/2026/10/pipeline.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/pipeline.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/pipeline.jpg 1600w, https://whynotmake.it/content/images/2026/10/pipeline.jpg 2200w" sizes="(min-width: 720px) 720px"></figure><h3 id="32-bits-per-pixel">32 bits per pixel</h3><p>The matte is an ordinary RGBA8 texture. That&apos;s 32 bits per pixel to describe<br>the lens at that point, and every one of them gets used.</p><p>Here&apos;s why precision matters. With 60 points of refraction on a 3&#xD7; screen, the<br>background can shift by about 180 physical pixels. Store that offset in 8 bits<br>and each step is 0.7 pixels, which is coarse enough to show up as stair steps<br>along the curved edge. So the matte packs a 12-bit surface direction and a<br>12-bit offset into three channels. The fourth stores the distance to the edge,<br>on a curve that puts most of its precision right at the edge.</p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/hero-blend.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="2000" height="693" srcset="https://whynotmake.it/content/images/size/w600/2026/10/hero-blend.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/hero-blend.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/hero-blend.jpg 1600w, https://whynotmake.it/content/images/2026/10/hero-blend.jpg 2320w" sizes="(min-width: 720px) 720px"></figure><p><em>Shapes that get close merge through a smooth union. The blend depends on how<br>differently the two surfaces face, so neighbours form a clean bridge instead<br>of swelling into a blob.</em></p><h2 id="the-memory-bug-that-wasnt-in-dart">The memory bug that wasn&apos;t in Dart</h2><p>The old pipeline recorded the shapes into a picture and rasterized it into a<br>brand-new image with <code>Picture.toImageSync</code> every time a shape changed. During<br>a stretch animation, that was one new GPU texture per frame. On the Dart side<br>everything looked correct, because we dropped each old image as soon as its<br>replacement arrived.</p><p>Dropping the image doesn&apos;t free its memory right away, though. The texture<br>stays alive until the engine is done with it, which can be several frames<br>later. A sheet-sized matte at 3&#xD7; is about 8 MB. At 120 frames per second,<br>those leftover textures add up to hundreds of megabytes while the animation<br>runs.</p><p>Moving to Flutter GPU didn&apos;t fix this by itself. A Flutter GPU texture is a<br>small Dart object wrapping a large native allocation, and the garbage<br>collector only counts the small object. A matte replaced every frame looks<br>like a few bytes of garbage to the GC, so it sees no reason to hurry. In one<br>host test we rendered 1,200 mattes in a row, and 570&#x2013;700 MB was still<br>allocated when the loop finished. It was released after a few seconds of<br>idle time. A phone that&apos;s low on memory might kill the app before then.</p><p><code>liquid_glass_widgets</code>, which vendors our old pipeline, handles this by<br>making the textures smaller. While a large shape animates, it renders the<br>matte at reduced resolution, capped at about one megapixel. When the<br>animation ends, it renders one more matte at full resolution. That limits<br>how big each leftover texture gets, but three problems remain:</p><ul><li><strong>It still allocates a new texture on every frame.</strong> The pile of leftover<br>textures is smaller but still there. Our benchmarks showed the allocation<br>itself is the expensive part: the UI thread spends milliseconds of every<br>frame creating and submitting a texture it throws away one frame later.</li><li><strong>Low-resolution mattes are wrong at the corners.</strong> We tried this<br>ourselves. The matte&apos;s values don&apos;t change linearly from one pixel to the<br>next, so smoothly interpolating a low-resolution matte bends the<br>refraction exactly where the curvature is highest.</li><li><strong>The quality changes when the animation stops.</strong> The glass animates at<br>one resolution and comes to rest at another.</li></ul><p>Flutter GPU textures have no <code>dispose()</code>, so we couldn&apos;t free them early<br>either. 1.0 stops creating them instead: each glass layer keeps a small ring<br>of textures and draws into the same ones over and over, at full resolution.<br>That took three attempts.</p><ol><li><strong>Plain reuse.</strong> On the Pixel this was a clear win. On the Mac it made<br>memory <em>worse</em> for anything that kept changing size. Retired textures of<br>every size piled up faster than the GC released them, and one large resize<br>peaked at 2.4 GB.</li><li><strong>Reuse with headroom.</strong> We made each texture a bit bigger than needed, so<br>small size changes would fit without a new one. Some scenes improved. A<br>full-width sheet resize went from 688 MB to 1,185 MB, because every texture<br>the ring replaced was now larger.</li><li><strong>Grow-only.</strong> A texture in the ring never shrinks. It grows in steps of<br>about 1.5&#xD7;, and the renderer only draws into the part it needs. A sheet<br>moving between 45% and 90% of the screen keeps reusing one large texture<br>instead of allocating dozens of different sizes.</li></ol><p>The third design shipped. Compared with creating a new texture on every<br>change:</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Scene</th>
<th style="text-align:right">New texture per change</th>
<th style="text-align:right">1.0 (grow-only reuse)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pixel 10, full-width sheet resize: peak memory</td>
<td style="text-align:right">647 MB</td>
<td style="text-align:right">321 MB</td>
</tr>
<tr>
<td>Pixel 10, full-width sheet resize: frame rate</td>
<td style="text-align:right">93 fps</td>
<td style="text-align:right">108 fps</td>
</tr>
<tr>
<td>Pixel 10, three stacked glass layers: frame rate</td>
<td style="text-align:right">70 fps</td>
<td style="text-align:right">119 fps</td>
</tr>
<tr>
<td>Pixel 10, button stretch: UI-thread paint time</td>
<td style="text-align:right">3.7 ms</td>
<td style="text-align:right">0.9 ms</td>
</tr>
<tr>
<td>M4 Max, large resize: peak memory</td>
<td style="text-align:right">2,119 MB</td>
<td style="text-align:right">610 MB</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<h2 id="how-we-benchmark">How we benchmark</h2><p>A screenshot can&apos;t tell you what an effect costs to run. To compare builds<br>fairly, we built two benchmark runners.</p><p>The <strong>Pixel 10</strong> runner installs several builds side by side and alternates<br>between them, reversing the order each repetition so neither build always<br>goes first. Before every run it waits until the phone has cooled to a fixed<br>skin temperature. Each run records a Perfetto trace with power draw in<br>milliwatts (GPU, CPU and memory, measured separately), GPU cycles per frame,<br>UI-thread timings and memory use. A second runner on an <strong>M4 Max</strong> records<br>frame timings, memory footprint and Metal GPU time. Each benchmark scene<br>isolates one cost: a button stretch, a sheet resize, sixteen shapes merging,<br>three stacked layers.</p><p>A few things might be surprising here:</p><ul><li><strong>Higher frame rates can raise power draw.</strong> With texture reuse, scenes<br>that used to drop frames hit 120 fps. The phone renders more frames, so<br>GPU power goes <em>up</em>. The energy per frame went down: 10% less on a button<br>stretch and 22% less with three stacked layers. Judged on milliwatts alone,<br>the fix would have looked like a regression.</li><li><strong>A small blur can cost more than a medium one.</strong> At low sigmas,<br>Impeller stops downsampling and blurs at full resolution. On the Pixel, a<br>small blur cost more than a sigma-7 blur. 1.0 handles very light frost<br>inside the final shader with three texture reads, so it never starts a<br>separate blur pass.</li><li><strong>Shader maths barely mattered.</strong> We tried lower precision, fewer <code>pow</code><br>calls and fewer texture reads in the final shader. On the Pixel, power<br>draw didn&apos;t change. Reading back the screen behind the glass is what costs<br>power: about 115 mW for each separate backdrop filter.</li><li><strong>Fake glass isn&apos;t free either.</strong> <code>FakeGlass</code> skips the shape work and the<br>refraction, but it still reads the background and blurs it, and those are<br>usually the biggest costs. We ran a scripted 54-second session in a real<br>production app, covering scrolling, tab switching and opening menus. In the<br>old renderer, fake glass was the most expensive setting. In the rewrite it<br>draws about a third less power. If you need a fallback that actually saves<br>battery, use an opaque surface.</li></ul><p>Not every result was clear. Metal GPU timings were sometimes too noisy to<br>show whether a change helped, and the results files report those as<br>inconclusive.</p><h2 id="matching-apple-without-fooling-ourselves">Matching Apple without fooling ourselves</h2><p>To check whether our glass looks like Apple&apos;s, we compare the two directly.</p><p>Each test scene is a JSON file that describes the canvas, the glass shapes<br>and a set of backgrounds. A small SwiftUI app renders the scene with Apple&apos;s<br>public glass APIs (<code>.buttonStyle(.glass)</code> and <code>GlassEffectContainer</code>) on a<br>fixed iOS 27 simulator. It saves the median of three settled frames, together<br>with the OS version, appearance and accessibility settings. A Flutter app<br>renders the same JSON, and a set of Python scripts compares the two images.</p><p>Every scene is rendered over four backgrounds. Each background isolates<br>different parts of the effect:</p><ul><li>a <strong>colour grid</strong> shows how the glass bends lines and shifts colour;</li><li>a <strong>second grid</strong> checks that a fit still holds on a different pattern;</li><li><strong>pure black</strong> leaves only the glints and the glow from the surface;</li><li><strong>pure white</strong> leaves only the outline, the inner shadow and the drop<br>shadow.</li></ul><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/toolbar-comparison.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="2000" height="1082" srcset="https://whynotmake.it/content/images/size/w600/2026/10/toolbar-comparison.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/toolbar-comparison.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/toolbar-comparison.jpg 1600w, https://whynotmake.it/content/images/2026/10/toolbar-comparison.jpg 2206w" sizes="(min-width: 720px) 720px"></figure><p><em>An iOS 27 toolbar button and ours, over three of the four backgrounds. The<br>right column is the plain pixel difference: black where the two match,<br>brighter where they don&apos;t. Nothing was aligned or colour-corrected. Over<br>black and white only a faint line along the rim remains. Over the colour<br>grid the difference shows across the whole face. Most of that comes from how<br>the two blur the background, explained below.</em> But some color transfer differences still remain for now, which we hope to iron out before 1.0.0.</p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/merge-bridges.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="2000" height="526" srcset="https://whynotmake.it/content/images/size/w600/2026/10/merge-bridges.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/merge-bridges.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/merge-bridges.jpg 1600w, https://whynotmake.it/content/images/2026/10/merge-bridges.jpg 2052w" sizes="(min-width: 720px) 720px"></figure><p><em>The neck between two merging rectangles, over black. iOS 27 on the left,<br>then ours at blend radii from 28 to 40. At 32 the bridge has the same shape;<br>at Apple&apos;s own value of 40 it&apos;s far too thick.</em></p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/merge-comparison.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="2000" height="880" srcset="https://whynotmake.it/content/images/size/w600/2026/10/merge-comparison.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/merge-comparison.jpg 1000w, https://whynotmake.it/content/images/size/w1600/2026/10/merge-comparison.jpg 1600w, https://whynotmake.it/content/images/2026/10/merge-comparison.jpg 2206w" sizes="(min-width: 720px) 720px"></figure><p><em>The full comparison at blend radius 32. Over black and white the two are<br>nearly indistinguishable. The colour grid shows where we still differ: the<br>glint in the neck and the way the grid bends inside it.</em></p><p>We don&apos;t publish an &quot;Apple accuracy&quot; percentage, because the pipeline can&apos;t<br>produce a meaningful one. It&apos;s a regression tool. It shows whether a change<br>moved us closer to Apple&apos;s rendering or further away, and in which part of<br>the image. The captures are SDR, so they don&apos;t show HDR. On a real iPhone,<br>the glint along the rim is brighter than SDR white, and we&apos;ve verified that<br>ours reaches the same HDR range there. On Android it can&apos;t: Flutter renders<br>into 8-bit surfaces, so the glint stays within SDR.</p><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/hero-colors.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="1110" height="1080" srcset="https://whynotmake.it/content/images/size/w600/2026/10/hero-colors.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/hero-colors.jpg 1000w, https://whynotmake.it/content/images/2026/10/hero-colors.jpg 1110w" sizes="(min-width: 720px) 720px"></figure><p><em>Four appearances in one layer. The light, dark, clear and tinted presets<br>came out of this tuning work. They&apos;re starting points, and you can adjust<br>them from Dart.</em></p><h2 id="render-passes-cost-more-than-the-shader">Render passes cost more than the shader</h2><p>Much of what glass costs on the GPU happens outside the glass shader. On<br>Impeller, every backdrop filter ends the current render pass, reads back<br>everything drawn so far, and starts a new pass. A blur adds passes of its<br>own. A CPU profiler won&apos;t show this, and neither will the widget tree.</p><p>To measure it, we built <a href="https://pub.dev/packages/impeeler?ref=whynotmake.it" rel="noreferrer"><strong>impeeler</strong></a>, a small dev tool that records what<br>Flutter draws and replays Impeller&apos;s decisions about where to split render<br>passes. For each frame it reports how many passes there are, how large they<br>are, and which widgets caused them. It predicts rather than profiles, but on<br>14 test scenes its predictions matched real GPU traces on both Metal and the<br>Pixel.</p><p>Two patterns in 1.0 come from those measurements:</p><ul><li><strong>Share the background where possible.</strong> All glass shapes in one layer<br>sample a single shared copy of the background. In the test scene above,<br>two blurs sharing one background needed half as many passes as two<br>separate blurs.</li><li><strong>Capture once when glass sits on glass.</strong> A tab bar&apos;s selection indicator<br>has to refract the bar beneath it, so it can&apos;t use the same background copy<br>as the bar. <code>LiquidGlassCapture</code> reads the screen once into a small, tightly<br>cropped area. The bar draws inside it first, then the indicator, which sees<br>the finished bar. This only helps when the effects are small and close<br>together. A fullscreen layer gains nothing.</li></ul><figure class="kg-card kg-image-card"><img src="https://whynotmake.it/content/images/2026/10/hero-loupe.jpg" class="kg-image" alt="Rebuilding liquid glass for Flutter" loading="lazy" width="1326" height="1224" srcset="https://whynotmake.it/content/images/size/w600/2026/10/hero-loupe.jpg 600w, https://whynotmake.it/content/images/size/w1000/2026/10/hero-loupe.jpg 1000w, https://whynotmake.it/content/images/2026/10/hero-loupe.jpg 1326w" sizes="(min-width: 720px) 720px"></figure><p><em>The text-selection loupe and a round magnifier, both magnifying the live<br>content underneath.</em></p><h2 id="what-changes-for-you">What changes for you</h2><p>1.0 is a breaking release. <code>Glassify</code>, arbitrary shapes and child refraction<br>are gone. The supported shapes are circles, ovals, rounded rectangles and<br>rounded superellipses, with up to 16 per layer. Children are drawn sharp on<br>top of the glass instead of being refracted through it. The full effect<br>needs Flutter 3.47 with Impeller and uses <code>flutter_gpu</code>, which is still a<br>preview API. Skia and web builds fall back to <code>FakeGlass</code>. The license<br>changed from MIT to Apache 2.0.</p><h2 id="whats-next">What&apos;s next</h2><p>Animating glass no longer uses extra memory. Composition costs are<br>predictable, and we can measure how far our rendering is from iOS.</p><p>That measurement shows work still to do. In the comparison images, the<br>refraction, color transfer, the rims and the glint where shapes merge all still differ from<br>Apple&apos;s. Our memory numbers come from<br>one Pixel and one Mac, so measure GPU time and battery on the devices your<br>app targets.</p><p>1.0.0-dev.0 is a preview, and the API can still change before 1.0. Try it in<br>your app and tell us what breaks. The scenes, capture scripts and benchmark<br>runners are all in the repository. If our glass looks clearly wrong in some<br>situation, the most useful bug report is a scene file that reproduces it.</p>]]></content:encoded></item><item><title><![CDATA[You too can Enjoy Writing Tests for Your Apps!]]></title><description><![CDATA[If everybody talks about writing tests so much, how come barely anybody actually writes tests for their code? This post teaches you when, not how, to write unit tests.]]></description><link>https://whynotmake.it/you-too-can-enjoy-testing/</link><guid isPermaLink="false">6630092ef631ae0001aec8ac</guid><category><![CDATA[DevOps]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Tim Lehmann]]></dc:creator><pubDate>Mon, 29 Apr 2024 20:59:32 GMT</pubDate><media:content url="https://whynotmake.it/content/images/2024/04/Test-everything.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-accent"><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">After reading this...</strong></b><br><br>&#x1F4A1; you might gain a new perspective on unit testing<br>&#x23F1;&#xFE0F; you will know not just how, but when to write unit tests</div></div><img src="https://whynotmake.it/content/images/2024/04/Test-everything.png" alt="You too can Enjoy Writing Tests for Your Apps!"><p>Spend any amount of time with software developers online or offline, and at some point someone is likely going to start talking about how important it is to write tests. You will hear people talk about how it is important to reach metrics like a <a href="https://verygood.ventures/blog/road-to-100-test-coverage?ref=whynotmake.it">100% test coverage</a> and you might even start to feel a bit inadequate for how you write code; Did you just write a function without first writing a bunch of failing unit tests for it? A cardinal sin if you listen to Test-Driven-Development missionaries.</p><p>Strategies like enforcing test coverage or Test-Driven-Development can work well for larger teams with sufficient time and financial budgets, but they fall apart in the fast-moving environment of smaller, more dynamic projects. When it comes to the teams that I have worked with in my career so far, I have seen very few people actually write tests for their code and even fewer seemed to have a real strategy for when to write them. I can only speculate about how many single developers write tests for their apps, but I suspect the number is shockingly low. </p><p>I learned how to write tests way too late in my life, but when I did, I felt how they gave me a sense of security and made me sleep better, how they made me think about my software&apos;s architecture in a new way and how they helped me write more maintainable code. All this, however, still didn&apos;t get me to test regularly. </p><p>That is because, to write tests reliably and reap all their benefits, developers don&apos;t just need to know <strong>how</strong> to test, but also <strong>when</strong> to test.</p><p>You hear that you should reach a certain test coverage, or write tests before every bit of code that you write, but when you realise that this approach is not feasible for your project, you are suddenly back in the wild west, where you don&apos;t have any rules or structure for what to test and when to do it. And in the absence of clear guidelines, people often revert to the easiest solution, which is writing no tests at all. Luckily, I found a simple shift in perspective could help me make sense of it all. Not only are you going to find yourself writing more tests, through small, cumulative change you are going to end up with a more robust codebase. I am even going to go out there and say that you are going to have some <em>fun</em> while doing it.</p><h2 id="use-tests-to-try-out-your-code">Use Tests to Try Out Your Code.</h2><p>Don&apos;t be fooled by how obvious this sounds. During the same years I resisted writing tests because it felt too cumbersome, I still put dummy buttons in my UI to call functions with dummy parameters, just to see if they do what they should <strong>all. the. time</strong>.</p><p>Let&apos;s think about this for a second, because I bet most of you have had this experience. You just wrote some nice business logic, maybe an API call or two somewhere, and of course you want to try out if it works. But you don&apos;t have any way to interact with this code yet, so to verify that it does its thing, you would need to wait until everything is hooked up to UI. Lots of times, this is what I did, and by that time I had forgotten half of the cases I wanted to test, and if something went wrong it there were already ten different places it could&apos;ve happened in.</p><p>To mitigate that, I often did what many developers do. I just hooked up my code to some random button, to call it with some made up dummy arguments. A couple <code>print</code>s here, a breakpoint there, just to verify the output was what I wanted, and I was satisfied. Time to undo everything again, and hope that I don&apos;t forget to clean up behind me.</p><p>What I described just now is basically writing a test, but I&apos;m the manual test runner! Writing a unit test would&apos;ve accomplished the same, but repeatable, automatic, and future-proof. This gives rise to a simple strategy.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">Write a test every time you feel the urge to try out some code you wrote</strong></b>.</div></div><p>This also includes times when you receive a bug report and you&apos;re trying to reproduce the error. If you manage to write a failing test, you&apos;ll make your life so much easier And believe me, if you&apos;re scared that writing the test would&apos;ve been more effort, a little bit of learning will take you a huge way</p><p>I&apos;ve seen people try out their business logic by hand so much, that I believe it is a deeply engrained habit for many, if not most developers like it was for me. And who can blame us? By the time I finished my Bachelor&apos;s, I had written multiple apps for uni. Someone had to test all of this code? Nobody told me I could let the computer do it for me, much less how to do it. But once this framework clicked for me, I started enjoying the satisfaction so much, that some of my side project even sport the mythical 100% coverage now. And I have to admit, that does feel pretty great, so maybe they weren&apos;t all wrong after all.</p><hr><p>Of course, the tests you wrote are only going to unfold their full potential once you have a set-up that runs them against your codebase regularly. In other words, you will want at least a simple DevOps setup. If you don&apos;t have that yet, and you&apos;re curious about how to set it up for GitHub, consider checking out our <a href="https://whynotmake.it/your-one-stop-shop-for-flutter-code-coverage-in-github-actions/" rel="noreferrer">Flutter CI Tour</a>.</p>]]></content:encoded></item><item><title><![CDATA[Your One-Stop-Shop for Flutter CI on GitHub]]></title><description><![CDATA[In today's world of software development, Continuous Integration (CI) is an essential part of the development process for most fancy-pants software development teams. However, many small Flutter teams still don't enjoy even the benefits of a CI setup. This article gives a great starting point!]]></description><link>https://whynotmake.it/flutter-ci-tour/</link><guid isPermaLink="false">65022c373703180001e939b8</guid><category><![CDATA[Flutter]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Tim Lehmann]]></dc:creator><pubDate>Wed, 13 Sep 2023 21:40:31 GMT</pubDate><media:content url="https://whynotmake.it/content/images/2024/04/Code-Coverage-Assistant.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-accent"><div class="kg-callout-text"><b><strong style="white-space: pre-wrap;">After reading this...</strong></b><br><br>&#x1F4A1; you will know how to create a basic CI setup<br>&#x1F530; you can add coverage badges and reports to your repo automatically</div></div><img src="https://whynotmake.it/content/images/2024/04/Code-Coverage-Assistant.jpg" alt="Your One-Stop-Shop for Flutter CI on GitHub"><p>In today&apos;s world of software development, Continuous Integration (CI) is an essential part of the development process for most fancy-pants software development teams. However, many hobbyists and small Flutter shops still don&apos;t enjoy even the most basic benefits of a CI setup, maybe because they feel overwhelmed, or don&apos;t really know where to start. If you don&apos;t use CI on even your most niche side projects, I promise you, there is a better way. We&apos;ll provide you with a CI starter kit (batteries included) and introduce you to a free tool we built called <a href="https://github.com/whynotmake-it/dart-coverage-assistant?ref=whynotmake.it" rel="noreferrer"><strong>Dart Coverage Assistant</strong></a> that you will (learn to) love! Keep reading, and within 10 minutes, you can have a GitHub workflow running that </p><ul><li>&#x1F9EA; Tests your code</li><li>&#x1F50E; Checks your formatting</li><li>&#x1F4DD; Reports code coverage on your Pull Requests</li><li>&#x1F530; Creates coverage badges for your READMEs</li><li>&#x1F634; Improves your sleep through peace of mind (citation needed)</li></ul><div class="kg-card kg-callout-card kg-callout-card-green"><div class="kg-callout-emoji">&#x1F914;</div><div class="kg-callout-text">If your first thought is &quot;Why would I need coverage, I don&apos;t even have any tests in my codebase?&quot;, this article might not be for you. But please hang tight, we have another post coming that you will find interesting! </div></div><p>If all of this sounds attractive to you, let&apos;s dive in!</p><h2 id="flutter-ci-starter-pack">Flutter CI Starter Pack</h2><p>If you are starting from zero, there are a few things your CI workflow should do for you. I know you&apos;re smart, but why not let the computer do some of the dumb stuff for you, so you can <strong>build </strong>and<strong> ship </strong>more?</p><p>The most basic CI workflow should accomplish a few things for you:</p><ul><li>Check your code for compile-time errors</li><li>Run your unit tests (even if you don&apos;t have any yet)</li></ul><p>For a Flutter Repository, this can quickly be achieved by adding the following file:</p><figure class="kg-card kg-code-card"><pre><code class="language-yml">name: Continuous Integration

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  flutter-check:
    name: Build Check
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: &#x1F4DA; Checkout
        uses: actions/checkout@v4

      - name: &#x1F426; Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          sdk: &apos;stable&apos;
          
      - name: &#x1F50E; Check for compile-time errors
        run: dart analyze .
      
      - name: &#x1F9EA; Run Tests
        run: flutter test --coverage
</code></pre><figcaption><p><span style="white-space: pre-wrap;">The absolute most basic workflow</span></p></figcaption></figure><p>This should be good-to-go out of the box for most repositories, but a few things are notable. If you take a look at the <code>on</code> section, the workflow will run on every Pull Request, and every push to the main branch. Using the Pull Request runs is a great way to make sure everything still works before you push to <code>main</code>. And the <code>concurrency</code> section will make sure that subsequent pushes cancel ongoing runs and save you a bunch of action minutes.</p><h2 id="the-next-building-blockfancy-coverage-badges">The next building block - Fancy Coverage Badges</h2><p>After using your CI for a while, you might desire a bit more, and maybe you want to show off your progress and have some fancy-looking badges in your PRs and READMEs, to show off your 2% test coverage (&#x1F972;). We built a GitHub Action called <strong>Dart Coverage Assistant</strong>, that adds coverage reports to your repository without any extra setup.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://github.com/whynotmake-it/dart-coverage-assistant?ref=whynotmake.it"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - whynotmake-it/dart-coverage-assistant: Magically generates code coverage reports from your dart projects</div><div class="kg-bookmark-description">Magically generates code coverage reports from your dart projects - whynotmake-it/dart-coverage-assistant</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="Your One-Stop-Shop for Flutter CI on GitHub"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">whynotmake-it</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/dc1429e5d490fc13e07d62913344abaa8bc4038229e3ac429e2e3828d56677c9/whynotmake-it/dart-coverage-assistant" alt="Your One-Stop-Shop for Flutter CI on GitHub"></div></a><figcaption><p><span style="white-space: pre-wrap;">Dart Coverage Assistant Repository</span></p></figcaption></figure><p>Our recommended Level 2 CI workflow looks as follows:</p><pre><code class="language-yaml">name: Continuous Integration

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  flutter-check:
    name: Build Check
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      pull-requests: write
    steps:
      - name: &#x1F4DA; Checkout
        uses: actions/checkout@v4

      - name: &#x1F426; Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          sdk: &apos;stable&apos;
          
      - name: &#x1F50E; Check for compile-time errors and warnings
        run: dart analyze . --fatal-infos
      
      - name: &#x1F9EA; Run Tests
        run: flutter test --coverage

      - name: &#x1F4CA; Generate Coverage
        id: coverage-report
        uses: whynotmake-it/dart-coverage-assistant@v1
        with:
          generate_badges: pr
</code></pre><p>As you can see, not that much has actually changed from the Level 1 workflow. The main two differences, are that the third step in the workflow now also fails when any analyser infos have not been dealt with, and the aforementioned dart-coverage-assistant action has been added. This will</p><ul><li>comment with a coverage report on every PR you open (it will even show you the difference in coverage that that PR makes)</li><li>open a PR with svg coverage badges every time your coverage changes on <code>main</code></li></ul><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x2139;&#xFE0F;</div><div class="kg-callout-text">To enable PRs with coverage badges, you need to allow Actions to open Pull Requests in your GitHub Repository settings. If you don&apos;t want to do that, set generate_badges: none</div></div><p>Dart Coverage Assistant also seamlessly handles monorepos, so if you manage multiple Dart projects from one repository, you won&apos;t even have to set anything up for that to work, as long as you ran your tests with <code>--coverage</code> in all of the projects. And if you want to enforce a certain minimum coverage, or disallow PRs that decrease the coverage, that&apos;s possible too.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://whynotmake.it/content/images/2024/04/Screenshot-2024-04-30-at-01.24.22@2x.png" class="kg-image" alt="Your One-Stop-Shop for Flutter CI on GitHub" loading="lazy" width="1832" height="1732" srcset="https://whynotmake.it/content/images/size/w600/2024/04/Screenshot-2024-04-30-at-01.24.22@2x.png 600w, https://whynotmake.it/content/images/size/w1000/2024/04/Screenshot-2024-04-30-at-01.24.22@2x.png 1000w, https://whynotmake.it/content/images/size/w1600/2024/04/Screenshot-2024-04-30-at-01.24.22@2x.png 1600w, https://whynotmake.it/content/images/2024/04/Screenshot-2024-04-30-at-01.24.22@2x.png 1832w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">An example coverage Report generated by Dart Coverage Assistant</span></figcaption></figure><hr><h2 id="bonus-level-%E2%80%93-check-code-generation">Bonus Level &#x2013; Check Code Generation</h2><p>Many Dart and Flutter projects rely on Code Generation through packages like freezed, json_annotation, and many more. This houses the danger that you push a change to a class, but forget to run the code generation, so it&apos;s associated generated files weren&apos;t updated. In some cases, this will be detected already, since it can cause compile-time errors, however, it makes a lot of sense to check it separately.</p><p>Thus, if your project uses code generation, add the following code under the Level 2 workflow:</p><pre><code class="language-yaml">   check_generation:
    name: Check Code Generation
    timeout-minutes: 10
    runs-on: ubuntu-latest
    steps:
      - name: &#x1F4DA; Checkout
        uses: actions/checkout@v4

      - name: &#x1F3AF; Setup Dart
        uses: dart-lang/setup-dart@v1.6.4
        with:
          sdk: &quot;stable&quot;

      - name: &#x1F528; Generate
        run: dart run build_runner build --delete-conflicting-outputs

      - name: &#x1F50E; Check there are no uncommitted changes
        run: git diff --exit-code</code></pre><p>This job will now fail if you committed something where the generated files are in a different state than they should be.</p>]]></content:encoded></item></channel></rss>