<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Azeem Hassan</title>
    <description>The latest articles on Forem by Azeem Hassan (@azeemhassanch).</description>
    <link>https://forem.com/azeemhassanch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3869837%2Fbdeb68f0-1646-48bd-93cc-e6876cad7b05.jpg</url>
      <title>Forem: Azeem Hassan</title>
      <link>https://forem.com/azeemhassanch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/azeemhassanch"/>
    <language>en</language>
    <item>
      <title>GPS Camera App - Geo Tagging</title>
      <dc:creator>Azeem Hassan</dc:creator>
      <pubDate>Sun, 12 Apr 2026 20:45:51 +0000</pubDate>
      <link>https://forem.com/azeemhassanch/gps-camera-app-geo-tagging-4400</link>
      <guid>https://forem.com/azeemhassanch/gps-camera-app-geo-tagging-4400</guid>
      <description>&lt;h1&gt;
  
  
  When a “Simple” GPS Camera App Wasn’t Simple at All
&lt;/h1&gt;

&lt;p&gt;I remember sitting late one night, testing the app on a low-end Android phone. I tapped the capture button, waited… and the photo came out late, with slightly wrong GPS coordinates. I tried again, and it lagged even more.  &lt;/p&gt;

&lt;p&gt;That’s when I stopped and thought — something isn’t right.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment everything changed
&lt;/h2&gt;

&lt;p&gt;I went into this project thinking I was building a simple GPS camera app. You know… open camera, take photo, stamp location, done.&lt;/p&gt;

&lt;p&gt;But that assumption didn’t last long.&lt;/p&gt;

&lt;p&gt;Because once you start combining &lt;strong&gt;camera capture + GPS timing + image processing&lt;/strong&gt;, you’re no longer building a simple app. You’re dealing with coordination between multiple systems that don’t naturally sync well.&lt;/p&gt;

&lt;p&gt;And if you’ve ever worked on something like this, you probably know exactly what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Here’s the thing
&lt;/h2&gt;

&lt;p&gt;When you rely fully on a framework, you’re trusting it to handle complexity for you. And most of the time, that works.&lt;/p&gt;

&lt;p&gt;But when you need &lt;strong&gt;precise control&lt;/strong&gt; — like capturing GPS at the exact moment of image capture — you start to notice limitations.&lt;/p&gt;

&lt;p&gt;I learned this the hard way.&lt;/p&gt;

&lt;p&gt;You can’t treat every part of your app equally. Some parts are UI-driven. Others are performance-critical. And you have to make that distinction early, or you’ll feel it later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rethinking the approach
&lt;/h2&gt;

&lt;p&gt;So I changed direction.&lt;/p&gt;

&lt;p&gt;I kept Flutter for the UI because it’s fast, flexible, and honestly great for building product screens. But I moved the core pieces — camera handling, GPS synchronization, and image processing — into native Kotlin.&lt;/p&gt;

&lt;p&gt;And that decision changed everything.&lt;/p&gt;

&lt;p&gt;Actually… let me rephrase that.&lt;/p&gt;

&lt;p&gt;It didn’t make things easier immediately. But it gave me control. And that control is what allowed me to fix the real problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the numbers actually said
&lt;/h2&gt;

&lt;p&gt;I didn’t want to rely on “it feels faster,” so I started tracking things using Flutter DevTools and some internal logging.&lt;/p&gt;

&lt;p&gt;Here’s what I saw after restructuring the architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvaj6mgnth5w822lnc829.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvaj6mgnth5w822lnc829.png" alt="screenshot 1" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3reym3fcjjvs18an402.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu3reym3fcjjvs18an402.png" alt="screenshot 2" width="652" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camera startup time&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: ~1.8s – 2.4s
&lt;/li&gt;
&lt;li&gt;After: ~0.7s – 1.1s
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Capture latency (tap → saved image)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: ~450ms – 700ms
&lt;/li&gt;
&lt;li&gt;After: ~120ms – 220ms
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Preview performance (low-end devices)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: frequent drops below ~40 FPS
&lt;/li&gt;
&lt;li&gt;After: stable ~55–60 FPS
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Memory behavior after multiple captures&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: noticeable spikes and GC pauses
&lt;/li&gt;
&lt;li&gt;After: much more stable usage
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GPS accuracy at capture moment&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: slight delay or drift
&lt;/li&gt;
&lt;li&gt;After: consistent and aligned with capture timing
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These weren’t lab tests. Just real-world usage across different Android devices.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you should take from this
&lt;/h2&gt;

&lt;p&gt;If you’re building something that involves real-time data, especially camera or GPS, you can’t just assume it’ll behave consistently across devices.&lt;/p&gt;

&lt;p&gt;You need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;test on low-end devices, not just your own phone
&lt;/li&gt;
&lt;li&gt;question delays and inconsistencies early
&lt;/li&gt;
&lt;li&gt;measure performance instead of guessing
&lt;/li&gt;
&lt;li&gt;separate UI concerns from system-level work
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because if you don’t, you’ll end up chasing bugs that are actually architectural decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  A few real examples
&lt;/h2&gt;

&lt;p&gt;There were moments where everything “worked”… but didn’t feel right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The capture felt slightly delayed
&lt;/li&gt;
&lt;li&gt;GPS data was just a bit off
&lt;/li&gt;
&lt;li&gt;Memory usage increased after repeated photos
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Individually, they didn’t seem critical. But together, they broke the experience.&lt;/p&gt;

&lt;p&gt;And that’s the tricky part.&lt;/p&gt;

&lt;p&gt;Small issues in isolation don’t look serious. But combined, they define how your app actually feels.&lt;/p&gt;




&lt;h2&gt;
  
  
  One thing I didn’t expect
&lt;/h2&gt;

&lt;p&gt;Simple-looking apps are often the most deceptive.&lt;/p&gt;

&lt;p&gt;Because you’re not solving what the user sees. You’re solving what happens underneath — timing, memory, hardware interaction.&lt;/p&gt;

&lt;p&gt;It’s a bit like trying to take a perfectly timed photo while multiple systems are slightly out of sync. Everything works… just not when it should.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you’re building something similar
&lt;/h2&gt;

&lt;p&gt;If you’re working on a camera, GPS, or real-time app, take a step back and ask yourself:&lt;/p&gt;

&lt;p&gt;Are you solving the visible problem… or the actual one?&lt;/p&gt;

&lt;p&gt;If you’re curious how this approach turned out in a real project, you can take a look here (just for reference, not a pitch): &lt;/p&gt;

&lt;p&gt;Spare some time and try it: &lt;a href="https://play.google.com/store/apps/details?id=com.neutrosolutions.freegpscamera" rel="noopener noreferrer"&gt;GPS Camera App&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: The link is shared purely to experience Flutter performance in a real-world scenario—it’s nearly on par with native (~98%). This project was developed for a client.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The biggest takeaway for me wasn’t just performance improvements.&lt;/p&gt;

&lt;p&gt;It was learning where abstraction helps… and where it gets in your way.&lt;/p&gt;

&lt;p&gt;And once you understand that, you start building very differently.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>android</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>I Made It Rain on Your Flutter Screen (Literally)</title>
      <dc:creator>Azeem Hassan</dc:creator>
      <pubDate>Thu, 09 Apr 2026 13:23:22 +0000</pubDate>
      <link>https://forem.com/azeemhassanch/i-made-it-rain-on-your-flutter-screen-literally-1f4i</link>
      <guid>https://forem.com/azeemhassanch/i-made-it-rain-on-your-flutter-screen-literally-1f4i</guid>
      <description>&lt;p&gt;It was raining. I had nothing specific to work on. I had this idea for a rain-on-glass effect — drops with reflections, physics, wind — and thought: &lt;em&gt;this doesn't exist for Flutter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Six nights later, it does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pub.dev/packages/rainy_day" rel="noopener noreferrer"&gt;rainy_day&lt;/a&gt; is a Flutter package that renders a hyper-realistic rain-on-glass effect over any background image you give it. Falling drops. Reflections inside every drop. Collisions. Wind gusts. Accelerometer-driven parallax. One widget, one line.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it and what actually made it hard.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;Drop this into your widget tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;RainWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;backgroundAsset:&lt;/span&gt; &lt;span class="s"&gt;'assets/images/background.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;blur:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;fps:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;enableCollisions:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;gravityThreshold:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;windIntensity:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;rainPresets:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;RainPreset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;RainPreset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;RainPreset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your screen becomes this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feifawvpbhys62sszx0nd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feifawvpbhys62sszx0nd.gif" alt="demo gif" width="600" height="1298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole API surface. Everything has sensible defaults. You don't need to manage a ticker, handle images, or touch a single canvas method.&lt;/p&gt;

&lt;p&gt;But building that API took several nights of genuinely frustrating debugging. Here's what actually tripped me up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 1: Reflections
&lt;/h2&gt;

&lt;p&gt;This was the thing I underestimated the most.&lt;/p&gt;

&lt;p&gt;Real drops on glass act like convex lenses — they refract what's behind them. So each drop in the scene shows a tiny, slightly distorted version of the background. Not the blurred version. The &lt;em&gt;sharp&lt;/em&gt; version, at scale.&lt;/p&gt;

&lt;p&gt;Flutter's &lt;code&gt;Canvas&lt;/code&gt; doesn't have a CSS-style background-clip shortcut. You can't just say "fill this path with a section of this image." You have to do it manually.&lt;/p&gt;

&lt;p&gt;What I ended up doing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At startup, scale down a copy of the original background image by a factor (default 5×). This is the "reflection image."&lt;/li&gt;
&lt;li&gt;At paint time, for every single drop, calculate where in the reflection image this drop's position maps to.&lt;/li&gt;
&lt;li&gt;Clip the canvas to the drop's Bézier path.&lt;/li&gt;
&lt;li&gt;Draw the mapped portion of the reflection image inside that clip region.&lt;/li&gt;
&lt;li&gt;Unclip, move to the next drop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The math to get the reflection coordinate mapping right was where I spent most of my time. Parallax offset bleeds into it. Image scaling bleeds into it. Get either one slightly wrong and every drop reflection looks like it's sampling from the wrong part of the scene.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 2: Drop Shapes
&lt;/h2&gt;

&lt;p&gt;I assumed drops were circles. They're not.&lt;/p&gt;

&lt;p&gt;A bead sitting still is roughly circular. But a drop that's moving fast is a teardrop — elongated in the direction of travel, compressed at the sides. And a drop mid-collision briefly shifts sideways and deforms before stabilizing.&lt;/p&gt;

&lt;p&gt;Three different Bézier path shapes depending on state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// tiny static bead — plain oval&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addOval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromCircle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;center:&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nl"&gt;radius:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colliding&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ySpeed&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// fast or colliding — stretched teardrop&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ySpeed&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;moveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cubicTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cubicTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// slow-moving — softer teardrop with asymmetric arcs&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;moveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cubicTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cubicTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These paths are recomputed every frame for every visible drop. When you're running 600+ drops at 60fps, that adds up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge 3: Performance
&lt;/h2&gt;

&lt;p&gt;My first version just called &lt;code&gt;setState&lt;/code&gt; on a timer. Rebuilds the entire widget tree 60 times per second. With hundreds of drops and canvas operations per frame.&lt;/p&gt;

&lt;p&gt;It was, generously: not great.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;CustomPainter&lt;/code&gt; that listens to a &lt;code&gt;ValueNotifier&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlassPainter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;CustomPainter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;GlassPainter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Listenable&lt;/span&gt; &lt;span class="n"&gt;repaint&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;repaint:&lt;/span&gt; &lt;span class="n"&gt;repaint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;shouldRepaint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GlassPainter&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// never rebuild the painter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;shouldRepaint&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; — always. The painter structure doesn't change; only the data does. Flutter skips the full widget rebuild and just calls &lt;code&gt;paint()&lt;/code&gt; directly on the canvas each tick.&lt;/p&gt;

&lt;p&gt;Beyond that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spatial collision grid&lt;/strong&gt;: drops only check cells adjacent to them, not every other drop (O(n) instead of O(n²))&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background blur via &lt;code&gt;ImageFilter&lt;/code&gt;&lt;/strong&gt;: applied at draw time on the canvas, not pre-blurred into a new image in memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection image pre-rendered once&lt;/strong&gt;: not recomputed per frame&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After all of this: smooth 60fps on a mid-range Android device.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: Wind That Feels Real
&lt;/h2&gt;

&lt;p&gt;Once physics was solid, I wanted wind. Not a constant sideways push — actual &lt;em&gt;gusts&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The implementation sums three sine waves at different frequencies and phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;_windForce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;windIntensity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0008&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0021&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0053&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result isn't random and isn't mechanical — it builds, peaks, recedes, and comes back differently each time. Exactly like actual gusts. Watching drops slowly drift sideways and then fall back down is probably my favorite thing in the whole package.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pubspec.yaml&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rainy_day&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget to declare your image asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets/images/background.jpg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:rainy_day/rainy_day.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Minimal usage&lt;/span&gt;
&lt;span class="n"&gt;RainWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;backgroundAsset:&lt;/span&gt; &lt;span class="s"&gt;'assets/images/background.jpg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Stormy&lt;/span&gt;
&lt;span class="n"&gt;RainWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;backgroundAsset:&lt;/span&gt; &lt;span class="s"&gt;'assets/images/background.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;windIntensity:&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;gravityAngleVariance:&lt;/span&gt; &lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;onControllerReady:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speedMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works on Android and iOS. MIT licensed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;pub.dev → &lt;a href="https://pub.dev/packages/rainy_day" rel="noopener noreferrer"&gt;pub.dev/packages/rainy_day&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub → &lt;a href="https://github.com/azeemhassanch/flutter_rainyday" rel="noopener noreferrer"&gt;github.com/azeemhassanch/flutter_rainyday&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find it useful, a ⭐ on GitHub or a 👍 on pub.dev goes a long way for discoverability. And if you build something with it — please share it, I genuinely want to see what you make.&lt;/p&gt;

&lt;p&gt;It started as a rainy-night rabbit hole. Ended up being one of the most satisfying things I've shipped this year.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
