<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Taiwo Oluwatobiloba Festus on Medium]]></title>
        <description><![CDATA[Stories by Taiwo Oluwatobiloba Festus on Medium]]></description>
        <link>https://medium.com/@taiwoluwatobilobafestus?source=rss-9137940d0903------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*5Rn55Go4TlGxoajVt_tA5Q.jpeg</url>
            <title>Stories by Taiwo Oluwatobiloba Festus on Medium</title>
            <link>https://medium.com/@taiwoluwatobilobafestus?source=rss-9137940d0903------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 07:35:44 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@taiwoluwatobilobafestus/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Running Multi-Flavor Flutter Desktop on Windows Without File-Lock Nightmares]]></title>
            <link>https://medium.com/@taiwoluwatobilobafestus/running-multi-flavor-flutter-desktop-on-windows-without-file-lock-nightmares-05e024094391?source=rss-9137940d0903------2</link>
            <guid isPermaLink="false">https://medium.com/p/05e024094391</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[windows]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Taiwo Oluwatobiloba Festus]]></dc:creator>
            <pubDate>Wed, 09 Sep 2026 00:35:38 GMT</pubDate>
            <atom:updated>2026-09-09T00:35:38.814Z</atom:updated>
            <content:encoded><![CDATA[<p>If you’ve ever built a multi-role Flutter application — like an on-demand delivery app with separate entry points for Customers, Drivers, and Station Admins — you know that mobile flavors work almost seamlessly out of the box. You configure productFlavors on Android, Build Configurations on iOS, and you are good to go.</p><p>Try doing the same on Flutter Desktop for Windows, and you will hit a brick wall.</p><p>By default, Flutter’s Windows toolchain compiles every flavor into the exact same build directory (build\windows\x64\runner\Debug\), outputs the exact same executable name, and locks native DLLs when running. Try running two flavors at once to test live socket communication, and MSBuild will fail with file-lock errors.</p><p>Here is an architectural breakdown of why this happens, how Flutter’s internal tools parse Windows builds, and how you can run multiple Windows flavors side by side with full Hot Reload and debugging.</p><h3><strong>The Core Problems</strong></h3><h4>1. Build Collision and File Locks</h4><p>When you run:</p><pre>flutter run -d windows --flavor customer -t lib/main_customer.dart</pre><p>and then try running station in another terminal, MSBuild attempts to write to the exact same directory: build\windows\x64\runner\Debug\. Because the first instance is currently running, Windows places an exclusive lock on flutter_windows.dll and the application binary. The second build crashes immediately with:</p><pre>LINK : fatal error LNK1104: cannot open file &#39;...app.exe&#39;</pre><h4>2. The Infamous null.exe Crash</h4><p>To isolate flavors, the natural instinct is to change the binary name dynamically in windows/CMakeLists.txt:</p><pre>if(NOT &quot;$ENV{APP_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set(BINARY_NAME &quot;my_app_$ENV{APP_FLAVOR}&quot;)<br>endif()</pre><p>CMake will compile my_app_customer.exe without issues. But right after MSBuild completes, flutter run fails with:</p><pre>ProcessException: Failed to find &quot;build\windows\x64\runner\Debug\null.exe&quot; in the search path.</pre><h4>Why does this happen?</h4><p>Inside flutter_tools (packages/flutter_tools/lib/src/windows/build_windows.dart), Flutter uses a rigid regular expression to determine what binary it just compiled:</p><pre>final RegExp binaryNameRegExp = RegExp(<br>  r&#39;^\s*set\(BINARY_NAME\s*&quot;([^&quot;]+)&quot;\s*\)\s*$&#39;, <br>  multiLine: true,<br>);</pre><p>Flutter doesn’t actually evaluate your CMake scripting logic. It runs a regex scan over windows/CMakeLists.txt. When you wrap set(BINARY_NAME ...) inside CMake conditionals or use string interpolations like &quot;$ENV{APP_FLAVOR}&quot;, the regex fails to find a static literal match. It defaults to null, appends .exe, and looks for null.exe.</p><h4>3. The Installation Prefix Privilege Error</h4><p>When CMake runs its INSTALL target, it tries to bundle assets beside the executable. If the build directories aren&#39;t strictly isolated, or if CMake’s default prefix isn&#39;t explicitly forced to the binary directory, MSBuild falls back to Windows defaults:</p><pre>file cannot create directory: C:/Program Files/your_app. Maybe need administrative privileges.</pre><h3>The Solution Architecture</h3><p>To achieve clean, parallel debugging for multiple flavors on Windows:</p><ol><li><strong>Isolate output directories:</strong> Direct each flavor to its own folder (build\windows\x64\&lt;flavor&gt;\).</li><li><strong>Dynamic C++ targets:</strong> Inject target properties in windows/runner/CMakeLists.txt based on the flavor environment variable.</li><li><strong>Bypass the regex bug with </strong><strong>--use-application-binary:</strong> Point Flutter directly to the compiled .exe, skipping the tool’s faulty binary discovery step.</li><li><strong>Automate pre-launch validation:</strong> Use an IDE task hook (VS Code task or IntelliJ Before-Launch target) to ensure native binaries exist before launching the Dart VM.</li></ol><h4>Step 1: Patching windows/CMakeLists.txt</h4><p>Open your root windows/CMakeLists.txt.</p><p>First, configure dynamic naming while keeping string compatibility safe:</p><pre># The name of the executable created for the application.<br>if(NOT &quot;$ENV{APP_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set(BINARY_NAME &quot;my_app_$ENV{APP_FLAVOR}&quot;)<br>elseif(NOT &quot;$ENV{FLUTTER_APP_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set(BINARY_NAME &quot;my_app_$ENV{FLUTTER_APP_FLAVOR}&quot;)<br>else()<br>  set(BINARY_NAME &quot;my_app&quot;)<br>endif()</pre><p>Next, locate the installation section and force CMAKE_INSTALL_PREFIX unconditionally to avoid the C:/Program Files permission trap:</p><pre># === Installation ===<br>set(BUILD_BUNDLE_DIR &quot;$&lt;TARGET_FILE_DIR:${BINARY_NAME}&gt;&quot;)<br>set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)<br><br># Unconditionally force install prefix to the bundle directory<br>set(CMAKE_INSTALL_PREFIX &quot;${BUILD_BUNDLE_DIR}&quot; CACHE PATH &quot;Installation prefix&quot; FORCE)<br><br>set(INSTALL_BUNDLE_DATA_DIR &quot;${CMAKE_INSTALL_PREFIX}/data&quot;)<br>set(INSTALL_BUNDLE_LIB_DIR &quot;${CMAKE_INSTALL_PREFIX}&quot;)</pre><h4>Step 2: Isolating the Runner Output</h4><p>In windows/runner/CMakeLists.txt, find the add_executable(${BINARY_NAME} ...) block. Right <strong>after</strong> the closing parenthesis ), configure the target&#39;s runtime output directories:</p><pre>add_executable(${BINARY_NAME} WIN32<br>  &quot;flutter_window.cpp&quot;<br>  &quot;main.cpp&quot;<br>  &quot;utils.cpp&quot;<br>  &quot;win32_window.cpp&quot;<br>  &quot;${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc&quot;<br>  &quot;Runner.rc&quot;<br>  &quot;runner.exe.manifest&quot;<br>)<br><br># Isolate flavor output directories<br>set(TARGET_FLAVOR &quot;&quot;)<br>if(NOT &quot;$ENV{APP_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set(TARGET_FLAVOR &quot;$ENV{APP_FLAVOR}&quot;)<br>elseif(NOT &quot;$ENV{FLUTTER_APP_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set(TARGET_FLAVOR &quot;$ENV{FLUTTER_APP_FLAVOR}&quot;)<br>endif()<br><br>if(NOT &quot;${TARGET_FLAVOR}&quot; STREQUAL &quot;&quot;)<br>  set_target_properties(${BINARY_NAME} PROPERTIES<br>    RUNTIME_OUTPUT_DIRECTORY &quot;${CMAKE_BINARY_DIR}/runner/${TARGET_FLAVOR}/$&lt;CONFIG&gt;&quot;<br>    RUNTIME_OUTPUT_DIRECTORY_DEBUG &quot;${CMAKE_BINARY_DIR}/runner/${TARGET_FLAVOR}/Debug&quot;<br>    RUNTIME_OUTPUT_DIRECTORY_RELEASE &quot;${CMAKE_BINARY_DIR}/runner/${TARGET_FLAVOR}/Release&quot;<br>    RUNTIME_OUTPUT_DIRECTORY_PROFILE &quot;${CMAKE_BINARY_DIR}/runner/${TARGET_FLAVOR}/Profile&quot;<br>  )<br>endif()</pre><h4>Step 3: Automating Native Builds via Pre-Launch Script</h4><p>Because we use --use-application-binary, Flutter expects the binary to already exist on disk. To make this completely automatic, create a lightweight PowerShell script at scripts/ensure_binary.ps1:</p><pre>param(<br>    [Parameter(Mandatory=$true)][string]$Flavor,<br>    [Parameter(Mandatory=$true)][string]$Target<br>)<br><br>$binaryPath = &quot;build\windows\x64\$Flavor\runner\$Flavor\Debug\my_app_$Flavor.exe&quot;<br><br># If binary is already compiled, skip CMake entirely<br>if (Test-Path $binaryPath) {<br>    exit 0<br>}<br><br>Write-Host &quot;[Pre-Run] Native binary for &#39;$Flavor&#39; not found. Compiling...&quot; -ForegroundColor Cyan<br><br>$env:APP_FLAVOR = $Flavor<br>flutter build windows --debug --flavor $Flavor --target $Target<br><br>if (Test-Path $binaryPath) {<br>    Write-Host &quot;[Pre-Run] Binary compiled successfully.&quot; -ForegroundColor Green<br>    exit 0<br>} else {<br>    Write-Error &quot;[Pre-Run] Failed to build $binaryPath.&quot;<br>    exit 1<br>}</pre><p>If the .exe exists, the script exits in ~20 milliseconds. If you ran flutter clean or just cloned the repo, it compiles the native debug binary automatically before launching.</p><h4>Step 4: IDE Configurations</h4><h4>For VS Code</h4><p>Define the build check in .vscode/tasks.json:</p><pre>{<br>  &quot;version&quot;: &quot;2.0.0&quot;,<br>  &quot;tasks&quot;: [<br>    {<br>      &quot;label&quot;: &quot;Ensure customer Binary&quot;,<br>      &quot;type&quot;: &quot;shell&quot;,<br>      &quot;command&quot;: &quot;powershell.exe&quot;,<br>      &quot;args&quot;: [<br>        &quot;-ExecutionPolicy&quot;, &quot;Bypass&quot;,<br>        &quot;-File&quot;, &quot;${workspaceFolder}/scripts/ensure_binary.ps1&quot;,<br>        &quot;-Flavor&quot;, &quot;customer&quot;,<br>        &quot;-Target&quot;, &quot;lib/main_customer.dart&quot;<br>      ],<br>      &quot;presentation&quot;: { &quot;reveal&quot;: &quot;silent&quot; }<br>    }<br>  ]<br>}</pre><p>Attach it to .vscode/launch.json:</p><pre>{<br>  &quot;version&quot;: &quot;0.2.0&quot;,<br>  &quot;configurations&quot;: [<br>    {<br>      &quot;name&quot;: &quot;Windows (Customer)&quot;,<br>      &quot;request&quot;: &quot;launch&quot;,<br>      &quot;type&quot;: &quot;dart&quot;,<br>      &quot;program&quot;: &quot;lib/main_customer.dart&quot;,<br>      &quot;preLaunchTask&quot;: &quot;Ensure customer Binary&quot;,<br>      &quot;args&quot;: [<br>        &quot;-d&quot;, &quot;windows&quot;,<br>        &quot;--flavor&quot;, &quot;customer&quot;,<br>        &quot;--use-application-binary=${workspaceFolder}/build/windows/x64/customer/runner/customer/Debug/my_app_customer.exe&quot;<br>      ]<br>    }<br>  ],<br>  &quot;compounds&quot;: [<br>    {<br>      &quot;name&quot;: &quot;All Flavors&quot;,<br>      &quot;configurations&quot;: [&quot;Windows (Customer)&quot;, &quot;Windows (Station)&quot;]<br>    }<br>  ]<br>}</pre><h4>For IntelliJ / Android Studio</h4><p>Store your configurations in .idea/runConfigurations/ as project-level XML files so your entire team has them immediately.</p><p>Create .idea/runConfigurations/Ensure_customer_Binary.xml:</p><pre>&lt;component name=&quot;ProjectRunConfigurationManager&quot;&gt;<br>  &lt;configuration default=&quot;false&quot; name=&quot;Ensure customer Binary&quot; type=&quot;ShConfigurationType&quot;&gt;<br>    &lt;option name=&quot;SCRIPT_TEXT&quot; value=&quot;powershell.exe -ExecutionPolicy Bypass -File &amp;quot;$PROJECT_DIR$/scripts/ensure_binary.ps1&amp;quot; -Flavor customer -Target lib/main_customer.dart&quot; /&gt;<br>    &lt;option name=&quot;INDEPENDENT_SCRIPT_WORKING_DIRECTORY&quot; value=&quot;true&quot; /&gt;<br>    &lt;option name=&quot;SCRIPT_WORKING_DIRECTORY&quot; value=&quot;$PROJECT_DIR$&quot; /&gt;<br>    &lt;method v=&quot;2&quot; /&gt;<br>  &lt;/configuration&gt;<br>&lt;/component&gt;</pre><p>Create .idea/runConfigurations/Windows_customer.xml:</p><pre>&lt;component name=&quot;ProjectRunConfigurationManager&quot;&gt;<br>  &lt;configuration default=&quot;false&quot; name=&quot;Windows (Customer)&quot; type=&quot;FlutterRunConfigurationType&quot; factoryName=&quot;Flutter&quot;&gt;<br>    &lt;option name=&quot;additionalArgs&quot; value=&quot;-d windows --flavor customer --use-application-binary=$PROJECT_DIR$\build\windows\x64\customer\runner\customer\Debug\my_app_customer.exe&quot; /&gt;<br>    &lt;option name=&quot;buildFlavor&quot; value=&quot;customer&quot; /&gt;<br>    &lt;option name=&quot;filePath&quot; value=&quot;$PROJECT_DIR$/lib/main_customer.dart&quot; /&gt;<br>    &lt;envs&gt;<br>      &lt;env name=&quot;APP_FLAVOR&quot; value=&quot;customer&quot; /&gt;<br>    &lt;/envs&gt;<br>    &lt;method v=&quot;2&quot;&gt;<br>      &lt;option name=&quot;RunConfigurationTask&quot; enabled=&quot;true&quot; run_configuration_name=&quot;Ensure customer Binary&quot; run_configuration_type=&quot;ShConfigurationType&quot; /&gt;<br>    &lt;/method&gt;<br>  &lt;/configuration&gt;<br>&lt;/component&gt;</pre><h3>The Workflow in Practice</h3><p>With this setup:</p><ul><li><strong>Hitting Run / F5:</strong> The pre-launch script checks for the binary. If present, it skips native compilation and starts in seconds.</li><li><strong>Pure Dart Changes:</strong> Dart kernel compilation and debugging proceed normally. Hot Reload and Hot Restart work across all running instances without CMake interfering.</li><li><strong>Side-by-Side Debugging:</strong> Launching your Customer app alongside your Station app gives each instance its own runtime folder, its own DLL bundle, and its own debugging port.</li></ul><p>No file locks, no null.exe crashes, and true multi-flavor parity on Windows.</p><h3>Or… Skip the Entire Manual Setup</h3><p>If reading through CMake rules, PowerShell execution bypasses, and IDE runner XMLs felt tedious, you don’t actually have to do any of this by hand.</p><p>I automated the entire workflow into an open-source CLI tool and published it to pub.dev: <a href="https://pub.dev/packages/windows_flavourizer"><strong>windows_flavourizer</strong></a>.</p><h4>What the package handles for you:</h4><ul><li><strong>Auto-Discovery:</strong> Scans your project for existing main_&lt;flavor&gt;.dart entry points, Gradle productFlavors, or Flavorizr configurations—no need to list flavors manually if they exist.</li><li><strong>Scaffolding:</strong> Creates any missing lib/main_&lt;flavor&gt;.dart starter files automatically.</li><li><strong>CMake Patching:</strong> Updates root and runner CMakeLists.txt files with runtime directory isolation and forced bundle paths.</li><li><strong>IDE Profiles:</strong> Generates complete .vscode/ (with compound launchers) and .idea/ run configurations with attached pre-launch tasks.</li></ul><h3>Getting Started</h3><p>Activate the tool globally from pub.dev:</p><pre>dart pub global activate windows_flavourizer</pre><p>Navigate to your Flutter project root and run:</p><pre># Automatically detects flavors and generates configs for both VS Code and IntelliJ<br>windows_flavourizer<br><br># Or specify your targets and flavors explicitly<br>windows_flavourizer -f customer,station,rider --ide all</pre><p>Reload your IDE, hit Run on your flavors, and enjoy isolated, concurrent multi-flavor debugging on Windows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5N6bQVbMdFYbJV0Si6rcRg.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wQK9sDXf7N9hHasPGxJ-lA.jpeg" /><figcaption>Flavors running on Windows (left: Intellj IDEA/Android Studio, right: VS Code)</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=05e024094391" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Exploring The Vibrant and Colorful World of Fragment Shaders]]></title>
            <link>https://medium.com/@taiwoluwatobilobafestus/exploring-the-vibrant-and-colorful-world-of-fragment-shaders-ab8603508ff3?source=rss-9137940d0903------2</link>
            <guid isPermaLink="false">https://medium.com/p/ab8603508ff3</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[shaders]]></category>
            <category><![CDATA[computer-graphics]]></category>
            <dc:creator><![CDATA[Taiwo Oluwatobiloba Festus]]></dc:creator>
            <pubDate>Sun, 03 May 2026 19:15:19 GMT</pubDate>
            <atom:updated>2026-05-03T19:15:19.018Z</atom:updated>
            <content:encoded><![CDATA[<p>Hi! This is my first article, and while I’m a bit nervous, I’m excited to share this journey with you. My name is <strong>Oluwatobiloba</strong>, and I’ve always been obsessed with video games and computer graphics.</p><figure><img alt="Playing Far Cry 6
" src="https://cdn-images-1.medium.com/max/1024/0*3TyFlJNGNfkrgDKo" /><figcaption>Photo by <a href="https://unsplash.com/@onurbinay?utm_source=medium&amp;utm_medium=referral">Onur Binay</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>At one point, I decided to teach myself game development. However, I hit a wall early on: <strong>Unreal Engine</strong> and <strong>Unity</strong> were out of the question due to my hardware limitations. Instead of giving up, I found a YouTube channel called <a href="https://youtu.be/VyKE7vz65rY?si=Sj_x7oBFDZwU3W4Z"><em>GamesWithGabe</em></a> about making games using <strong>LWJGL3</strong> (Lightweight Java Game Library).</p><p>That spark of curiosity led me down a rabbit hole of low-level programming. I eventually built my own engine from scratch — it could only play <em>Mario</em>, but it was mine, and it worked!</p><p>Eventually, I picked up <strong>Flutter</strong> because I loved the idea of building beautiful mobile apps. Flutter feels like playing with LEGO; you use widgets as building bricks to assemble your UI piece by piece. But I couldn’t stay away from graphics for long. I eventually had a “lightbulb” moment: <strong>Why not combine Flutter with computer graphics?</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eU1uhtlNj9sBFmWDUQkTtQ.png" /><figcaption>My first procedural graphics app using SDFs</figcaption></figure><p>The final spark of inspiration came while discussing the last chapter of Flutter Engineering by <a href="https://medium.com/u/2b0aabe05af4">Majid Hajian</a> with my Flutter Book Club. Yes, it’s an actual club! We spend our time geeking out over everything from deep-dive backend architecture to our favorite anime—and it was that community energy that finally pushed me to hit &#39;publish&#39; in this article.</p><h3>What Exactly Are Shaders?</h3><p>Shaders are small, specialized programs that run directly on your <strong>GPU</strong> (Graphics Processing Unit). Their job is to manipulate data to produce a final color or image on your screen.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-Gztst4XO_glLeRJ" /><figcaption>Photo by <a href="https://unsplash.com/@manas_rb?utm_source=medium&amp;utm_medium=referral">manas rb</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Shaders work in a pipeline, meaning they are assembled in stages where the output of one stage becomes the input for the next. While there are several types, we usually focus on the “Big Two”:</p><ul><li><strong>Vertex Shader</strong>: This is the first stage. It receives data directly from your application — things like vertices (points in 3D space), texture coordinates, and transformation matrices. Its job is to calculate the position of these points and transform them into a “normalized” form the screen can understand.</li><li><strong>Fragment Shader</strong>: This is the final stage, and it’s where the magic happens. It determines the color of every single pixel on your screen.</li></ul><p>To give you an idea of the workload: if you have a standard 1920 by 1080 display, the fragment shader has to run over <strong>2,073,600 times</strong> just to output a single frame!</p><blockquote><strong>Note:</strong> There are also Geometry and Tessellation shaders that can be chained in between to add even more detail, but those are a bit outside the scope of this introduction.</blockquote><h3>Shaders in the Flutter Universe</h3><p>In Flutter, you don’t typically touch the vertex shader. That’s handled internally for you by the <strong>Canvas</strong> and the underlying rendering engine (like Impeller or Skia).</p><p>However, Flutter gives us the power to write our own <strong>Fragment Shaders</strong>. You can write your custom logic, attach it to a Paint object, and then wrap it all in a CustomPainter widget.</p><p>Fragment shaders are, in my opinion, the best thing since sliced bread. They allow you to replicate high-end visual effects that would be impossible with standard widgets, such as:</p><ul><li><strong>Chromatic Aberration</strong> (that “glitchy” color-shift look)</li><li><strong>Dynamic Reflections</strong></li><li><strong>Bloom and Glow effects</strong></li></ul><figure><img alt="Minimalistic Dark 3D Rendering Wallpaper in 8K Resolution." src="https://cdn-images-1.medium.com/max/1024/0*7MZv7tRA0GVLwPDR" /><figcaption>Photo by <a href="https://unsplash.com/@sebastiansvenson?utm_source=medium&amp;utm_medium=referral">Sebastian Svenson</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>What’s Next?</h3><p>Now that we have the theory down, it’s time to get our hands dirty. In this article, we’re going to walk through how to set up a fragment shader in Flutter, pair it with a CustomPainter, and render a <strong>procedural checkerboard pattern</strong> on the screen.</p><p>Ready? Let’s dive in.</p><p>That “bridge” between your code and the GPU can feel a bit abstract at first, but in Flutter, it follows a very specific hand-off process.</p><p>Think of it as a relay race: your <strong>Shader</strong> has the data, the <strong>Paint</strong> object holds the instructions, and the <strong>CustomPainter</strong> is the athlete that actually runs the race on the screen.</p><h3>1. The CustomPainter (The Logic)</h3><p>The CustomPainter is a class where you tell Flutter <em>what</em> to draw. It gives you a Canvas (your drawing surface) and a Size (how much space you have).</p><pre>import &#39;package:flutter/material.dart&#39;;<br><br>class CheckerboardPainter extends CustomPainter {<br>  final FragmentShader shader;<br><br>  CheckerboardPainter(this.shader);<br><br>  @override<br>  void paint(Canvas canvas, Size size) {<br>    // 1. Create a Paint object<br>    final paint = Paint();<br><br>    // 2. Attach your shader to the paint object<br>    // This tells the GPU: &quot;Use this shader program for whatever I draw next&quot;<br>    paint.shader = shader;<br><br>    // 3. Draw a rectangle that covers the whole area<br>    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);<br>  }<br><br>  @override<br>  bool shouldRepaint(covariant CustomPainter oldDelegate) =&gt; true;<br>}</pre><h3>2. The Paint Object (The Bridge)</h3><p>The Paint object is the most important part of this equation. It’s not just a &quot;color&quot; picker; it&#39;s a configuration object for the specialized hardware. When you say paint.shader = shader, you are effectively uploading your custom code to the GPU&#39;s &quot;instruction manual&quot; for that specific draw call.</p><h3>3. The CustomPaint Widget (The Canvas)</h3><p>Finally, you need to actually put this on your UI tree. You use the CustomPaint widget to give your painter a home.</p><pre>// Inside your Widget build method:<br>CustomPaint(<br>  size: Size(300, 300), // How big the &quot;canvas&quot; should be<br>  painter: CheckerboardPainter(myLoadedShader),<br>)</pre><p>Now that the basics has been set up, we would need to make some minor changes to our pubspec.yaml file. For this example, we would be adding a shader called checkerboard.frag</p><pre>flutter:<br><br>  shaders:<br>    - shaders/checkerboard.frag</pre><p>Then we would need to create a FlutterProgram which contains the FragmentShader.</p><pre>import &#39;dart:developer&#39; show log;<br>import &#39;dart:ui&#39;;<br><br>class ShaderFactory {<br>  static Future&lt;FragmentShader?&gt; loadShader(String shaderName) async {<br>    try {<br>      FragmentProgram program = await FragmentProgram.fromAsset(<br>        &#39;packages/my_awesome_shader_app/shaders/$shaderName&#39;,<br>      );<br>      return program.fragmentShader();<br>    } catch (e) {<br>      log(&#39;Failed to load shader $shaderName: $e&#39;);<br>      return null;<br>    } <br>  }<br>}</pre><blockquote>Note: I used my_awesome_shader_app as it is the actual name of the project as specified in pubspec.yaml.</blockquote><h3>The Final Piece: Uniforms and Memory Locations</h3><p>We are almost at the finish line! But there is one more piece of the puzzle: <strong>How do we send data from our Flutter code to the shader?</strong></p><p>Suppose you want to change the colors of your checkerboard dynamically without rebuilding the entire app. You need a way to &quot;talk&quot; to the GPU while it’s running. The answer is <strong>Uniforms</strong>.</p><p>Think of a <strong>Uniform</strong> as a constant variable that you send directly from your CPU (Dart code) to the GPU. It’s &quot;uniform&quot; because its value stays the same for every single pixel processed during a single draw call.</p><ol><li><strong>Data Types and &quot;Packing&quot;</strong><br>Uniforms can handle various data types: float, int, vectors, and matrices.<br>A color is usually represented as <strong>RGBA</strong> or <strong>RGB</strong>. While you could send three separate floats for Red, Green, and Blue, it’s much cleaner to pack them into a single vec3 (a 3-component vector).</li><li><strong>The Concept of &quot;Locations&quot;</strong><br>This is the part where you need to pay close attention. When you send data to a shader in Flutter, you have to tell it exactly where that data should go.</li></ol><p>You can think of <strong>Uniform Locations</strong> as specific slots in memory. In Flutter’s fragment shaders, these locations are index-based and start at <strong>0</strong>.</p><p>Here is the kicker: <strong>Locations are based on floats.</strong></p><ul><li>If you send one float, it takes up index 0.</li><li>If you send a vec3 (which contains 3 floats), it occupies indices 0, 1, and 2.</li><li>The <em>next</em> uniform you send would then start at index 3.</li></ul><p><strong>Why this matters:</strong> When you use the setFloat(index, value) method in Dart, you aren&#39;t just pointing to a variable name; you are pointing to a specific spot in the GPU&#39;s memory &quot;waiting room.&quot; If you get the index wrong, your colors will look... well, very weird.</p><p>For our checkerboard, we’ll need two vec3 uniforms—one for each of our alternating colors.</p><h3>Putting it into Practice: The Checkerboard Shader</h3><p>Enough with the theory—let’s get our hands on some code. To build our checkerboard, we’re going to use a bit of math to tell the GPU how to decide which color belongs on which pixel.</p><p>We’ll be using the gl_FragCoord, which gives us the precise coordinates of the pixel currently being processed. By dividing these coordinates into &quot;tiles&quot; and using some clever arithmetic, we can create a repeating pattern without using a single image asset.</p><p>We will also be adding one extra uniform uResolution, which is also size of the canvas being drawn.</p><p>Here is the GLSL code for our procedural checkerboard:</p><pre>#version 320 es<br>precision mediump float;<br><br>// These are our uniforms. <br>// Remember: color1 starts at location 0, color2 starts at location 3!<br>layout(location = 0) uniform vec3 uColor1;<br>layout(location = 3) uniform vec3 uColor2;<br>layout(location = 6) uniform vec2 uResolution;<br><br>out vec4 fragColor;<br><br>void main() {<br>    // 1. Define the size of our squares (in pixels)<br>    float tileSize = 50.0;<br><br>    // 2. Determine which &quot;grid cell&quot; the current pixel belongs to<br>    // We divide the coordinate by the tile size and use floor() <br>    // to get integer steps (0, 1, 2, 3...)<br>    vec2 grid = floor(gl_FragCoord.xy / tileSize);<br><br>    // 3. The Checkerboard Math<br>    // By adding the X and Y grid positions and using mod 2, <br>    // we get a flipping pattern of 0.0 and 1.0<br>    float check = mod(grid.x + grid.y, 2.0);<br><br>    // 4. Pick the color based on the result<br>    // If check is 0, we use color1. If it&#39;s 1, we use color2.<br>    vec3 finalColor = mix(uColor1, uColor2, check);<br><br>    fragColor = vec4(finalColor, 1.0);<br>}</pre><h3><strong>Why the </strong>mod()<strong> function?</strong></h3><p>The mod() (modulo) function is the secret sauce of procedural patterns. In our case, mod(grid.x + grid.y, 2.0) calculates the remainder of our grid sum divided by 2.</p><p>Imagine moving across the screen:</p><p>At grid cell (0,0), the sum is 0. 0 % 2 = 0.</p><p>At grid cell (1,0), the sum is 1. 1 % 2 = 1.</p><p>At grid cell (1,1), the sum is 2. 2 % 2 = 0.</p><p>This creates that classic alternating &quot;0, 1, 0, 1&quot; pattern both horizontally and vertically, which perfectly maps to our two colors.</p><blockquote>Writing code like this is why my ‘Mario engine’ could run on almost anything — procedural graphics are incredibly lightweight compared to loading heavy textures!</blockquote><h3>Wiring up the shader</h3><p>The last thing we need to do is to send over the uniforms to the fragment shader.</p><pre>void setCustomUniforms(FragmentShader shader, List&lt;Color&gt; colors) {<br>    int floatIndex = 0;<br>    <br>    // Sending over uColor1<br>    shader.setFloat(floatIndex++, colors[0].r);<br>    shader.setFloat(floatIndex++, colors[0].g);<br>    shader.setFloat(floatIndex++, colors[0].b);<br><br>    // Sending over uColor2<br>    shader.setFloat(floatIndex++, colors[1].r);<br>    shader.setFloat(floatIndex++, colors[1].g);<br>    shader.setFloat(floatIndex++, colors[1].b);<br><br>    // Sending over uResolution<br>    shader.setFloat(floatIndex++, 300.0); // width of the canvas<br>    shader.setFloat(floatIndex++, 300.0); // height of the canvas<br>  }</pre><p>After everything has been wired together and you run your app, you should see this beautiful pattern.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/466/1*qt5HKE-TgnXrFID9DowLAw.png" /><figcaption>Our very own checkerboard pattern</figcaption></figure><h3>What Next?</h3><p>This checkerboard is just the tip of the iceberg. Now that you’ve seen how to bridge the gap between Dart and the GPU, the possibilities are limited only by your imagination.</p><p>Shaders aren&#39;t just for static patterns; they are the key to high-performance, cinematic UI. You can take this implementation much further by exploring these areas:</p><ul><li><strong>Time-Based Animations</strong>: Pass a uTimeuniform to create flowing gradients, pulsing glows, or shifting colors.</li><li><strong>Interactive Visuals</strong>: Use a GestureDetectorto feed touch coordinates into your shader, making your UI react physically to the user&#39;s finger.</li><li><strong>Advanced Masking</strong>: Combine CustomPaint with ClipPath to create unique, procedurally generated shapes that would be impossible with standard widgets.</li></ul><p>The world of computer graphics is vast and vibrant. By mastering fragment shaders, you’re moving beyond simply &quot;using&quot; a framework and starting to paint directly onto the screen&#39;s hardware.</p><p><strong>Final Thought</strong>: Whether you’re building a simple app or a complex game engine, remember that every pixel is a canvas. Don&#39;t be afraid to experiment, break things, and see what happens when you push the GPU to its limits.</p><p>Till next time—<strong>Happy Fluttering!</strong></p><blockquote>What’s the first shader effect you’re planning to build?</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ab8603508ff3" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>