<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Anmol's blog]]></title><description><![CDATA[Anmol's blog]]></description><link>https://blog.anmolthedeveloper.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 10 Oct 2026 16:52:29 GMT</lastBuildDate><atom:link href="https://blog.anmolthedeveloper.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Tracking From the Grave: How Apps Follow Your Location After They're Killed]]></title><description><![CDATA[The one idea to hold in your head


A background location app does NOT stay running the whole time. It cannot. Both Android and iOS will kill it. So instead of staying alive, it hands a few smart trig]]></description><link>https://blog.anmolthedeveloper.com/tracking-from-the-grave-how-apps-follow-your-location-after-they-re-killed</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/tracking-from-the-grave-how-apps-follow-your-location-after-they-re-killed</guid><category><![CDATA[Android]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[React Native]]></category><category><![CDATA[location ]]></category><category><![CDATA[gps]]></category><category><![CDATA[location tracking app]]></category><category><![CDATA[privacy]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Sat, 05 Sep 2026 08:28:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/fbd17b4f-fde6-4df6-904c-08bbccf24911.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>The one idea to hold in your head</strong></p>
</blockquote>
<blockquote>
<p>A background location app does NOT stay running the whole time. It cannot. Both Android and iOS will kill it. So instead of staying alive, it hands a few smart triggers to the phone's operating system and then lets itself die. The OS keeps those triggers and wakes the app back up at exactly the right second. Accuracy does not come from running 24x7. It comes from being clever about WHEN to wake up and switch on GPS.</p>
</blockquote>
<p>Read that twice. Once you accept that the app is supposed to die, everything else makes sense. Beginners usually assume the app is secretly running in a corner forever. It is not. It is mostly dead, and that is the whole trick.</p>
<h2>Before you start: there are three ways to build this</h2>
<p>This blog covers all three, so pick the one that fits you before you follow the code.</p>
<ol>
<li><p><strong>The plugin way</strong> (<code>flutter_background_geolocation</code>), covered in "Step by step: build this yourself in Flutter". Fastest to ship and the best battery behaviour. It is a paid plugin from Transistorsoft, free in debug builds, but a release build needs a license key tied to your package id or bundle id.</p>
</li>
<li><p><strong>The Dart-only way</strong> (<code>geolocator</code> plus your own foreground service), covered in the teardown section. Free, simpler, but weaker at surviving a force quit.</p>
</li>
<li><p><strong>The native way</strong> (your own Kotlin and Swift code over method and event channels), also in the teardown section. Free, most control, most work.</p>
</li>
</ol>
<p>So if you are building your own tracking from scratch, options 2 and 3 need no license key at all. The key is only for option 1.</p>
<h2>Why you cannot just keep the app running</h2>
<p>Imagine if any app could run GPS forever in the background. Your battery would be gone by lunch, and every shady app would be following you around. So phone makers put a hard stop on it. Two things happen:</p>
<ol>
<li><p><strong>The OS kills background apps.</strong> When your phone needs memory, or you swipe the app away, or it just decides your app has been idle too long, the operating system shuts your app down. You do not get a vote.</p>
</li>
<li><p><strong>GPS is expensive.</strong> The GPS chip is one of the hungriest things on the phone. Keep it on continuously and the battery melts.</p>
</li>
</ol>
<p>So the plugin works WITH these rules instead of fighting them. It assumes it will be killed, and it plans for resurrection. Think of it like leaving a note for a friend that says "wake me up when I cross this line." You can go to sleep. The friend (the OS) holds the note and shakes you awake at the right moment.</p>
<h2>The heart of it: be lazy to be accurate</h2>
<p>Here is the clever bit that makes both battery life AND accuracy possible at the same time. It sounds backwards but stay with me.</p>
<p>When you are <strong>standing still</strong>, GPS is turned OFF. The app drops an invisible circle around your current spot (this circle is called a <strong>geofence</strong>) and goes to sleep. The phone uses almost no battery. The app process can even die. Nobody cares, because nothing is moving.</p>
<p>The moment you <strong>start moving</strong> and step outside that circle, the OS notices and wakes the app up. Now the app turns on high-accuracy GPS and records every point while you travel. When you stop again, it drops a fresh circle, switches GPS off, and goes back to sleep.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/19bf859f-bf9e-447b-a706-f4d637ceab78.jpg" alt="" style="display:block;margin:0 auto" />

<p>So it is accurate BECAUSE it is lazy. It does not waste GPS while you sit at your desk, which keeps the battery happy, which means the OS does not feel the urge to kill it aggressively, which means it is alive and precise the moment you actually move. Battery saving and accuracy are the same feature here, not a trade off.</p>
<h2>Now the part everyone asks about: what happens when the app dies</h2>
<p>This is where Android and iOS go in totally different directions. Let me do them one at a time, simply.</p>
<h3>Android: a permanent worker plus notes held by the system</h3>
<p>While your app is alive in the background, Android keeps it tracking using something called a <strong>foreground service</strong>. You have seen it. It is that notification that says "App is using your location" which you cannot swipe away. That notification is not annoying-for-no-reason. It is literally the deal you make with Android: "show the user a notification, and in exchange I let you keep running and reading GPS in the background." No notification, no permission. That is the rule on modern Android.</p>
<p>This foreground service is set to restart itself if Android kills it for memory reasons. So far so good. But what about when your app process is FULLY dead? This is the magic part.</p>
<p>The plugin registers two kinds of "notes" with <strong>Google Play Services</strong>, which is a separate system app that does NOT die when your app dies:</p>
<ul>
<li><p><strong>Geofence triggers.</strong> "Hey Play Services, when this phone crosses this circle, wake my app."</p>
</li>
<li><p><strong>Activity Recognition triggers.</strong> "Hey Play Services, when this phone goes from still to walking or driving, wake my app."</p>
</li>
</ul>
<p>When one of those conditions happens, Play Services reaches into your dead app and cold-starts a tiny piece of it called a <strong>BroadcastReceiver</strong>. Your app gets a few seconds of life, the tracking engine wakes up, grabs the location, and does its job. Your Flutter and Dart code can be completely shut down and this still works. That is what people mean by <strong>headless</strong> mode. The native engine runs without the rest of your app being awake.</p>
<p>And if the phone reboots? The plugin also registers a "on boot" note, so when the phone turns back on, it re-arms everything automatically.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/b73618f7-cd0d-4236-8bc9-6ae43cfc164d.jpg" alt="" style="display:block;margin:0 auto" />

<h3>iOS: the system relaunches your app for you</h3>
<p>iOS is much stricter. You genuinely cannot run forever in the background, end of story. But Apple gives location tracking one special superpower: a couple of services are allowed to <strong>relaunch your app even after it has been force-quit, and even after a reboot</strong>. Almost nothing else on iOS can do this.</p>
<p>The two services are:</p>
<ul>
<li><p><strong>Region Monitoring</strong>, which is iOS's version of geofences. Cross a monitored circle and iOS relaunches your app in the background.</p>
</li>
<li><p><strong>Significant Location Change (SLC)</strong>, which fires when you move a meaningful distance, detected cheaply using cell towers and wifi instead of GPS.</p>
</li>
</ul>
<p>When one of these fires, iOS quietly relaunches your app in the background, hands it a little flag that says "you woke up because of location," gives you a short moment to act, and then suspends you again. Important detail: plain continuous GPS does NOT survive a force-quit on iOS by itself. So the plugin uses regions and SLC as the "wake me up" layer, and only switches on full precise GPS once it is already awake and senses real movement.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/046bd91a-86c5-4744-aa39-c62c60c1efcd.jpg" alt="" style="display:block;margin:0 auto" />

<p>This is also why, on iOS, the very first point after a kill can feel a little delayed. The app had to be relaunched by an event before it could even start tracking.</p>
<h2>"Terminated" is not just one thing</h2>
<p>A big source of confusion for beginners is treating "closed" as a single state. It is actually a ladder, and behaviour changes at each rung.</p>
<table>
<thead>
<tr>
<th>State</th>
<th>What it means</th>
<th>Does tracking survive?</th>
</tr>
</thead>
<tbody><tr>
<td>Backgrounded, still alive</td>
<td>You pressed home, app is in memory</td>
<td>Yes, easily (foreground service on Android, background mode on iOS)</td>
</tr>
<tr>
<td>Killed by the OS for memory</td>
<td>Phone needed RAM and shut your app</td>
<td>Yes (Android restarts it and geofence/motion triggers revive it, iOS relaunches via SLC/regions)</td>
</tr>
<tr>
<td>Force-quit by the user</td>
<td>You swiped it out of recents</td>
<td>Android can be harsher and may delay, but Play Services triggers still cold-start a receiver. iOS still relaunches via regions and SLC</td>
</tr>
<tr>
<td>After reboot</td>
<td>Phone was turned off and on</td>
<td>Yes (Android re-arms with a boot receiver, iOS regions and SLC persist)</td>
</tr>
</tbody></table>
<p>So when someone asks "does it work after the app is closed," the honest answer is "yes, but the mechanism that revives it depends on exactly how it was closed."</p>
<h2>The last piece: never lose a single point</h2>
<p>Accuracy is not only about capturing locations. It is about not dropping them. What if the phone is in a tunnel, or has no signal, or your server is down for a minute?</p>
<p>The plugin handles this by writing every location into a small local database (<strong>SQLite</strong>) on the phone FIRST, before doing anything else. Then a separate part of the system tries to upload those saved points to your server. If the upload fails because there is no internet, the points just wait in the local database and get uploaded later, in the correct order, once the connection is back.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/69231e2a-d2f4-4b00-9f0f-987a08dc7c8a.jpg" alt="" style="display:block;margin:0 auto" />

<p>Golden rule to copy for any tracking app: record locally first, sync as a totally separate concern. A dead zone should never punch a hole in your track.</p>
<h2>The full picture in one diagram</h2>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/bb27fdc5-3e64-4dca-99da-3cbd976396cf.jpg" alt="" style="display:block;margin:0 auto" />

<p>That loop is the entire product. Sleep cheaply, wake on a real signal, track precisely while moving, save everything, repeat.</p>
<hr />
<h1>Step by step: build this yourself in Flutter</h1>
<p>This section is a complete walk through using the <code>flutter_background_geolocation</code> package, since that is the battle-tested way to get all of the above without writing the native code yourself. Follow it top to bottom.</p>
<h2>Step 0: understand what you are signing up for</h2>
<p>This package is free to fully test in DEBUG builds. For a RELEASE build on Android, and for the App Store on iOS, you need a paid license key tied to your app's package id (Android) or bundle id (iOS). So you can build and test everything for free, and only pay when you ship to production. Plan for that.</p>
<h2>Step 1: add the package</h2>
<p>In your <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dependencies:
  flutter_background_geolocation: ^4.16.0
  # the package also needs its companion, added automatically in most cases
</code></pre>
<p>Then run <code>flutter pub get</code>. Always check pub.dev for the latest version number rather than copying mine.</p>
<h2>Step 2: Android setup</h2>
<h3>2a. Permissions in AndroidManifest.xml</h3>
<p>Inside <code>android/app/src/main/AndroidManifest.xml</code>, above the <code>&lt;application&gt;</code> tag, declare what you need. Each line has a reason:</p>
<pre><code class="language-xml">&lt;uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /&gt;
&lt;uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /&gt;
&lt;!-- lets you track while the app is in the background --&gt;
&lt;uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /&gt;
&lt;!-- the permanent notification that keeps you alive --&gt;
&lt;uses-permission android:name="android.permission.FOREGROUND_SERVICE" /&gt;
&lt;uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /&gt;
&lt;!-- detect still vs walking vs driving --&gt;
&lt;uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /&gt;
&lt;!-- re-arm tracking after the phone reboots --&gt;
&lt;uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /&gt;
&lt;uses-permission android:name="android.permission.WAKE_LOCK" /&gt;
</code></pre>
<h3>2b. License key (only needed for release)</h3>
<p>Inside the <code>&lt;application&gt;</code> tag, add your license key as a meta-data line:</p>
<pre><code class="language-xml">&lt;meta-data
  android:name="com.transistorsoft.locationmanager.license"
  android:value="YOUR_LICENSE_KEY" /&gt;
</code></pre>
<h3>2c. Gradle repository</h3>
<p>The package pulls its native engine from Maven. In most recent versions this is wired up for you, but if a build fails saying it cannot find <code>tslocationmanager</code>, make sure your <code>android/build.gradle</code> (or <code>settings.gradle</code>) has <code>mavenCentral()</code> and the JitPack repo listed under repositories.</p>
<h2>Step 3: iOS setup</h2>
<h3>3a. Info.plist text</h3>
<p>In <code>ios/Runner/Info.plist</code>, add the permission descriptions. iOS shows these sentences to the user, so write them in plain language explaining WHY:</p>
<pre><code class="language-xml">&lt;key&gt;NSLocationWhenInUseUsageDescription&lt;/key&gt;
&lt;string&gt;We use your location to track field visits while you use the app.&lt;/string&gt;
&lt;key&gt;NSLocationAlwaysAndWhenInUseUsageDescription&lt;/key&gt;
&lt;string&gt;We track your location in the background to log field activity.&lt;/string&gt;
&lt;key&gt;NSMotionUsageDescription&lt;/key&gt;
&lt;string&gt;We use motion data to save battery while tracking.&lt;/string&gt;
</code></pre>
<h3>3b. Background modes</h3>
<p>Still in <code>Info.plist</code>, switch on the background abilities:</p>
<pre><code class="language-xml">&lt;key&gt;UIBackgroundModes&lt;/key&gt;
&lt;array&gt;
  &lt;string&gt;location&lt;/string&gt;
  &lt;string&gt;fetch&lt;/string&gt;
&lt;/array&gt;
</code></pre>
<p>You can also enable these by ticking "Location updates" and "Background fetch" under Signing and Capabilities in Xcode.</p>
<h2>Step 4: ask for permission the right way</h2>
<p>Do not ask for background location on the very first screen. Both stores dislike that and users reject it. Ask for "while in use" first, explain why in your own UI, and only then request "always." The plugin will request what it needs when you call <code>ready</code>, but a gentle in-app explanation screen before that hugely improves acceptance.</p>
<h2>Step 5: configure and start tracking (the Dart part)</h2>
<p>Here is a minimal but complete setup. Read the comments, they explain each choice.</p>
<pre><code class="language-dart">import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;

Future&lt;void&gt; initTracking() async {
  // 1. Listen for each new location
  bg.BackgroundGeolocation.onLocation((bg.Location location) {
    print('New location: ${location.coords.latitude}, ${location.coords.longitude}');
  });

  // 2. Listen for the still &lt;-&gt; moving switch
  bg.BackgroundGeolocation.onMotionChange((bg.Location location) {
    print('Moving state changed. isMoving = ${location.isMoving}');
  });

  // 3. Configure the engine
  await bg.BackgroundGeolocation.ready(bg.Config(
    desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH, // best GPS while moving
    distanceFilter: 10,            // record a point every 10 meters of movement
    stopOnTerminate: false,        // keep tracking after the app is killed
    startOnBoot: true,             // re-arm after the phone reboots
    enableHeadless: true,          // allow waking with no UI alive
    foregroundService: true,       // the persistent notification on Android
    url: 'https://your-server.com/locations', // where to upload points
    autoSync: true,                // upload automatically
    batchSync: false,              // send points one by one (or batch them)
  )).then((bg.State state) {
    if (!state.enabled) {
      // 4. Actually start it
      bg.BackgroundGeolocation.start();
    }
  });
}
</code></pre>
<p>The two settings that make it survive being killed are <code>stopOnTerminate: false</code> and <code>startOnBoot: true</code>. The one that makes it work with no UI is <code>enableHeadless: true</code>.</p>
<h2>Step 6: handle the headless wake-up (Android)</h2>
<p>When the OS wakes your dead app, your normal Dart code is not running. You register a separate tiny function that the engine can call in that headless moment. Put this at the very top level of your <code>main.dart</code>, outside any class.</p>
<pre><code class="language-dart">// must be a top-level function
@pragma('vm:entry-point')
void headlessTask(bg.HeadlessEvent headlessEvent) async {
  switch (headlessEvent.name) {
    case bg.Event.LOCATION:
      bg.Location location = headlessEvent.event;
      print('Headless location: $location');
      break;
    case bg.Event.MOTIONCHANGE:
      print('Headless motionchange');
      break;
  }
}

void main() {
  runApp(const MyApp());
  // register the headless handler
  bg.BackgroundGeolocation.registerHeadlessTask(headlessTask);
}
</code></pre>
<p>The <code>@pragma('vm:entry-point')</code> line is not optional. It tells Flutter not to throw this function away during release optimization, because the engine needs to find it by name when the app is otherwise dead.</p>
<h2>Step 7: test it properly</h2>
<p>Do not just test by leaving the app open. That proves nothing. Real tests:</p>
<ol>
<li><p>Start tracking, then swipe the app out of recents. Walk around the block. Check your server received points.</p>
</li>
<li><p>Reboot the phone without opening the app. Move. Confirm tracking re-armed itself.</p>
</li>
<li><p>Sit still for ten minutes and watch the battery. It should barely move. If GPS is stuck on, your config is wrong.</p>
</li>
<li><p>Turn on airplane mode, move around, then turn internet back on. Confirm the queued points upload in order.</p>
</li>
</ol>
<h2>Step 8: survive the cheap-phone problem (very important in India)</h2>
<p>Many budget Android phones (Xiaomi, Oppo, Vivo, Realme and others) run aggressive "battery saver" software that kills background apps no matter what the rules say. This is the number one reason field-tracking apps fail in the real world here. Defenses:</p>
<ul>
<li><p>Guide the user to disable battery optimization for your app. The plugin has a helper, <code>bg.DeviceSettings</code>, that can open the right settings screen.</p>
</li>
<li><p>For company-owned devices, look into Android <strong>Device Owner</strong> or a managed enrollment. When your app is the device owner, the OEM battery killers no longer apply, and force-quit and optimization simply stop being a problem. For a controlled fleet like field agents, this is the most reliable answer by far.</p>
</li>
<li><p>Always show the user a clear one-time screen explaining they must allow background location and turn off battery optimization, or tracking will be unreliable.</p>
</li>
</ul>
<h2>Quick mental checklist</h2>
<ol>
<li><p>The app is meant to die. Plan for resurrection, do not fight it.</p>
</li>
<li><p>Sleep with GPS off behind a geofence. Wake on geofence exit or motion.</p>
</li>
<li><p>Android revives via a foreground service plus Play Services geofence and activity triggers, re-armed on boot.</p>
</li>
<li><p>iOS revives via Region Monitoring and Significant Location Change, which can relaunch even after force-quit.</p>
</li>
<li><p>Save every point to local SQLite first, upload as a separate retrying step.</p>
</li>
<li><p><code>stopOnTerminate: false</code>, <code>startOnBoot: true</code>, <code>enableHeadless: true</code> are the three settings that matter most.</p>
</li>
<li><p>On cheap Android phones, defeat the OEM battery killer or your tracking dies silently.</p>
</li>
</ol>
<hr />
<h2>When does the foreground service actually show up? (clearing the confusion)</h2>
<p>This part confuses almost everyone, so let us nail it down. First, remember one thing: <strong>that notification IS the foreground service.</strong> They are the same thing wearing two faces. Notification visible means the service is running. Notification gone means the service has stopped.</p>
<h3>When it starts</h3>
<p>The foreground service starts only when the app is <strong>actively tracking</strong>, which means when the person is in the <strong>MOVING</strong> state. The flow looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/d47b0825-e5d2-4231-9a20-1309bad0f269.jpg" alt="" style="display:block;margin:0 auto" />

<p>So the notification is not meant to be visible all the time. It comes and goes depending on whether the person is moving. This is not a bug. It is deliberate, to save battery. When the person is sitting still, both GPS and the service shut down, the phone sleeps, and the battery is safe.</p>
<h3>If it stops (notification disappears), is that a problem?</h3>
<p>No. This is the real source of the confusion. The notification disappearing does NOT mean tracking has died. It only means the app has gone to sleep (stationary mode). The operating system still holds the two triggers, the geofence and the motion trigger. The moment the person moves again, the OS wakes the app, the service starts again, and the notification comes back.</p>
<table>
<thead>
<tr>
<th>Notification state</th>
<th>What it actually means</th>
</tr>
</thead>
<tbody><tr>
<td>Visible</td>
<td>The app is actively tracking with GPS (moving)</td>
</tr>
<tr>
<td>Gone</td>
<td>The app is asleep (stationary), waiting for a trigger. Tracking is NOT dead</td>
</tr>
</tbody></table>
<p>So during testing, if you notice the notification vanish, do not panic. Move around and it will reappear. Tracking is still alive the whole time, it is just sleeping cheaply.</p>
<h3>If you want the notification to stay visible always</h3>
<p>If you do not like this come-and-go behavior and prefer the user to always see that tracking is on, the plugin can be configured so the foreground service runs continuously the entire time tracking is enabled. Then the notification stays permanent. The trade off is more battery usage.</p>
<p>So there are two modes to choose between:</p>
<ul>
<li><p><strong>Battery friendly:</strong> notification appears only while moving. Best battery life.</p>
</li>
<li><p><strong>Always visible:</strong> notification stays up the whole time tracking is enabled. More transparent and more reliable on aggressive phones, but uses more battery.</p>
</li>
</ul>
<p>One caution: on newer Android (12 and above) Google restricts starting a foreground service from the background, so the exact come-and-go behavior can vary a little by Android version and plugin version. The plugin handles this internally, but always test on your real target phones to confirm the notification behaves the way you expect.</p>
<hr />
<h1>When does the foreground service start, and is it a problem if it stops?</h1>
<p>This part confuses almost everyone, so let us settle it cleanly. First, the single most important fact:</p>
<blockquote>
<p><strong>That notification IS the foreground service.</strong> They are the same thing in two forms. Notification visible means the service is running. Notification gone means the service has stopped. There is no hidden third state.</p>
</blockquote>
<h2>When does it start?</h2>
<p>The foreground service starts only when the plugin is <strong>actively tracking with GPS</strong>, which is when the person is in the <strong>moving</strong> state. It does not run all the time. Here is the lifecycle:</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/e1d7dcb7-48d2-4963-bf3b-0338b120f749.jpg" alt="" style="display:block;margin:0 auto" />

<p>So the notification naturally comes and goes depending on whether the person is moving. This is not a bug. It is deliberate, and it is exactly how the battery gets saved. When the agent is sitting still, both GPS and the service shut down, the phone sleeps, and almost no battery is used.</p>
<h2>Is it a problem when the notification disappears?</h2>
<p>No. This is the key point that causes the confusion. The notification disappearing does <strong>not</strong> mean tracking has died. It only means the app has gone to sleep in stationary mode. The two wake-up triggers (the geofence and the motion sensor) are still armed and held by the operating system. The moment the person moves again, the OS wakes the app, the service restarts, and the notification comes back on its own.</p>
<table>
<thead>
<tr>
<th>Notification state</th>
<th>What it actually means</th>
</tr>
</thead>
<tbody><tr>
<td>Visible</td>
<td>App is actively tracking with GPS (moving)</td>
</tr>
<tr>
<td>Gone</td>
<td>App is asleep (stationary), waiting for a trigger, tracking is NOT dead</td>
</tr>
</tbody></table>
<p>So during testing, if you see the notification vanish while standing still, do not panic. Move around and it will reappear. Tracking was alive the whole time, just sleeping cheaply.</p>
<h2>If you want the notification to stay visible all the time</h2>
<p>If this come-and-go behaviour is not what you want, and you would prefer the user always sees that tracking is on, the plugin can be configured to keep the foreground service running continuously for as long as tracking is enabled. The notification then stays permanent. The trade-off is higher battery usage.</p>
<p>So you are choosing between two modes:</p>
<ol>
<li><p><strong>Battery-friendly mode:</strong> notification appears only while moving, disappears while still. Best battery life.</p>
</li>
<li><p><strong>Always-on mode:</strong> notification stays visible the entire time tracking is enabled. Higher battery use, but more transparent and more resistant to being killed.</p>
</li>
</ol>
<p>One caveat worth knowing: on newer Android (12 and above) Google restricts starting a foreground service from the background, so the exact behaviour depends on the Android version and plugin version. The plugin handles this internally, but always test on your real target devices to confirm the notification behaves as expected.</p>
<hr />
<h1>A different way real apps do this (a teardown story)</h1>
<p>Everything above uses <code>flutter_background_geolocation</code>, which is the gold standard. But when you actually pull apart other tracking apps in the wild, many of them do NOT use it. They build a simpler version by hand. It helps to understand that version too, because you will meet it far more often than you expect, and it comes with very different trade-offs.</p>
<p>Here is the common pattern you find inside a lot of HR and field-attendance apps once you look under the hood.</p>
<p>There are actually two different ways to build this simpler version. One stays entirely in Dart and leans on the <code>geolocator</code> plugin. The other drops down into native Android and iOS code and talks to Flutter through channels, which hands you far more control and lets you read signals the plugin never surfaces. Let us walk through both, one after the other.</p>
<h2>Way 1: The Dart-only approach (the geolocator plugin)</h2>
<p>Instead of the clever geofence-sleep engine, these apps use the basic <code>geolocator</code> package to read positions, and wrap it in their own foreground service that simply stays on the whole time tracking is enabled. There is no "sleep behind a geofence" trick. The service starts, holds a wake lock, and streams positions on a distance filter until tracking is turned off.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/e8d88080-b0d0-4f54-b3a5-993e55833696.jpg" alt="" style="display:block;margin:0 auto" />

<p>Why do people build it this way? Because it is simple, it has no license cost, and for many use cases you do not actually need the app to survive a full swipe-away for days. You just need reliable tracking during an active shift, which this handles fine.</p>
<p>The trade-offs you must know:</p>
<ul>
<li><p>It burns more battery, because GPS and the service run continuously instead of sleeping behind a geofence.</p>
</li>
<li><p>Surviving a hard force-quit is weaker. These apps lean on<code>stopWithTask=false</code>, a boot receiver and a battery-optimisation exemption, but they do not have the Play Services geofence resurrection trick. So a fully killed process can simply stop until the user opens the app again.</p>
</li>
<li><p>It is much easier to reason about and debug, which is a real advantage when you are shipping fast.</p>
</li>
</ul>
<blockquote>
<p><strong>Tip</strong></p>
</blockquote>
<blockquote>
<p>If your tracking only needs to be reliable during a known active window, like a shift, a delivery, or a site visit, the simple always-on service is often the pragmatic choice. Save the full geofence engine for true all-day passive tracking where battery life is the main worry.</p>
</blockquote>
<h2>Way 2: The native approach with method and event channels</h2>
<p>The Dart-only way is easy, but it can only give you what the plugin chooses to expose. When you want full control, you drop into native code. You write the location logic in Kotlin on Android and Swift on iOS, run your own foreground service, and send the results up to Flutter through a channel. This is the approach a serious field-ops app usually lands on, because the richest signals live in the native layer, not in Dart.</p>
<h3>Method channel versus event channel, in one line each</h3>
<ul>
<li><p>A <strong>MethodChannel</strong> is a request and a response. Dart calls something like<code>startTracking</code>, native runs it and replies once. It is made for commands: start, stop, or ask a one-time question.</p>
</li>
<li><p>An <strong>EventChannel</strong> is a continuous stream. Native keeps pushing values, and Dart keeps listening. This is the perfect fit for a live location feed, because points arrive again and again.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/7bd742dd-38dd-4670-81a7-37353475d301.jpg" alt="" style="display:block;margin:0 auto" />

<h3>The signals you can grab natively, and why they matter</h3>
<p>Once you are in native code, each location update can carry much more than latitude and longitude. These are exactly the extra fields that make a track trustworthy and easy to debug:</p>
<ul>
<li><p><strong>Speed</strong> in metres per second, taken straight from the GPS fix. Handy to catch impossible jumps and to tell walking from driving.</p>
</li>
<li><p><strong>isMocked</strong>, the real spoof flag, read right at the source instead of through a wrapper.</p>
</li>
<li><p><strong>Device info</strong> like model and manufacturer, so you can spot the cheap OEM phones that love to kill background apps.</p>
</li>
<li><p><strong>Battery level and charging state</strong>, so your server understands that a dead battery, not a bug, is why the points stopped arriving.</p>
</li>
</ul>
<p>Bundling all of these into every event turns a plain coordinate into a record you can actually trust.</p>
<h3>Android native (Kotlin)</h3>
<p>Inside your own foreground service, you use the FusedLocationProviderClient. On each update, you build a map with the extra signals and push it into the EventChannel sink.</p>
<pre><code class="language-kotlin">// inside your location EventChannel StreamHandler
private val locationCallback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
        val location = result.lastLocation ?: return

        // read the spoof flag at the source
        val isMock = if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
            location.isMock              // API 31 and above
        } else {
            @Suppress("DEPRECATION")
            location.isFromMockProvider  // older devices
        }

        // battery level right now
        val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        val batteryLevel = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)

        val data = mapOf(
            "latitude" to location.latitude,
            "longitude" to location.longitude,
            "speed" to location.speed,          // metres per second
            "accuracy" to location.accuracy,
            "isMocked" to isMock,
            "deviceModel" to Build.MODEL,
            "manufacturer" to Build.MANUFACTURER,
            "batteryLevel" to batteryLevel,
            "timestamp" to location.time
        )

        // send this event up to Dart
        eventSink?.success(data)
    }
}
</code></pre>
<h3>iOS native (Swift)</h3>
<p>On iOS you use CLLocationManager. The mock story is different here. Plain iOS does not hand you a mock provider flag the way Android does, but from iOS 15 you can check whether a fix was produced by a simulator through the location's sourceInformation.</p>
<pre><code class="language-swift">func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }

    // spoof hint on iOS 15 and above
    var isMocked = false
    if #available(iOS 15.0, *) {
        isMocked = location.sourceInformation?.isSimulatedBySoftware ?? false
    }

    // battery level right now
    UIDevice.current.isBatteryMonitoringEnabled = true
    let batteryLevel = Int(UIDevice.current.batteryLevel * 100)

    let data: [String: Any] = [
        "latitude": location.coordinate.latitude,
        "longitude": location.coordinate.longitude,
        "speed": location.speed,             // metres per second, -1 if unknown
        "accuracy": location.horizontalAccuracy,
        "isMocked": isMocked,
        "deviceModel": UIDevice.current.model,
        "batteryLevel": batteryLevel,
        "timestamp": location.timestamp.timeIntervalSince1970
    ]

    // send this event up to Dart
    eventSink?(data)
}
</code></pre>
<h3>The Dart side, just listening</h3>
<p>On the Flutter side you do almost nothing. You open the same two channels and listen to the stream. All the heavy lifting already happened natively, so Dart only reads a clean event.</p>
<pre><code class="language-dart">class NativeTracking {
  static const _commands = MethodChannel('app/tracking/commands');
  static const _events = EventChannel('app/tracking/events');

  Future&lt;void&gt; start() =&gt; _commands.invokeMethod('startTracking');
  Future&lt;void&gt; stop() =&gt; _commands.invokeMethod('stopTracking');

  Stream&lt;Map&lt;String, dynamic&gt;&gt; get locationStream =&gt;
      _events.receiveBroadcastStream().map((e) =&gt; Map&lt;String, dynamic&gt;.from(e));
}

// usage
final tracking = NativeTracking();
tracking.locationStream.listen((data) {
  if (data['isMocked'] == true) {
    // reject, native already flagged this point as fake
    return;
  }
  print('lat ${data['latitude']}, speed ${data['speed']}, '
      'battery ${data['batteryLevel']}, device ${data['deviceModel']}');
});
</code></pre>
<blockquote>
<p><strong>Tip</strong></p>
</blockquote>
<blockquote>
<p>Do the mock check, the speed sanity check, and the device and battery reads in native code, not in Dart. That is where the real signals live, and it means the event that reaches Dart is already clean and trustworthy. The Dart layer should just consume a validated event, not try to police it after the fact.</p>
</blockquote>
<h3>Which way should you pick?</h3>
<table>
<thead>
<tr>
<th>You want</th>
<th>Go with</th>
</tr>
</thead>
<tbody><tr>
<td>Fastest to ship, standard needs</td>
<td>Way 1, the geolocator plugin in pure Dart</td>
</tr>
<tr>
<td>Full control, richer signals, your own foreground service and spoof logic</td>
<td>Way 2, native code through method and event channels</td>
</tr>
</tbody></table>
<p>Most apps start on Way 1 and move to Way 2 only when they hit a wall, like needing a signal the plugin does not expose, or needing tighter control over the service and the anti-spoof checks.</p>
<h2>The piece the earlier sections skipped: stopping fake GPS</h2>
<p>Everything above quietly assumed that the location you receive is honest. In the real world, especially for attendance and field apps, it often is not. An employee can install a fake-GPS app and punch in from home while the map shows the office. If you are building anything where location decides money, like attendance, travel allowance, or delivery proof, you have to defend against this or the whole feature is meaningless.</p>
<p>Every Android location object carries a simple flag that tells you whether it came from a mock provider. In the <code>geolocator</code> package, each <code>Position</code> exposes <code>isMocked</code>. Checking it is the cheapest first line of defence you can add.</p>
<pre><code class="language-dart">import 'package:geolocator/geolocator.dart';

Future&lt;void&gt; readLocation() async {
  Position position = await Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.high,
  );

  // Android marks positions that came from a fake GPS app
  if (position.isMocked) {
    // do not trust this point, flag the user and reject it
    print('Mocked location detected, rejecting this point');
    return;
  }

  print('Trusted location: ${position.latitude}, ${position.longitude}');
}
</code></pre>
<p>Real apps go further and refuse to run at all on devices that make spoofing easy:</p>
<ul>
<li><p>Rooted or jailbroken devices are blocked, because root makes faking location trivial and hard to detect.</p>
</li>
<li><p>Emulators are blocked, since an emulator can report any coordinate you type into it.</p>
</li>
<li><p>Developer Mode being switched on is flagged, because most mock-location apps need it enabled.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/5f3c6e45-3d6b-4012-9024-8b25516172cd.jpg" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p><strong>Tip</strong></p>
</blockquote>
<blockquote>
<p><code>isMocked</code> is necessary but not enough on its own. A rooted device can hide the mock flag entirely. That is why serious apps combine the flag with root and emulator checks, and add a server-side sanity check too, like rejecting impossible speed between two points or a location that jumps across cities in a few seconds. Trust a position only after it clears all of these gates.</p>
</blockquote>
<h2>Live tracking versus store and forward</h2>
<p>Earlier we said: save every point to SQLite first, then sync it up separately. That is the right pattern for route logging, where you care about a complete and gap-free history.</p>
<p>But some apps have a different need. An admin wants to watch a live map and see field agents move in real time, right now. For that, the store-and-forward queue is too slow. These apps push each position straight into a realtime database, and Firebase Realtime Database is the common choice, so the admin dashboard updates the instant a new point arrives.</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Best for</th>
<th>Latency</th>
<th>Offline behaviour</th>
</tr>
</thead>
<tbody><tr>
<td>Store and forward (local SQLite queue)</td>
<td>Route history, reports, payroll proof</td>
<td>Seconds to minutes</td>
<td>Points wait safely and sync later in order</td>
</tr>
<tr>
<td>Live push (realtime database)</td>
<td>An admin watching a live map</td>
<td>Almost instant</td>
<td>Needs a fallback, a purely live point can be lost with no connection</td>
</tr>
</tbody></table>
<p>The two are not enemies. A robust app can do both at once: push live for the dashboard, and still keep a local SQLite copy so a dead zone never punches a hole in the recorded history. Live for the eyes, local for the record.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/dcfad1b8-f14d-4dfb-b69c-25fb2f30246d.jpg" alt="" style="display:block;margin:0 auto" />

<h2>What to copy and what to avoid from real apps</h2>
<p>Good ideas worth stealing from apps that ship this successfully:</p>
<ul>
<li><p>Check <code>isMocked</code> on every point and reject mocked ones.</p>
</li>
<li><p>Block, or at least flag, rooted and emulator devices for any money-critical tracking.</p>
</li>
<li><p>Consider a live push to a realtime database when someone genuinely needs to watch a map, but keep the local queue as well.</p>
</li>
<li><p>Use <code>stopWithTask=false</code> plus a boot receiver so a swipe-away or a reboot does not silently end a shift.</p>
</li>
</ul>
<p>Mistakes worth avoiding, all of which show up in real shipped apps if you look closely:</p>
<ul>
<li><p>Shipping cleartext HTTP (<code>usesCleartextTraffic=true</code>) so that location data travels unencrypted. Always send coordinates over HTTPS.</p>
</li>
<li><p>Leaving a developer's local test URL, like an <code>http://192.168.x.x</code> endpoint, hardcoded in the release build.</p>
</li>
<li><p>Hardcoding unrestricted map API keys in the app. Restrict every key by package name and signing certificate, and by the specific API it is allowed to call, or someone will run up your billing.</p>
</li>
<li><p>Trusting the phone's location blindly with no spoof defence at all.</p>
</li>
</ul>
<blockquote>
<p><strong>The one line to remember from this teardown</strong></p>
</blockquote>
<blockquote>
<p>The fancy plugin is not the only way, and it is not always the right way. Pick your architecture from the need. Simple always-on service for a bounded shift, full geofence engine for all-day passive tracking. And whatever you build, if location decides money, never trust a coordinate until it has proven it is not faked.</p>
</blockquote>
<hr />
<h1>The third wake-up style: timer-driven periodic sampling</h1>
<p>So far the blog has shown two ways an app gets woken up. The geofence and motion approach, where the OS wakes you because you moved. And the continuous foreground service, where you simply stay awake the whole time. There is a third style you will meet in real apps, and it answers a very specific question: "the phone is in deep sleep, but I still want a location every few minutes." This is called timer-driven or periodic sampling, and it is built from two Android pieces working together, <code>SCHEDULE_EXACT_ALARM</code> and <code>WAKE_LOCK</code>.</p>
<h2>First, why a normal timer does not work</h2>
<p>Your instinct might be to write a simple loop or a <code>Timer</code> that fires every five minutes and grabs a location. In the background this quietly fails, and it is important to understand why.</p>
<p>When the phone goes idle with the screen off, it enters a power saving state called <strong>Doze</strong>. In Doze the CPU is put to sleep to save battery. While that happens, your app's own timers, <code>Future.delayed</code> calls, and background loops are all <strong>frozen</strong>. They do not fire until something wakes the CPU. Your app process can even be killed entirely. So a plain "every five minutes" timer inside your app is simply not running when the phone is asleep, which is exactly when you needed it.</p>
<blockquote>
<p><strong>The key realisation</strong></p>
</blockquote>
<blockquote>
<p>Anything that must fire while the phone sleeps cannot live inside your app's own timers. It has to be handed to the operating system, because the OS does not sleep the way your app does. That is what <code>AlarmManager</code> is for.</p>
</blockquote>
<h2>Piece one: SCHEDULE_EXACT_ALARM, the part that wakes the phone</h2>
<p><code>AlarmManager</code> is a system service that runs a small piece of your code at a chosen time, whether your app is alive or dead. Think of it as leaving an alarm clock with the operating system instead of trying to stay awake yourself.</p>
<p>There are two flavours, and the difference matters:</p>
<ul>
<li><p>A <strong>normal (inexact) alarm</strong> gets batched and delayed by Doze to save battery. A five minute alarm might actually fire twenty minutes later. For location sampling this is useless.</p>
</li>
<li><p>An <strong>exact alarm</strong>, using <code>setExactAndAllowWhileIdle</code>, is allowed to fire on time even in Doze.</p>
</li>
</ul>
<p>To use exact alarms on Android 12 and above (API 31), you must declare the <code>SCHEDULE_EXACT_ALARM</code> permission. Without it the system only gives you the inexact kind. This is exactly why field-tracking apps request it.</p>
<blockquote>
<p><strong>A catch worth knowing</strong></p>
</blockquote>
<blockquote>
<p>An exact alarm is one-shot. It fires once and forgets. So every time it fires, you must schedule the next one yourself, or the cycle stops after a single tick. The pattern is: wake up, do the work, then set the next alarm before going back to sleep.</p>
</blockquote>
<h2>Piece two: WAKE_LOCK, the part that keeps the phone awake long enough</h2>
<p>Here is the subtle part that trips people up. When your alarm fires, the system gives your <code>BroadcastReceiver</code> only a very short window to run, roughly ten seconds, and it expects you to finish quickly.</p>
<p>But getting a GPS fix is slow and asynchronous. A cold fix can take five to thirty seconds. If you request a location and then let your receiver return, the CPU goes straight back to sleep <strong>before the fix arrives</strong>. Your location callback never completes, or your upload dies halfway.</p>
<p>The fix is to grab a <strong>partial wake lock</strong> the moment your receiver runs. A partial wake lock keeps the CPU awake while the screen stays off, using very little battery. You hold it while you request the location, wait for the fix, save it, and upload it. Then you <strong>release</strong> it so the phone can sleep again.</p>
<blockquote>
<p><strong>One line to remember</strong></p>
</blockquote>
<blockquote>
<p>The alarm wakes the phone at the right second. The wake lock keeps it awake just long enough to finish the job. One is about when, the other is about how long.</p>
</blockquote>
<h2>The full loop</h2>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/fc707193-0eec-489b-bb74-a8c0b94c3a6d.jpg" alt="" style="display:block;margin:0 auto" />

<h2>What it looks like in Kotlin</h2>
<pre><code class="language-kotlin">// 1. Schedule the next wake-up
fun scheduleNextWakeup(context: Context) {
    val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, LocationAlarmReceiver::class.java)
    val pi = PendingIntent.getBroadcast(
        context, 0, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )
    val triggerAt = System.currentTimeMillis() + 5 * 60 * 1000 // 5 minutes later

    // exact, and allowed to fire even in Doze (needs SCHEDULE_EXACT_ALARM)
    am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi)
}

// 2. The receiver: stay awake, get a location, re-arm, then sleep
class LocationAlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        val wakeLock = pm.newWakeLock(
            PowerManager.PARTIAL_WAKE_LOCK, "app:LocationWakeLock"
        )
        wakeLock.acquire(60_000) // safety timeout, hold at most 60 seconds

        val fused = LocationServices.getFusedLocationProviderClient(context)
        fused.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
            .addOnSuccessListener { location -&gt;
                saveAndUpload(location) // your own save plus upload
            }
            .addOnCompleteListener {
                scheduleNextWakeup(context)          // re-arm the next alarm
                if (wakeLock.isHeld) wakeLock.release() // let the phone sleep
            }
    }
}
</code></pre>
<h2>Two caveats you must plan for</h2>
<ul>
<li><p><strong>Doze rate-limits even exact alarms.</strong> In deep Doze, <code>setExactAndAllowWhileIdle</code> will reliably fire only about once every nine minutes or so. So you cannot get a location every thirty seconds this way. For short intervals or continuous tracking you still need a foreground service. This is why real apps often combine both: a foreground service for dense tracking while the person is active, and alarm-based sampling for occasional check-ins while the phone is idle.</p>
</li>
<li><p><strong>Battery optimisation can block it.</strong> Requesting <code>REQUEST_IGNORE_BATTERY_OPTIMIZATIONS</code> and guiding the user to allow it makes these alarms fire far more reliably, especially on the aggressive cheap OEM phones we keep coming back to.</p>
</li>
</ul>
<h2>So where does this fit among the three styles?</h2>
<table>
<thead>
<tr>
<th>Wake-up style</th>
<th>What triggers a location</th>
<th>Best for</th>
</tr>
</thead>
<tbody><tr>
<td>Geofence and motion (the plugin way)</td>
<td>You physically move</td>
<td>All-day passive tracking with great battery life</td>
</tr>
<tr>
<td>Continuous foreground service</td>
<td>Nothing, it never sleeps</td>
<td>Dense, precise tracking during an active shift</td>
</tr>
<tr>
<td>Timer-driven periodic sampling</td>
<td>A clock, every few minutes</td>
<td>Occasional check-ins while the phone is idle, cheaper than staying awake</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>The takeaway</strong></p>
</blockquote>
<blockquote>
<p>Exact alarms plus a wake lock are the tool when you want a location on a schedule rather than on movement. The alarm handles when to wake, the wake lock handles staying awake long enough to finish, and you re-arm the alarm on every tick. Just remember Doze will not let you sample faster than roughly every nine minutes, so pair it with a foreground service whenever you need something tighter.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[How to Open-Source Your Flutter Plugin and Still Hide the Code That Pays You]]></title><description><![CDATA[Open any package on pub.dev and you can read every line. That is the deal with open source, and most of the time it is wonderful. But it raises an awkward question for anyone trying to build a busines]]></description><link>https://blog.anmolthedeveloper.com/how-to-open-source-your-flutter-plugin-and-still-hide-the-code-that-pays-you</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/how-to-open-source-your-flutter-plugin-and-still-hide-the-code-that-pays-you</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Android]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Dart]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Tue, 30 Jun 2026 12:22:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/d485cc3c-e78f-4e91-8c87-2e7efd486e33.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Open any package on pub.dev and you can read every line. That is the deal with open source, and most of the time it is wonderful. But it raises an awkward question for anyone trying to build a business: if your best work is just sitting there in plain Dart, what stops the next developer from copying it in an afternoon?</p>
<p>Some plugins solve this in a way that looks like magic. You install them, they work perfectly, and yet the actual clever logic is nowhere to be found in the source. People assume the code must be downloaded secretly at runtime and run off some server. That guess is wrong, and getting it wrong matters, because if you ever want to protect your own code the same way, you would build the wrong thing entirely.</p>
<p>Here is what is really going on, and how you can use the exact same pattern to ship a paid feature without handing over your source.</p>
<h2>The myth, killed quickly</h2>
<p>The valuable code is already sitting inside your APK before the app even launches. It gets baked in when you build the app, not while a user is running it. Build time, not run time. The secret sauce ships with the app, just in a form that is hard to read.</p>
<p>This distinction is not just trivia. If a plugin really did download and run fresh code at runtime, Google Play would flag it. The Device and Network Abuse policy does not allow apps to fetch and execute code (a DEX file or a native library) from outside the app after install. So even if you wanted to do the runtime download thing, you should not. The build-time approach is the safe, standard one, and it is what these plugins actually do.</p>
<h2>Source code vs binary, in plain language</h2>
<p>When you write Dart, Kotlin, or Swift, that is source code. Anyone can open the file and read exactly what it does.</p>
<p>Before your app ships, a compiler chews through that source and spits out a binary. A binary is a packed file full of low-level instructions. A person can still pry it open with the right tools, but it is slow, messy, and most people will not bother. Think of source code as a recipe written out step by step, and a binary as that same recipe blended into a smoothie. Everything is still in there. Good luck reading it back out.</p>
<p>So a package author has two choices:</p>
<ol>
<li><p>Ship the source. This is what almost every pub.dev package does. Open the folder, read every line, copy it freely.</p>
</li>
<li><p>Ship only the binary and keep the source private.</p>
</li>
</ol>
<p>That second option is the whole trick. Nothing fancier than that.</p>
<h2>What is actually inside a plugin like this</h2>
<p>A plugin built this way is three layers stacked together. Only one of them is open.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>What it is</th>
<th>File</th>
<th>Open or closed</th>
</tr>
</thead>
<tbody><tr>
<td>Dart wrapper</td>
<td>The friendly API you call from Flutter, like <code>.start()</code> and <code>.stop()</code></td>
<td><code>lib/</code> folder</td>
<td>Fully open</td>
</tr>
<tr>
<td>Android engine</td>
<td>The real logic for Android</td>
<td>a <code>.aar</code> binary</td>
<td>Closed</td>
</tr>
<tr>
<td>iOS engine</td>
<td>The real logic for iOS</td>
<td>a <code>.xcframework</code> binary</td>
<td>Closed</td>
</tr>
</tbody></table>
<p>The Dart layer is a thin remote control. It holds none of the clever logic. It just forwards your calls down to the native binary through Flutter platform channels. All the brains live in the <code>.aar</code> and the <code>.xcframework</code>. That is exactly why an author can leave the Dart part open on GitHub and lose nothing. The valuable part is not there.</p>
<p>A quick aside that trips people up: an AAR is not the same as a JAR. A JAR is just compiled Java or Kotlin classes. An AAR is an Android Archive, so it can also bundle resources, a manifest, and native <code>.so</code> files. That extra packaging is why a full Android engine can ship as a single dependency.</p>
<h2>How the binary lands inside your app</h2>
<p>Here is the line that does the heavy lifting. The plugin ships a small Gradle file that your app applies, and inside it there is basically this:</p>
<pre><code class="language-groovy">dependencies {
    // Pull the closed-source engine
    api(group: 'com.yourcompany', name: 'yourengine', version: '1.0.0')
}
</code></pre>
<p>When you run <code>flutter build apk</code>, Gradle reads that line, reaches out to a Maven repository, downloads the AAR, and bundles it straight into your APK. After that the binary is part of your app, sitting on the phone like any other code. No network call when the app runs.</p>
<p>The AAR can sit on a fully public Maven repo, and that is fine, because the file is useless to anyone without a valid license. We will get to that in a second.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/05318411-3b1d-424d-b5f8-6f6c62aa53a2.jpg" alt="" style="display:block;margin:0 auto" />

<p>The user never downloads the engine separately. It travelled inside the APK the entire time.</p>
<blockquote>
<p>Tip: you will often see a version written as <code>'1.+'</code>, which means "give me the latest 1.x". Convenient, but it quietly breaks reproducible builds, because two people building the same commit on different days can pull different binaries. For anything you ship to production, pin the exact version and bump it on purpose. Future you, staring at a bug that only shows up on one machine, will be grateful.</p>
</blockquote>
<h2>The one thing that runs at runtime: the license check</h2>
<p>If the binary is free to download, how does anyone make money from it? A license gate baked inside the binary.</p>
<p>You drop a license key into your <code>AndroidManifest.xml</code>:</p>
<pre><code class="language-xml">&lt;meta-data
  android:name="com.yourcompany.engine.license"
  android:value="YOUR_LICENSE_KEY_HERE" /&gt;
</code></pre>
<p>When the app starts, the native binary reads that key and checks it against your app's package name. The key is generated for one specific package id, tied to a buyer account. If the key is missing or wrong, the engine refuses to run in a release build. In debug builds it works without any key, so a developer can try before they buy.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/1b77680b-67df-43fc-a563-7a959042f24b.jpg" alt="" style="display:block;margin:0 auto" />

<p>The check is offline friendly. The key works like a signed certificate that says "this package id is allowed," and the binary verifies it locally. It does not phone home on every launch.</p>
<blockquote>
<p>Hard-won gotcha: because debug builds skip the license entirely, everything works beautifully in development, and then your first release build silently kills the feature. People lose hours to this. Always smoke-test a real signed release build before you assume licensing is wired up correctly.</p>
</blockquote>
<p>So, two clean phases. Build time bakes in the binary. Run time verifies a key. Neither phase downloads code over the network.</p>
<h2>How to hide your own code the same way</h2>
<p>Say you build something genuinely valuable, like a custom anti-spoofing module or a pricing engine, and you want to license it to other companies without handing over the source. Here is the full recipe.</p>
<h3>Step 1: Pull the secret logic into a native module</h3>
<p>Do not leave the valuable logic in Dart. Dart in a plugin is readable. Move it down into native code.</p>
<p>On Android, create an Android Library module and write the logic in Kotlin or Java. For the most sensitive parts, drop into C or C++ with the NDK, which compiles to a <code>.so</code> file that is far harder to reverse than Kotlin bytecode. On iOS, create a Framework target and write it in Swift or Objective-C.</p>
<h3>Step 2: Compile it into a binary</h3>
<p>On Android, run the <code>assembleRelease</code> task to get a <code>.aar</code>. Turn on R8 or ProGuard with <code>minifyEnabled true</code> so the bytecode gets obfuscated and your method names collapse into <code>a</code>, <code>b</code>, <code>c</code>. On iOS, set <code>BUILD_LIBRARY_FOR_DISTRIBUTION = YES</code>, archive, and build an <code>.xcframework</code>, then zip it.</p>
<blockquote>
<p>Two tips most tutorials skip. First, obfuscation will happily rename the methods your platform channel calls by reflection, and then everything breaks at runtime with no compile error. Add explicit keep rules (or the <code>@Keep</code> annotation) for your public API and any entry points. Second, after you build native <code>.so</code> files, run <code>strip</code> on them to remove debug symbols. Smaller file, and you stop handing a reverse engineer a map with all the street names on it.</p>
</blockquote>
<h3>Step 3: Wrap it in a thin Flutter plugin</h3>
<p>Create a normal Flutter plugin package. The Dart side is just the remote control. It exposes friendly methods and forwards calls to your binary over a <code>MethodChannel</code>. This Dart code can stay open, because it holds nothing worth stealing. Your Gradle file and podspec point at the binary instead of bundling source.</p>
<img src="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/b76cbe3e-6bda-4b2e-a9bc-66d94c825d20.jpg" alt="" style="display:block;margin:0 auto" />

<h3>Step 4: Add your own license gate</h3>
<p>Inside the binary, before doing the real work, check something the buyer cannot fake. Read the app's package id and compare it against a signed key you generated for them. Verify that key with public-key cryptography, so a buyer cannot forge their own. They would need your private key, which never ships. Optionally do a light online activation the first time the app runs.</p>
<p>Keep the check inside the native binary, never in Dart, otherwise anyone can simply delete it.</p>
<blockquote>
<p>The sharpest version of this: do not write the gate as a plain <code>if (!valid) return;</code>. That is one line for someone to NOP out in a patched binary. Instead, make the license key actually do work the feature needs. Use it to derive a value, or to decrypt a config blob the engine cannot function without. Now patching out the check does not unlock the feature, it just breaks it. You have tied the lock to the engine instead of bolting it onto the door.</p>
</blockquote>
<h3>Step 5: Publish the binary and reference it from Gradle</h3>
<p>Upload your AAR to a Maven repo, then your plugin's Gradle does the same move:</p>
<pre><code class="language-groovy">dependencies {
    api(group: 'com.yourcompany', name: 'yourengine', version: '1.0.0')
}
</code></pre>
<p>That is the entire pattern. Dart stays open and friendly, the brains stay locked in a binary, and a license key controls who gets to run it.</p>
<h2>Where to host your binary</h2>
<p>For Android the binary lives in a Maven repository. For iOS it lives wherever CocoaPods or Swift Package Manager can reach a zipped xcframework, usually a plain file URL. Here are the real options, from laziest to most controlled.</p>
<p><strong>JitPack.</strong> You push a library to a GitHub repo and JitPack compiles and serves it as a Maven dependency automatically. Public repos are free, great for moving fast. The catch is that JitPack builds from source, so for a closed product your source has to live somewhere JitPack can read it, which usually means a paid private repo.</p>
<p><strong>Maven Central (Sonatype Central Portal).</strong> The industry-standard home, and where most serious Android libraries live. Free to publish, and every Android project can pull from it with zero config. The trade-off is that the binary becomes fully public, so all your security has to come from the license gate, not from hiding the file. Setup is a one-time chore (verify a namespace, sign artifacts with GPG), but smooth after that.</p>
<p><strong>GitHub Packages.</strong> A Maven repo attached to your GitHub account. Free for public packages. Private packages work too, but consumers need a token in their Gradle to authenticate, which is annoying to hand out to external buyers. Good for internal use where your own team holds the tokens.</p>
<p><strong>Self-hosted on your own server.</strong> A Maven repo is literally a folder of files served over HTTPS in a specific directory layout, so any web server does the job. Drop the files behind nginx on a VPS you already pay for and point Gradle at <code>https://maven.yourdomain.com</code>. Cloudflare R2 is even better for downloads, since it speaks the S3 API and charges zero egress fees. Plain AWS S3 works, but you pay for every gigabyte downloaded, which can sting if the file gets popular.</p>
<p>For iOS, zip the xcframework and host that single file (R2, S3, a VPS, or a GitHub Release asset), then point your CocoaPods podspec or SPM <code>binaryTarget</code> at the URL.</p>
<blockquote>
<p>SPM tip: a <code>binaryTarget</code> requires a checksum of the zip, and the build fails if it does not match. That is a feature, not a hassle. It means a buyer (or an attacker on the network) cannot quietly swap your binary for a tampered one. Generate it with <code>swift package compute-checksum yourframework.zip</code> and commit the value.</p>
</blockquote>
<h2>What it costs to host (2026 prices)</h2>
<p>An AAR or xcframework is tiny, usually a few megabytes, and a niche product does not pull much download traffic. So at this scale almost everything here is effectively free.</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Storage</th>
<th>Download (egress)</th>
<th>Realistic cost</th>
</tr>
</thead>
<tbody><tr>
<td>Maven Central</td>
<td>Free</td>
<td>Free</td>
<td>Free, but the binary is public</td>
</tr>
<tr>
<td>JitPack (public)</td>
<td>Free</td>
<td>Free</td>
<td>Free</td>
</tr>
<tr>
<td>JitPack (private)</td>
<td>Paid plan</td>
<td>Included</td>
<td>Monthly subscription, only if you need privacy</td>
</tr>
<tr>
<td>GitHub Packages (public)</td>
<td>Free</td>
<td>Free</td>
<td>Free</td>
</tr>
<tr>
<td>GitHub Packages (private)</td>
<td>Free tier, then usage</td>
<td>Usage based</td>
<td>Effectively free at small scale</td>
</tr>
<tr>
<td>Cloudflare R2</td>
<td>10 GB free, then ~0.015 USD per GB-month</td>
<td>Always free</td>
<td>Basically zero for a few MB</td>
</tr>
<tr>
<td>AWS S3</td>
<td>~0.023 USD per GB-month</td>
<td>~0.09 USD per GB</td>
<td>A few rupees, egress grows with popularity</td>
</tr>
<tr>
<td>Your own VPS</td>
<td>Already paid</td>
<td>Already paid</td>
<td>No extra cost</td>
</tr>
</tbody></table>
<p>The honest summary: hosting the file is the cheap part. R2 stands out because storage is almost nothing and downloads are genuinely free, so the bill never surprises you even if the product takes off. S3 is the one to watch, because egress is metered.</p>
<h2>Which option to actually pick</h2>
<p>Two situations, two answers.</p>
<p>If this is internal tooling that only your own apps use, self-host on a VPS or push to a private GitHub Packages repo. You control the tokens, nobody outside sees it, and it costs nothing extra. Keep the license gate simple, since you trust the consumers anyway.</p>
<p>If this becomes a real product you sell to other developers, go Maven Central for Android plus a zipped xcframework on R2 for iOS. Maven Central gives buyers the frictionless install they expect, and R2 keeps iOS downloads free. Then put your real effort into the license gate and key generation, because once the file is on Maven Central it is public, and the gate is your only lock.</p>
<h2>The catch nobody likes to admit</h2>
<p>A binary is harder to read, not impossible. Anyone can run a tool like <code>jadx</code> on an AAR and get back obfuscated bytecode, or pull the <code>.so</code> files out and study them. A determined person with time can work through it, and a simple license check can be patched out by someone skilled enough.</p>
<p>So be honest about what this actually buys you. It stops casual copy-paste. A regular developer cannot just lift your code. That is real and worth something. It does not stop a serious reverse engineer who has decided to crack it.</p>
<p>A couple of things that make their life harder, none of which make it impossible:</p>
<ul>
<li><p>Strings in a binary are trivially greppable. Run <code>strings</code> on any compiled file and out come your URLs, error messages, and any secret you were naive enough to hardcode. Never assume a value is safe just because it is compiled. If it must stay secret, it does not belong on the device.</p>
</li>
<li><p>A native check of the app's signing certificate at startup raises the bar on repackaging. It is not unbeatable, but it filters out the low-effort attempts.</p>
</li>
</ul>
<p>If the logic is truly your crown jewels, the strongest move is to keep the heavy lifting on a server you control and have the app call an API. The client only ever sees inputs and outputs, never the algorithm. The binary trick is the right middle ground when the logic has to run on-device, which is the case for things like location tracking or offline detection. For that on-device case, binary plus obfuscation plus a license gate is the sensible setup.</p>
<h2>Quick checklist if you want to build your own</h2>
<ol>
<li><p>Move the secret logic out of Dart into a native Android library and iOS framework.</p>
</li>
<li><p>Compile to <code>.aar</code> and <code>.xcframework</code> with R8 or ProGuard obfuscation on, and keep rules for your public API.</p>
</li>
<li><p>Strip debug symbols from native <code>.so</code> files.</p>
</li>
<li><p>Write a thin open Dart wrapper that talks to the binary over a MethodChannel.</p>
</li>
<li><p>Add a license gate inside the binary, verified with public-key crypto, tied to the package id, and woven into something the feature needs so it cannot be cleanly patched out.</p>
</li>
<li><p>Publish the AAR (Maven Central or private GitHub Packages) and host the xcframework zip (R2 is ideal), with a pinned version and an SPM checksum.</p>
</li>
<li><p>Remember the limit. This protects against casual copying, not a determined attacker. Keep the truly priceless stuff server-side.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[You Built It Once. Now Sell It a Hundred Times — The Complete Flutter App Rebranding Guide]]></title><description><![CDATA[TL;DR — This guide covers everything you need to rebrand a Flutter app for a new client: changing the app name, swapping the icon with flutter_launcher_icons, renaming the package ID with change_app_p]]></description><link>https://blog.anmolthedeveloper.com/you-built-it-once-now-sell-it-a-hundred-times-the-complete-flutter-app-rebranding-guide</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/you-built-it-once-now-sell-it-a-hundred-times-the-complete-flutter-app-rebranding-guide</guid><category><![CDATA[Android]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter package]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[white label]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Tue, 05 May 2026 05:38:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/656736a41a36724be2388eb7/b70c4fbb-4236-4f0d-b1d9-92450a03e963.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR</strong> — This guide covers everything you need to rebrand a Flutter app for a new client: changing the app name, swapping the icon with <code>flutter_launcher_icons</code>, renaming the package ID with <code>change_app_package_name</code>, and a checklist of things most developers miss. Whether you're white-labeling for the first time or the tenth, this is the reference you'll keep coming back to.</p>
</blockquote>
<hr />
<p>So you built something. An app that actually works, looks good, and solves a real problem. Now a client or company comes along and says — <em>"Can we have this, but with our name and logo?"</em></p>
<p>That's a great problem to have. But if you've never done a <strong>Flutter app rebrand</strong> before, it can feel oddly intimidating for something that sounds simple. Change the name, swap the icon, rename the package... how hard can it be?</p>
<p>Turns out, if you don't know where to look, it can get messy fast. This guide walks you through the complete <strong>Flutter white-label rebranding process</strong> — step by step, cleanly and confidently.</p>
<hr />
<h2>What Does "Rebranding a Flutter App" Actually Mean?</h2>
<p>When you hand off a white-labeled version of your Flutter app to another company, there are three things that almost always need to change:</p>
<ol>
<li><strong>The app name</strong> — what users see on their home screen</li>
<li><strong>The app icon</strong> — the visual identity of the brand</li>
<li><strong>The package name / bundle ID</strong> — the unique identifier used by the Google Play Store, Apple App Store, and the OS itself</li>
</ol>
<p>Most developers forget that third one until something breaks. Let's not do that.</p>
<p>Each of these changes targets a different part of your project, and skipping even one can cause build failures, store rejections, or worse — your app and the client's app sharing the same identifier in production.</p>
<hr />
<h2>Step 1 — How to Change the Flutter App Name</h2>
<p>The display name (the label that appears under the icon on a user's phone) lives in two platform-specific files.</p>
<h3>Android — AndroidManifest.xml</h3>
<p>Open <code>android/app/src/main/AndroidManifest.xml</code> and update the <code>android:label</code> attribute on the <code>&lt;application&gt;</code> tag:</p>
<pre><code class="language-xml">&lt;application
    android:label="ClientAppName"
    ...&gt;
</code></pre>
<h3>iOS — Info.plist</h3>
<p>Open <code>ios/Runner/Info.plist</code> and update the <code>CFBundleDisplayName</code> key:</p>
<pre><code class="language-xml">&lt;key&gt;CFBundleDisplayName&lt;/key&gt;
&lt;string&gt;ClientAppName&lt;/string&gt;
</code></pre>
<p>That covers both platforms. It's simple, but missing one of them means the rebrand looks half-done on one OS — and clients notice.</p>
<hr />
<h2>Step 2 — How to Change the Flutter App Icon</h2>
<p>Manually resizing icons for every screen density is the kind of thing that used to take an hour and still somehow looked wrong on one device. Skip all of that.</p>
<p>The <code>flutter_launcher_icons</code> package handles icon generation for every size and density on both Android and iOS from a single source image.</p>
<h3>Setup</h3>
<p>Add it to your <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icons/client_icon.png"
  min_sdk_android: 21
</code></pre>
<p>Use a high-quality PNG at <strong>1024x1024 pixels</strong> as your source. Then run:</p>
<pre><code class="language-bash">dart run flutter_launcher_icons
</code></pre>
<p>That single command generates all the required icon sizes for both platforms. No Photoshop, no manual asset folders.</p>
<h3>Things to Watch Out For</h3>
<ul>
<li><strong>Transparent backgrounds on Android 7 or below</strong> — they'll render as a black square. Use a solid background color instead.</li>
<li><strong>Adaptive icons for Android 13+</strong> — you can configure <code>adaptive_icon_background</code> and <code>adaptive_icon_foreground</code> separately in the config for a better look on modern Android.</li>
<li><strong>Always test on a real device</strong> — the icon preview during config doesn't represent how it actually renders on every launcher.</li>
</ul>
<hr />
<h2>Step 3 — How to Change the Flutter Package Name (The Critical Step)</h2>
<p>The <strong>package name</strong> on Android (e.g., <code>com.yourcompany.appname</code>) and the <strong>bundle identifier</strong> on iOS are what uniquely identify your app across the entire ecosystem. If two apps share the same ID, the Play Store and App Store will treat them as the same app — which causes real problems during submission.</p>
<p>When rebranding a Flutter app for a new client, this <strong>must</strong> change.</p>
<p>Doing it manually means touching <code>AndroidManifest.xml</code>, <code>build.gradle</code>, every directory path under <code>kotlin/</code> or <code>java/</code>, the package declarations inside those files, and iOS project settings in Xcode. Miss one file and you're looking at a cryptic build error and 20 minutes of head-scratching.</p>
<p>The cleaner way is the <code>change_app_package_name</code> pub package.</p>
<h3>Setting It Up</h3>
<p>Add it to <code>dev_dependencies</code> in your <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dev_dependencies:
  change_app_package_name: ^1.5.0
</code></pre>
<p>Then run a single command:</p>
<pre><code class="language-bash">dart run change_app_package_name:main com.clientcompany.appname
</code></pre>
<p>Replace <code>com.clientcompany.appname</code> with the new package name. The convention is reverse domain notation — use the client's domain if they have one (e.g., <code>com.acmecorp.deliveryapp</code>).</p>
<h3>What the Package Does Automatically</h3>
<ul>
<li>Updates <code>applicationId</code> in <code>android/app/build.gradle</code></li>
<li>Renames the directory structure under <code>android/app/src/main/kotlin/</code> (or <code>java/</code>)</li>
<li>Updates the <code>package</code> declaration inside all affected Kotlin/Java files</li>
<li>Updates <code>AndroidManifest.xml</code></li>
</ul>
<h3>What You Still Need to Do on iOS</h3>
<p>The package doesn't touch iOS. For the bundle identifier, open your project in Xcode, go to <strong>Runner &gt; Signing &amp; Capabilities</strong>, and update the <strong>Bundle Identifier</strong> field. It takes about 10 seconds.</p>
<hr />
<h2>Step 4 — The Rebranding Checklist (Things Most Developers Miss)</h2>
<p>The three steps above get the visible stuff done. But before you hand off the build to a client, run through this checklist.</p>
<p><strong>Firebase / Backend Config</strong>
If your app uses Firebase, the new package name won't be registered in the existing Firebase project. You'll need a new <code>google-services.json</code> (Android) and <code>GoogleService-Info.plist</code> (iOS) from a Firebase project linked to the new package ID. Skipping this breaks push notifications, analytics, and auth.</p>
<p><strong>Deep Links and Custom URL Schemes</strong>
If your app handles universal links or custom URL schemes (e.g., <code>yourapp://</code>), those are tied to the package name and domain. Update them in the manifest, entitlements, and wherever they're configured on your server.</p>
<p><strong>App Store and Play Store Listings</strong>
A new package name means a completely new store listing — it's not an update to the original app. The client will need their own developer accounts (or you submit under yours with their details). Plan for a fresh review cycle.</p>
<p><strong>Keystore and Signing Certificates</strong>
Never sign a client's build with your own personal keystore. Either generate a new keystore for them or have them generate one themselves. The same applies to iOS provisioning profiles and distribution certificates.</p>
<p><strong>Environment Variables and API Keys</strong>
If the original app points to your own backend or uses your API keys, the rebranded version needs its own config. The cleanest way to manage this is with Flutter's <code>--dart-define</code> flags or a flavor-based environment config — more on that below.</p>
<p><strong>Splash Screen</strong>
Don't forget to update the splash screen if it includes your logo or the original app name. The <code>flutter_native_splash</code> package makes this just as easy as <code>flutter_launcher_icons</code>.</p>
<hr />
<h2>Bonus: Set Up Flutter Flavors for Multi-Client Workflows</h2>
<p>If rebranding is something you're going to do more than once — multiple clients, same core codebase — it's worth setting up <strong>Flutter flavors</strong> (called build variants on Android).</p>
<p>Flavors let you maintain a single codebase and produce completely different builds — different names, icons, package names, API endpoints, and feature flags — just by switching a build target:</p>
<pre><code class="language-bash">flutter run --flavor clientA -t lib/main_clientA.dart
flutter run --flavor clientB -t lib/main_clientB.dart
</code></pre>
<p>The initial setup takes a few hours, but once it's in place, onboarding a new client becomes a matter of adding a new flavor config rather than manually cloning and modifying the entire project.</p>
<p>The <code>flutter_flavorizr</code> package automates most of the boilerplate for setting up flavors across both platforms. It's worth an afternoon of your time if you're building a white-label business around your app.</p>
<hr />
<h2>Quick Reference — Flutter Rebranding Checklist</h2>
<table>
<thead>
<tr>
<th>What to Change</th>
<th>Where</th>
<th>Tool / Method</th>
</tr>
</thead>
<tbody><tr>
<td>App display name</td>
<td><code>AndroidManifest.xml</code>, <code>Info.plist</code></td>
<td>Manual</td>
</tr>
<tr>
<td>App icon</td>
<td>Generated asset folders</td>
<td><code>flutter_launcher_icons</code></td>
</tr>
<tr>
<td>Package name / Bundle ID</td>
<td><code>build.gradle</code>, Kotlin files, Xcode</td>
<td><code>change_app_package_name</code> + Xcode</td>
</tr>
<tr>
<td>Firebase config</td>
<td>Project root</td>
<td>New <code>google-services.json</code> / <code>.plist</code></td>
</tr>
<tr>
<td>Splash screen</td>
<td>Platform folders</td>
<td><code>flutter_native_splash</code></td>
</tr>
<tr>
<td>Signing / Keystore</td>
<td>Build config</td>
<td>New keystore per client</td>
</tr>
<tr>
<td>Deep links</td>
<td>Manifest, entitlements, server</td>
<td>Manual</td>
</tr>
</tbody></table>
<hr />
<h2>Frequently Asked Questions</h2>
<p><strong>Can I use the same Firebase project for multiple rebranded apps?</strong>
Technically yes, but it's not recommended. Each rebranded app should ideally have its own Firebase project to keep analytics, crash reports, and user data separate per client.</p>
<p><strong>Does <code>change_app_package_name</code> work with flavors?</strong>
It works on the default app config. If you're using flavors, you'll want to manage package names per-flavor in your <code>build.gradle</code> directly rather than running the package command.</p>
<p><strong>Do I need a separate Apple Developer account for each client?</strong>
Not necessarily. You can distribute under your own account and list the client as the seller, or you can submit through their account. This is a business/contractual decision more than a technical one.</p>
<p><strong>Will changing the package name affect existing users of the original app?</strong>
No — your original app is untouched. The rebranded version is effectively a brand new app. Existing users of the original won't see any difference.</p>
<p><strong>What's the difference between package name and app name in Flutter?</strong>
The app name is what users see on their screen. The package name is an internal identifier used by the OS and app stores — users never see it, but it has to be unique.</p>
<hr />
<h2>Wrapping Up</h2>
<p>Rebranding a Flutter app for a client isn't complicated once you know the three core things to change — app name, icon, and package ID. The right packages handle most of the mechanical work, and a solid checklist takes care of the rest.</p>
<p>The real leverage comes when you start thinking about this at architecture time. Building flavor support into your project from day one means you're never scrambling when the second client shows up.</p>
<p>If you've done this before and ran into something I didn't cover here — a quirky edge case, a platform-specific gotcha — drop it in the comments. This kind of shared experience is what makes these guides actually useful over time.</p>
<p>And if this saved you some time or headache, share it with a Flutter dev who's about to go through their first white-label project. They'll owe you one.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Theme Guide: Light Mode, Dark Mode, System Theme and Custom Brand Colors]]></title><description><![CDATA[A Quick Story to Set the Scene
Meet Lumi and Nyx. They share one Flutter app. Lumi prefers light mode — bright cards, cheerful blues, a UI that feels open. Nyx wants the opposite — calm grays, a dark background, something easier on the eyes at night....]]></description><link>https://blog.anmolthedeveloper.com/flutter-theme-guide-light-mode-dark-mode-system-theme-and-custom-brand-colors</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-theme-guide-light-mode-dark-mode-system-theme-and-custom-brand-colors</guid><category><![CDATA[Android]]></category><category><![CDATA[app development]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Material Design]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Mon, 04 May 2026 12:14:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759648593248/cc65246b-5fc7-4094-813a-04fa5e751596.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-a-quick-story-to-set-the-scene">A Quick Story to Set the Scene</h2>
<p>Meet Lumi and Nyx. They share one Flutter app. Lumi prefers light mode — bright cards, cheerful blues, a UI that feels open. Nyx wants the opposite — calm grays, a dark background, something easier on the eyes at night.</p>
<p>Your job as the developer? One app, two moods, zero duplication.</p>
<p>That's what theming is for. In this article, we'll build a clean, production-ready theme system that:</p>
<ul>
<li>switches between light, dark, and system modes,</li>
<li>centralizes colors, typography, shapes, and spacing,</li>
<li>supports quick toggles and persistence, and</li>
<li>scales with Theme Extensions for custom brand tokens.</li>
</ul>
<hr />
<h2 id="heading-theme-101-what-actually-is-a-theme">Theme 101: What Actually Is a Theme?</h2>
<p>Before writing any code, it's worth getting the terminology straight:</p>
<ul>
<li><code>ThemeData</code> — the master container for all visual settings: colors, typography, shapes, component styles.</li>
<li><code>ColorScheme</code> — the Material 3 color palette. Always prefer this over manual color assignments.</li>
<li><code>ThemeMode</code> — tells Flutter which theme to use: <code>light</code>, <code>dark</code>, or <code>system</code>.</li>
<li><code>Theme.of(context)</code> — how you read the active theme from inside any widget.</li>
<li><code>AnimatedTheme</code> — wraps widgets and animates smoothly when the theme changes.</li>
<li><code>ThemeExtension</code> — lets you attach custom design tokens (like spacing or border radii) directly to the theme.</li>
</ul>
<hr />
<h2 id="heading-project-skeleton">Project Skeleton</h2>
<p>We'll start with Material 3, wire up a <code>ColorScheme</code>, add a quick toggle, and persist the user's choice.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shared_preferences/shared_preferences.dart'</span>;

<span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  WidgetsFlutterBinding.ensureInitialized();
  <span class="hljs-keyword">final</span> prefs = <span class="hljs-keyword">await</span> SharedPreferences.getInstance();
  <span class="hljs-keyword">final</span> saved = prefs.getString(<span class="hljs-string">'themeMode'</span>) ?? <span class="hljs-string">'system'</span>;
  <span class="hljs-keyword">final</span> initialMode = _parseThemeMode(saved);

  runApp(AppRoot(initialMode: initialMode, prefs: prefs));
}

ThemeMode _parseThemeMode(<span class="hljs-built_in">String</span> s) =&gt; <span class="hljs-keyword">switch</span> (s) {
  <span class="hljs-string">'light'</span> =&gt; ThemeMode.light,
  <span class="hljs-string">'dark'</span> =&gt; ThemeMode.dark,
  _ =&gt; ThemeMode.system,
};

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppRoot</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> AppRoot({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.initialMode, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.prefs});
  <span class="hljs-keyword">final</span> ThemeMode initialMode;
  <span class="hljs-keyword">final</span> SharedPreferences prefs;

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_AppRootState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">AppRoot</span>&gt; </span>{
  <span class="hljs-keyword">late</span> ThemeMode _mode = widget.initialMode;

  Future&lt;<span class="hljs-keyword">void</span>&gt; _setMode(ThemeMode m) <span class="hljs-keyword">async</span> {
    setState(() =&gt; _mode = m);
    <span class="hljs-keyword">await</span> widget.prefs.setString(<span class="hljs-string">'themeMode'</span>, <span class="hljs-keyword">switch</span> (m) {
      ThemeMode.light =&gt; <span class="hljs-string">'light'</span>,
      ThemeMode.dark =&gt; <span class="hljs-string">'dark'</span>,
      ThemeMode.system =&gt; <span class="hljs-string">'system'</span>,
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Lumi &amp; Nyx'</span>,
      debugShowCheckedModeBanner: <span class="hljs-keyword">false</span>,
      themeMode: _mode,
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      home: HomeScreen(
        mode: _mode,
        onChangeMode: _setMode,
      ),
    );
  }
}
</code></pre>
<p>A few things worth noting here:</p>
<ul>
<li>We read <code>SharedPreferences</code> on startup to restore the user's last chosen mode.</li>
<li><code>MaterialApp</code> takes three separate knobs: <code>theme</code>, <code>darkTheme</code>, and <code>themeMode</code>.</li>
<li>Calling <code>_setMode</code> triggers a rebuild and also writes the choice to disk.</li>
</ul>
<hr />
<h2 id="heading-your-design-tokens-colorscheme-first-material-3">Your Design Tokens: ColorScheme-First (Material 3)</h2>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppTheme</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _seed = Color(<span class="hljs-number">0xFF4F46E5</span>);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> _error = Color(<span class="hljs-number">0xFFB00020</span>);

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ColorScheme _lightScheme = ColorScheme.fromSeed(
    seedColor: _seed,
    brightness: Brightness.light,
    error: _error,
  );

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ColorScheme _darkScheme = ColorScheme.fromSeed(
    seedColor: _seed,
    brightness: Brightness.dark,
    error: _error,
  );

  <span class="hljs-keyword">static</span> ThemeData <span class="hljs-keyword">get</span> light =&gt; ThemeData(
        useMaterial3: <span class="hljs-keyword">true</span>,
        colorScheme: _lightScheme,
        scaffoldBackgroundColor: _lightScheme.background,
        appBarTheme: AppBarTheme(
          backgroundColor: _lightScheme.surface,
          foregroundColor: _lightScheme.onSurface,
          elevation: <span class="hljs-number">0</span>,
          centerTitle: <span class="hljs-keyword">false</span>,
        ),
        textTheme: _textTheme(_lightScheme),
        cardTheme: CardTheme(
          elevation: <span class="hljs-number">0</span>,
          margin: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">12</span>),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(<span class="hljs-number">16</span>),
          ),
        ),
        extensions: <span class="hljs-keyword">const</span> &lt;ThemeExtension&lt;<span class="hljs-built_in">dynamic</span>&gt;&gt;[
          AppSpacing.compact,
        ],
      );

  <span class="hljs-keyword">static</span> ThemeData <span class="hljs-keyword">get</span> dark =&gt; ThemeData(
        useMaterial3: <span class="hljs-keyword">true</span>,
        colorScheme: _darkScheme,
        scaffoldBackgroundColor: _darkScheme.background,
        appBarTheme: AppBarTheme(
          backgroundColor: _darkScheme.surface,
          foregroundColor: _darkScheme.onSurface,
          elevation: <span class="hljs-number">0</span>,
          centerTitle: <span class="hljs-keyword">false</span>,
        ),
        textTheme: _textTheme(_darkScheme),
        cardTheme: CardTheme(
          elevation: <span class="hljs-number">0</span>,
          margin: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">12</span>),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(<span class="hljs-number">16</span>),
          ),
        ),
        extensions: <span class="hljs-keyword">const</span> &lt;ThemeExtension&lt;<span class="hljs-built_in">dynamic</span>&gt;&gt;[
          AppSpacing.compact,
        ],
      );

  <span class="hljs-keyword">static</span> TextTheme _textTheme(ColorScheme scheme) {
    <span class="hljs-keyword">return</span> Typography.material2021().black.apply(
      bodyColor: scheme.onBackground,
      displayColor: scheme.onBackground,
    );
  }
}
</code></pre>
<p><code>ColorScheme.fromSeed</code> does a lot of heavy lifting — it derives a full, harmonious palette (primary, secondary, surface, background, and their "on" counterparts) from a single brand color, for both light and dark. You wire it in once and everything else just works.</p>
<hr />
<h2 id="heading-pro-move-themeextension-for-brand-tokens">Pro Move: ThemeExtension for Brand Tokens</h2>
<p>The built-in <code>ThemeData</code> covers most cases, but real apps often need things like consistent spacing values, custom border radii, or brand-specific shadow levels. <code>ThemeExtension</code> is the right way to handle this — it keeps your tokens in the theme tree, not scattered as constants across the codebase.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@immutable</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppSpacing</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ThemeExtension</span>&lt;<span class="hljs-title">AppSpacing</span>&gt; </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> xs, sm, md, lg, xl;
  <span class="hljs-keyword">const</span> AppSpacing({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.xs,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.sm,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.md,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.lg,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.xl,
  });

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> compact = AppSpacing(xs: <span class="hljs-number">4</span>, sm: <span class="hljs-number">8</span>, md: <span class="hljs-number">12</span>, lg: <span class="hljs-number">16</span>, xl: <span class="hljs-number">24</span>);

  <span class="hljs-meta">@override</span>
  AppSpacing copyWith({<span class="hljs-built_in">double?</span> xs, <span class="hljs-built_in">double?</span> sm, <span class="hljs-built_in">double?</span> md, <span class="hljs-built_in">double?</span> lg, <span class="hljs-built_in">double?</span> xl}) {
    <span class="hljs-keyword">return</span> AppSpacing(
      xs: xs ?? <span class="hljs-keyword">this</span>.xs,
      sm: sm ?? <span class="hljs-keyword">this</span>.sm,
      md: md ?? <span class="hljs-keyword">this</span>.md,
      lg: lg ?? <span class="hljs-keyword">this</span>.lg,
      xl: xl ?? <span class="hljs-keyword">this</span>.xl,
    );
  }

  <span class="hljs-meta">@override</span>
  AppSpacing lerp(ThemeExtension&lt;AppSpacing&gt;? other, <span class="hljs-built_in">double</span> t) {
    <span class="hljs-keyword">if</span> (other <span class="hljs-keyword">is</span>! AppSpacing) <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>;
    <span class="hljs-built_in">double</span> lerpDouble(<span class="hljs-built_in">double</span> a, <span class="hljs-built_in">double</span> b) =&gt; a + (b - a) * t;
    <span class="hljs-keyword">return</span> AppSpacing(
      xs: lerpDouble(xs, other.xs),
      sm: lerpDouble(sm, other.sm),
      md: lerpDouble(md, other.md),
      lg: lerpDouble(lg, other.lg),
      xl: lerpDouble(xl, other.xl),
    );
  }
}

<span class="hljs-keyword">extension</span> SpacingX <span class="hljs-keyword">on</span> BuildContext {
  AppSpacing <span class="hljs-keyword">get</span> space =&gt; Theme.of(<span class="hljs-keyword">this</span>).<span class="hljs-keyword">extension</span>&lt;AppSpacing&gt;()!;
}
</code></pre>
<p>Using it in widgets is clean and readable:</p>
<pre><code class="lang-dart">Padding(
  padding: EdgeInsets.all(context.space.lg),
  child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Consistent spacing everywhere.'</span>),
)
</code></pre>
<hr />
<h2 id="heading-quick-theme-switching-ui-with-animation">Quick Theme Switching UI (With Animation)</h2>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> HomeScreen({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.mode, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onChangeMode});
  <span class="hljs-keyword">final</span> ThemeMode mode;
  <span class="hljs-keyword">final</span> ValueChanged&lt;ThemeMode&gt; onChangeMode;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> scheme = Theme.of(context).colorScheme;

    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Lumi &amp; Nyx'</span>),
        actions: [
          PopupMenuButton&lt;ThemeMode&gt;(
            icon: <span class="hljs-keyword">const</span> Icon(Icons.light_mode),
            initialValue: mode,
            onSelected: onChangeMode,
            itemBuilder: (context) =&gt; <span class="hljs-keyword">const</span> [
              PopupMenuItem(value: ThemeMode.system, child: Text(<span class="hljs-string">'System'</span>)),
              PopupMenuItem(value: ThemeMode.light, child: Text(<span class="hljs-string">'Light'</span>)),
              PopupMenuItem(value: ThemeMode.dark, child: Text(<span class="hljs-string">'Dark'</span>)),
            ],
          ),
        ],
      ),
      body: AnimatedTheme(
        duration: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">250</span>),
        data: Theme.of(context),
        child: Center(
          child: Card(
            color: scheme.surface,
            child: Padding(
              padding: EdgeInsets.all(context.space.xl),
              child: Text(
                <span class="hljs-string">'This card adapts to the active theme.'</span>,
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          <span class="hljs-keyword">final</span> next = <span class="hljs-keyword">switch</span> (mode) {
            ThemeMode.system =&gt; ThemeMode.light,
            ThemeMode.light =&gt; ThemeMode.dark,
            ThemeMode.dark =&gt; ThemeMode.system,
          };
          onChangeMode(next);
        },
        label: Text(<span class="hljs-string">'Mode: <span class="hljs-subst">${mode.name}</span>'</span>),
        icon: <span class="hljs-keyword">const</span> Icon(Icons.auto_mode),
      ),
    );
  }
}
</code></pre>
<p>The FAB cycles through modes quickly; the menu gives the user explicit control. <code>AnimatedTheme</code> handles the transition so the switch doesn't feel abrupt.</p>
<hr />
<h2 id="heading-custom-fonts-and-typography">Custom Fonts and Typography</h2>
<p>Add your font to <code>pubspec.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter:</span>
  <span class="hljs-attr">uses-material-design:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">fonts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">family:</span> <span class="hljs-string">Inter</span>
      <span class="hljs-attr">fonts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">asset:</span> <span class="hljs-string">assets/fonts/Inter-Regular.ttf</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">asset:</span> <span class="hljs-string">assets/fonts/Inter-Medium.ttf</span>
          <span class="hljs-attr">weight:</span> <span class="hljs-number">500</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">asset:</span> <span class="hljs-string">assets/fonts/Inter-SemiBold.ttf</span>
          <span class="hljs-attr">weight:</span> <span class="hljs-number">600</span>
</code></pre>
<p>Then apply it in your <code>AppTheme</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> TextTheme _textTheme(ColorScheme scheme) {
  <span class="hljs-keyword">final</span> base = Typography.material2021().black;
  <span class="hljs-keyword">return</span> base.apply(
    fontFamily: <span class="hljs-string">'Inter'</span>,
    bodyColor: scheme.onBackground,
    displayColor: scheme.onBackground,
  );
}
</code></pre>
<p>If you want a different type scale in dark mode, you can create a separate builder for it.</p>
<hr />
<h2 id="heading-respecting-the-system-theme">Respecting the System Theme</h2>
<p>Using <code>ThemeMode.system</code> is the right default — it automatically follows the OS-level setting on Android, iOS, macOS, and Windows. If you ever need to query it manually:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
</code></pre>
<p>One practical note: if you're offering a manual override toggle, make sure you're also persisting the choice. Otherwise users will find their preference reset every time they launch the app.</p>
<hr />
<h2 id="heading-reading-theme-correctly-inside-widgets">Reading Theme Correctly Inside Widgets</h2>
<p>The single most common theming mistake in Flutter codebases is hardcoding colors. It breaks dark mode and makes brand updates painful.</p>
<p><strong>Do this:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> cs = Theme.of(context).colorScheme;

Container(
  decoration: BoxDecoration(
    color: cs.surface,
    borderRadius: BorderRadius.circular(<span class="hljs-number">16</span>),
  ),
  child: Text(<span class="hljs-string">'Hello'</span>, style: Theme.of(context).textTheme.bodyLarge),
);
</code></pre>
<p><strong>Avoid this:</strong></p>
<pre><code class="lang-dart">Container(color: Colors.white);
Text(<span class="hljs-string">'Hello'</span>, style: TextStyle(color: Colors.black));
</code></pre>
<p>Hardcoded colors are invisible to the theme system. They'll look wrong in dark mode and require manual updates whenever your brand changes.</p>
<hr />
<h2 id="heading-status-bar-and-system-overlays">Status Bar and System Overlays</h2>
<p>This detail is easy to overlook but visible to every user. Make sure your status bar icons stay readable against both themes:</p>
<pre><code class="lang-dart">AppBar(
  systemOverlayStyle: Theme.of(context).brightness == Brightness.dark
      ? SystemUiOverlayStyle.light
      : SystemUiOverlayStyle.dark,
);
</code></pre>
<p>For custom areas without an AppBar, wrap with <code>AnnotatedRegion&lt;SystemUiOverlayStyle&gt;</code>.</p>
<hr />
<h2 id="heading-theming-cupertino-widgets">Theming Cupertino Widgets</h2>
<p>If your app uses Cupertino-style components alongside Material ones, pass the current theme through:</p>
<pre><code class="lang-dart">CupertinoTheme(
  data: CupertinoThemeData(
    brightness: Theme.of(context).brightness,
    primaryColor: Theme.of(context).colorScheme.primary,
  ),
  child: <span class="hljs-keyword">const</span> CupertinoButton(child: Text(<span class="hljs-string">'iOS style'</span>), onPressed: <span class="hljs-keyword">null</span>),
);
</code></pre>
<hr />
<h2 id="heading-multiple-brand-themes">Multiple Brand Themes</h2>
<p>Some apps need more than light and dark — think "Blue plan" vs "Green plan", or white-labeled variants. You can handle this with the same approach:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> Brand { indigo, green }

<span class="hljs-keyword">final</span> brandThemes = &lt;Brand, ThemeData&gt;{
  Brand.indigo: AppTheme.light,
  Brand.green: AppTheme.light.copyWith(
    colorScheme: ColorScheme.fromSeed(seedColor: <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xFF0EA5A5</span>)),
  ),
};
</code></pre>
<p>Swap <code>brandThemes[currentBrand]!</code> the same way you'd swap <code>ThemeMode</code>, and persist the choice.</p>
<hr />
<h2 id="heading-common-pitfalls">Common Pitfalls</h2>
<p>A few things that come up repeatedly in code reviews:</p>
<ul>
<li><strong>Hardcoded colors</strong> — the most frequent issue. Always pull from <code>Theme.of(context).colorScheme</code>.</li>
<li><strong>Missing <code>darkTheme</code></strong> — if you only set <code>theme</code>, your app will show the light theme on dark-mode devices.</li>
<li><strong>No persistence</strong> — users expect their theme preference to survive an app restart.</li>
<li><strong>Text contrast issues</strong> — use <code>onSurface</code>, <code>onBackground</code>, and similar "on" colors from <code>ColorScheme</code> instead of guessing.</li>
<li><strong>No animation</strong> — <code>AnimatedTheme</code> is a small addition that makes theme switches feel intentional rather than jarring.</li>
<li><strong>Scattered global tokens</strong> — if you're defining spacing or border radii as constants spread across files, move them into a <code>ThemeExtension</code>.</li>
</ul>
<hr />
<h2 id="heading-a-quick-checklist-before-shipping">A Quick Checklist Before Shipping</h2>
<ul>
<li>Material 3 enabled with <code>useMaterial3: true</code></li>
<li>Both <code>theme</code> and <code>darkTheme</code> provided</li>
<li><code>themeMode</code> persisted across sessions</li>
<li>Quick toggle available, with system mode as an option</li>
<li>Custom fonts applied via <code>TextTheme</code></li>
<li>Brand-specific tokens in a <code>ThemeExtension</code></li>
<li>Animated transitions on theme change</li>
<li>Status bar icon colors correct per theme</li>
<li>No hardcoded colors anywhere in widgets</li>
</ul>
<hr />
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p><code>ThemeMode</code> controls which theme is active. <code>ColorScheme</code> is your source of truth for colors — build it from a seed and let Flutter derive the rest. <code>ThemeExtension</code> is the right place for custom design tokens like spacing and radii. Persist the user's preference and animate transitions. And keep hardcoded colors out of your widgets entirely — your future self will thank you.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter App Lifecycle Security: Why You Should Re-Validate State on App Resume]]></title><description><![CDATA[There's a category of bug that only shows up in production apps — the kind that doesn't crash, doesn't throw an exception, and doesn't even look wrong on the surface. It just quietly does the wrong thing.
One of the most common in security-sensitive ...]]></description><link>https://blog.anmolthedeveloper.com/flutter-app-lifecycle-security-why-you-should-re-validate-state-on-app-resume</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-app-lifecycle-security-why-you-should-re-validate-state-on-app-resume</guid><category><![CDATA[flutter-security]]></category><category><![CDATA[Android]]></category><category><![CDATA[App Lifecycle]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Wed, 14 Jan 2026 08:09:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768378075236/1891157a-0da9-4283-b9c0-ead5afd20048.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There's a category of bug that only shows up in production apps — the kind that doesn't crash, doesn't throw an exception, and doesn't even look wrong on the surface. It just quietly does the wrong thing.</p>
<p>One of the most common in security-sensitive Flutter apps is this: the app is backgrounded, the user changes something significant — enables developer options, toggles mock location, grants a new permission — and then returns. The app resumes and carries on as if nothing happened. It trusts whatever state it had when it left. That trust is the problem.</p>
<p><code>didChangeAppLifecycleState</code> is where you fix it.</p>
<hr />
<h2 id="heading-what-the-lifecycle-states-actually-mean">What the Lifecycle States Actually Mean</h2>
<p>Flutter surfaces four app-level states through <code>AppLifecycleState</code>:</p>
<ul>
<li><code>resumed</code> — the app is in the foreground and the user can interact with it</li>
<li><code>inactive</code> — the app is visible but not interactive (incoming call, app switcher, system dialog)</li>
<li><code>paused</code> — the app is in the background and not visible</li>
<li><code>detached</code> — the Flutter engine is running but no UI is attached</li>
</ul>
<p>For most security and freshness checks, <code>resumed</code> is the state you care about. It fires every time the app returns to the foreground — from the background, from a lock screen, from switching apps. That's the moment you want to re-examine assumptions.</p>
<hr />
<h2 id="heading-setting-it-up">Setting It Up</h2>
<p>Add <code>WidgetsBindingObserver</code> to your state class, register in <code>initState</code>, and always remove in <code>dispose</code>:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; <span class="hljs-title">with</span> <span class="hljs-title">WidgetsBindingObserver</span> </span>{

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

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    WidgetsBinding.instance.removeObserver(<span class="hljs-keyword">this</span>);
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didChangeAppLifecycleState(AppLifecycleState state) {
    <span class="hljs-keyword">if</span> (state == AppLifecycleState.resumed) {
      _onAppResumed();
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (state == AppLifecycleState.paused) {
      _onAppPaused();
    }
  }
}
</code></pre>
<p>The <code>removeObserver</code> call in <code>dispose</code> is not optional. Skipping it means the widget keeps receiving lifecycle callbacks after it's been removed from the tree, which can cause calls on disposed resources or setState on unmounted widgets.</p>
<hr />
<h2 id="heading-re-validating-security-on-resume">Re-Validating Security on Resume</h2>
<p>The most important use of <code>resumed</code> in security-sensitive apps is re-running the checks that determine whether the environment is still trustworthy. A user can:</p>
<ul>
<li>Enable developer options while your app is backgrounded</li>
<li>Enable mock location in developer settings</li>
<li>Connect a USB debugger</li>
<li>Grant or revoke permissions</li>
<li>Enable a VPN</li>
</ul>
<p>None of these trigger any callback while your app is in the background. The only opportunity you have to detect them is when the app comes back to the foreground.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _onAppResumed() {
  _validateEnvironment();
}

Future&lt;<span class="hljs-keyword">void</span>&gt; _validateEnvironment() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> isDeveloperModeEnabled = <span class="hljs-keyword">await</span> _checkDeveloperOptions();
  <span class="hljs-keyword">final</span> isMockLocationEnabled = <span class="hljs-keyword">await</span> _checkMockLocation();
  <span class="hljs-keyword">final</span> isRooted = <span class="hljs-keyword">await</span> _checkRootAccess();

  <span class="hljs-keyword">if</span> (isDeveloperModeEnabled || isMockLocationEnabled || isRooted) {
    _blockUsage();
  }
}
</code></pre>
<p>This is standard behavior in banking apps, delivery tracking apps, attendance systems, and anywhere location or session integrity matters. The app doesn't assume the environment is still safe — it checks every time it resumes.</p>
<hr />
<h2 id="heading-auto-logout-after-inactivity">Auto Logout After Inactivity</h2>
<p>For apps that handle sensitive data, automatic logout after a period of inactivity is a compliance requirement in many contexts. <code>didChangeAppLifecycleState</code> gives you a clean way to implement it:</p>
<pre><code class="lang-dart"><span class="hljs-built_in">DateTime?</span> _pausedAt;

<span class="hljs-keyword">void</span> _onAppPaused() {
  _pausedAt = <span class="hljs-built_in">DateTime</span>.now();
}

<span class="hljs-keyword">void</span> _onAppResumed() {
  <span class="hljs-keyword">if</span> (_pausedAt != <span class="hljs-keyword">null</span>) {
    <span class="hljs-keyword">final</span> elapsed = <span class="hljs-built_in">DateTime</span>.now().difference(_pausedAt!);
    <span class="hljs-keyword">if</span> (elapsed &gt; <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(minutes: <span class="hljs-number">10</span>)) {
      _logoutUser();
      <span class="hljs-keyword">return</span>;
    }
  }
  _pausedAt = <span class="hljs-keyword">null</span>;
  <span class="hljs-comment">// Continue normal resume flow</span>
}
</code></pre>
<p>The timeout window depends on the app's sensitivity. Financial apps often use 5 minutes. The pattern is the same regardless.</p>
<hr />
<h2 id="heading-refreshing-stale-data">Refreshing Stale Data</h2>
<p>Rather than polling on a timer or force-refreshing every time a screen is pushed, <code>resumed</code> is a natural moment to check whether data needs updating:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _onAppResumed() {
  <span class="hljs-keyword">final</span> timeSinceLastSync = <span class="hljs-built_in">DateTime</span>.now().difference(_lastSyncTime);
  <span class="hljs-keyword">if</span> (timeSinceLastSync &gt; <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(minutes: <span class="hljs-number">5</span>)) {
    _refreshData();
  }
}
</code></pre>
<p>This feels more intentional than a background timer — it only refreshes when the user actually returns to the app.</p>
<hr />
<h2 id="heading-pausing-and-resuming-resources">Pausing and Resuming Resources</h2>
<p>The <code>paused</code> state is the right place to release resources that shouldn't run in the background:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _onAppPaused() {
  _videoController.pause();
  _locationSubscription.cancel();
  _animationController.stop();
}

<span class="hljs-keyword">void</span> _onAppResumed() {
  <span class="hljs-keyword">if</span> (_shouldAutoPlay) _videoController.play();
  _startLocationUpdates();
}
</code></pre>
<p>Cancelling the location subscription when paused and restarting it on resume prevents unnecessary GPS usage while the app isn't visible. On battery-sensitive devices, this matters.</p>
<hr />
<h2 id="heading-handling-permission-changes">Handling Permission Changes</h2>
<p>A common UX scenario: your app requests a permission, the user declines, then goes to Settings and manually grants it. When they return to your app, you should detect the new permission state without asking again:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> _onAppResumed() {
  _recheckPermissions();
}

Future&lt;<span class="hljs-keyword">void</span>&gt; _recheckPermissions() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> status = <span class="hljs-keyword">await</span> Permission.camera.status;
  <span class="hljs-keyword">if</span> (status.isGranted &amp;&amp; !_cameraReady) {
    _initializeCamera();
  }
}
</code></pre>
<p>Silently re-checking on resume, rather than showing another permission dialog, is the right pattern here.</p>
<hr />
<h2 id="heading-using-applifecyclelistener-newer-api">Using AppLifecycleListener (Newer API)</h2>
<p>Flutter also offers <code>AppLifecycleListener</code>, added in Flutter 3.13, which provides named callbacks instead of a switch statement. For new code, it's cleaner:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AppLifecycleListener _listener;

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  _listener = AppLifecycleListener(
    onResume: _onAppResumed,
    onPause: _onAppPaused,
    onInactive: _onInactive,
  );
}

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> dispose() {
  _listener.dispose();
  <span class="hljs-keyword">super</span>.dispose();
}
</code></pre>
<p>The behavior is identical to <code>WidgetsBindingObserver</code>, but the intent of each callback is explicit from the name rather than requiring a switch on the state enum.</p>
<hr />
<h2 id="heading-a-few-practical-notes">A Few Practical Notes</h2>
<p><strong>Keep resume logic fast.</strong> <code>_onAppResumed</code> fires before the UI is interactive again. If you kick off a slow API call here, the user will notice the lag. Run quick local checks synchronously; defer heavy network work.</p>
<p><strong>Guard against async races.</strong> If <code>_onAppResumed</code> triggers an async operation, the widget might be disposed before it completes. Check <code>mounted</code> before calling <code>setState</code> in any callback that crosses an await boundary.</p>
<p><strong>Centralize your checks.</strong> Rather than duplicating security or permission checks across multiple screens, keep them in a single service class. Your lifecycle handler calls into that service, and the service decides what action to take. This keeps the lifecycle handler thin and the logic testable.</p>
<hr />
<h2 id="heading-the-underlying-point">The Underlying Point</h2>
<p>An app that resumes and blindly trusts its previous state is an app that can be manipulated. The window between backgrounding and resuming is exactly when users — or bad actors — can change conditions your app depends on. <code>didChangeAppLifecycleState</code> is how you close that window. Use it to re-check anything your app's correctness or security depends on, and treat <code>resumed</code> not as a continuation of the previous session but as a fresh opportunity to verify that the environment is still what you expect.</p>
]]></content:encoded></item><item><title><![CDATA[Dart Private Constructors and Factory Methods: Control Object Creation Like a Pro]]></title><description><![CDATA[When Classes Start Misbehaving
At some point in every Dart project, you run into a situation where your classes are being used in ways you never intended. Someone creates an object directly when there should only ever be one. Someone calls an interna...]]></description><link>https://blog.anmolthedeveloper.com/dart-private-constructors-and-factory-methods-control-object-creation-like-a-pro</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/dart-private-constructors-and-factory-methods-control-object-creation-like-a-pro</guid><category><![CDATA[dart-tutorial]]></category><category><![CDATA[Dart]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Object Oriented Programming]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Sun, 05 Oct 2025 06:41:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759646400205/4c98864e-63f1-417f-a3db-f4b235ccd1ad.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-when-classes-start-misbehaving">When Classes Start Misbehaving</h2>
<p>At some point in every Dart project, you run into a situation where your classes are being used in ways you never intended. Someone creates an object directly when there should only ever be one. Someone calls an internal helper from outside the file. Someone instantiates a utility class that was never meant to be instantiated.</p>
<p>The language gives you tools to prevent all of this — private constructors, factory constructors, and private functions. They're not just syntax features; they're the building blocks of intentional API design.</p>
<hr />
<h2 id="heading-constructors-the-starting-point">Constructors: The Starting Point</h2>
<p>A constructor is how you create an object from a class.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyBox</span> </span>{
  ToyBox() {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'A new ToyBox is created!'</span>);
  }
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">var</span> box = ToyBox(); <span class="hljs-comment">// Creates a new instance</span>
}
</code></pre>
<p>This is fine for most cases. But sometimes, you don't want every caller to freely spin up new instances. That's where private constructors come in.</p>
<hr />
<h2 id="heading-private-constructors-controlling-how-objects-are-created">Private Constructors: Controlling How Objects Are Created</h2>
<p>A private constructor uses the <code>ClassName._()</code> syntax. It's accessible only within the same file — nothing outside can call it directly.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyBox</span> </span>{
  ToyBox._(); <span class="hljs-comment">// private constructor</span>

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ToyBox _instance = ToyBox._();

  <span class="hljs-keyword">factory</span> ToyBox() =&gt; _instance;
}
</code></pre>
<p>Now <code>ToyBox._()</code> can't be called from anywhere outside this file. Only the class itself uses it — which makes it the foundation of the Singleton pattern, utility-only classes, and any situation where you want precise control over instantiation.</p>
<hr />
<h2 id="heading-private-constructors-for-static-only-classes">Private Constructors for Static-Only Classes</h2>
<p>One of the most practical uses of private constructors is preventing instantiation of classes that exist purely as namespaces for static methods or constants.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MathUtils</span> </span>{
  MathUtils._(); <span class="hljs-comment">// prevents instantiation</span>

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">double</span> pi = <span class="hljs-number">3.14159</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-built_in">double</span> areaOfCircle(<span class="hljs-built_in">double</span> radius) =&gt; pi * radius * radius;
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-built_in">print</span>(MathUtils.areaOfCircle(<span class="hljs-number">5</span>)); <span class="hljs-comment">// works fine</span>
  <span class="hljs-comment">// var m = MathUtils();           // compile error</span>
}
</code></pre>
<p>The private constructor sends a clear signal: this class isn't meant to be used as an object. You use its static members. Creating an instance makes no sense.</p>
<p>You'll see this pattern throughout Flutter's own codebase — <code>Colors</code>, <code>Icons</code>, and similar classes follow the same approach.</p>
<hr />
<h2 id="heading-factory-constructors-smart-object-creation">Factory Constructors: Smart Object Creation</h2>
<p>A factory constructor looks like a regular constructor from the outside, but internally it has full control over what gets returned. It doesn't have to create a new instance every time.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToyBox</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ToyBox _instance = ToyBox._();

  ToyBox._();

  <span class="hljs-keyword">factory</span> ToyBox() {
    <span class="hljs-keyword">return</span> _instance; <span class="hljs-comment">// same instance every time</span>
  }
}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">var</span> box1 = ToyBox();
  <span class="hljs-keyword">var</span> box2 = ToyBox();
  <span class="hljs-built_in">print</span>(box1 == box2); <span class="hljs-comment">// true</span>
}
</code></pre>
<p>Factory constructors are useful when you need to return a cached instance, choose between subclasses at runtime, or perform validation before construction. They hide that complexity from the caller.</p>
<hr />
<h2 id="heading-factory-constructors-that-return-subclasses">Factory Constructors That Return Subclasses</h2>
<p>A factory constructor can act as a dispatch mechanism — the caller asks for a <code>Shape</code>, and the factory decides which concrete type to return.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Shape</span> </span>{
  <span class="hljs-keyword">factory</span> Shape(<span class="hljs-built_in">String</span> type) {
    <span class="hljs-keyword">if</span> (type == <span class="hljs-string">'circle'</span>) <span class="hljs-keyword">return</span> Circle();
    <span class="hljs-keyword">if</span> (type == <span class="hljs-string">'square'</span>) <span class="hljs-keyword">return</span> Square();
    <span class="hljs-keyword">throw</span> ArgumentError(<span class="hljs-string">'Unknown shape type: <span class="hljs-subst">$type</span>'</span>);
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Circle</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Shape</span> </span>{}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Square</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Shape</span> </span>{}

<span class="hljs-keyword">void</span> main() {
  <span class="hljs-keyword">var</span> s = Shape(<span class="hljs-string">'circle'</span>);
  <span class="hljs-built_in">print</span>(s.runtimeType); <span class="hljs-comment">// Circle</span>
}
</code></pre>
<p>This is encapsulation in practice. The caller doesn't need to know which class they're getting — they just ask for a shape and work with the result.</p>
<hr />
<h2 id="heading-private-functions-keeping-internals-internal">Private Functions: Keeping Internals Internal</h2>
<p>Private functions — any method prefixed with <code>_</code> — are inaccessible outside the library. They let you keep your class's public surface area small and clean while moving implementation details out of sight.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordValidator</span> </span>{
  <span class="hljs-built_in">bool</span> isValid(<span class="hljs-built_in">String</span> password) {
    <span class="hljs-keyword">return</span> _hasMinLength(password) &amp;&amp; _hasUppercase(password);
  }

  <span class="hljs-built_in">bool</span> _hasMinLength(<span class="hljs-built_in">String</span> password) =&gt; password.length &gt;= <span class="hljs-number">8</span>;
  <span class="hljs-built_in">bool</span> _hasUppercase(<span class="hljs-built_in">String</span> password) =&gt; password.contains(<span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'[A-Z]'</span>));
}
</code></pre>
<p>The public API is just <code>isValid()</code>. The two private helpers do the actual work but are invisible to anyone using this class. This is what good encapsulation looks like — you expose what's necessary and hide everything else.</p>
<hr />
<h2 id="heading-real-world-use-cases">Real-World Use Cases</h2>
<p><strong>Singleton for shared services</strong></p>
<p>When you want a single shared instance across the entire app:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DatabaseManager</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> DatabaseManager _instance = DatabaseManager._();

  DatabaseManager._();

  <span class="hljs-keyword">factory</span> DatabaseManager() =&gt; _instance;

  <span class="hljs-keyword">void</span> connect() =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">'Connected to DB!'</span>);
}
</code></pre>
<p>Every call to <code>DatabaseManager()</code> returns the same object. No matter how many places in the app call it, they're all working with the same instance.</p>
<p><strong>Factory for JSON deserialization</strong></p>
<p>This is the most common real-world use of factory constructors in Flutter:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> age;

  User._(<span class="hljs-keyword">this</span>.name, <span class="hljs-keyword">this</span>.age);

  <span class="hljs-keyword">factory</span> User.fromJson(<span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">dynamic</span>&gt; json) {
    <span class="hljs-keyword">return</span> User._(json[<span class="hljs-string">'name'</span>], json[<span class="hljs-string">'age'</span>]);
  }
}
</code></pre>
<p>The factory hides parsing logic from the caller. If you later need to add validation or caching, you do it inside the factory — none of the calling code changes.</p>
<p><strong>Static-only utility class</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DateUtils</span> </span>{
  DateUtils._();

  <span class="hljs-keyword">static</span> <span class="hljs-built_in">String</span> today() =&gt; <span class="hljs-built_in">DateTime</span>.now().toIso8601String();
}
</code></pre>
<p>Nothing stops callers from misusing <code>DateUtils</code> as an object — except this private constructor.</p>
<p><strong>Private helpers for internal logic</strong></p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailValidator</span> </span>{
  <span class="hljs-built_in">bool</span> isValid(<span class="hljs-built_in">String</span> email) =&gt; _containsAtSymbol(email) &amp;&amp; _isProperLength(email);

  <span class="hljs-built_in">bool</span> _containsAtSymbol(<span class="hljs-built_in">String</span> email) =&gt; email.contains(<span class="hljs-string">'@'</span>);
  <span class="hljs-built_in">bool</span> _isProperLength(<span class="hljs-built_in">String</span> email) =&gt; email.length &gt;= <span class="hljs-number">6</span>;
}
</code></pre>
<p>One clean public method. Two private helpers doing the work behind the scenes.</p>
<hr />
<h2 id="heading-when-to-use-each">When to Use Each</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Concept</td><td>When to reach for it</td></tr>
</thead>
<tbody>
<tr>
<td>Private constructor</td><td>When you need a Singleton, or a class that should never be instantiated</td></tr>
<tr>
<td>Factory constructor</td><td>When object creation involves logic, caching, or subclass selection</td></tr>
<tr>
<td>Private functions</td><td>When internal helpers shouldn't be part of the public API</td></tr>
<tr>
<td>Static-only class</td><td>When the class is a container for constants or utility methods</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-the-core-idea">The Core Idea</h2>
<p>Private constructors and factory methods aren't clever tricks — they're the tools Dart gives you for writing APIs that are hard to misuse. The more explicitly you define how your classes should be created and accessed, the less time you spend debugging code that used them in ways you didn't anticipate.</p>
<p>Good code hides what should be hidden and exposes only what's meant to be used. These three features — private constructors, factory constructors, and private functions — are how you enforce that in Dart.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Custom UI: How to Use CustomPaint, RenderBox and Canvas to Build Any Design]]></title><description><![CDATA[Flutter's widget system is genuinely impressive. Stack, wrap, align, animate, react — it handles the vast majority of UI requirements without you ever needing to reach for anything lower-level. But eventually, you hit a wall. The design calls for som...]]></description><link>https://blog.anmolthedeveloper.com/flutter-custom-ui-how-to-use-custompaint-renderbox-and-canvas-to-build-any-design</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-custom-ui-how-to-use-custompaint-renderbox-and-canvas-to-build-any-design</guid><category><![CDATA[Android]]></category><category><![CDATA[CustomPaint]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 22 May 2025 11:23:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747912937993/11ff49c5-c1f1-43eb-98c3-322225cf201a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Flutter's widget system is genuinely impressive. Stack, wrap, align, animate, react — it handles the vast majority of UI requirements without you ever needing to reach for anything lower-level. But eventually, you hit a wall. The design calls for something the widget tree just can't express cleanly: a fluid wave that responds to touch, a custom graph with precise control over every drawn element, a radial menu with non-standard layout logic.</p>
<p>When that happens, you need to go below the widget layer. Here's how.</p>
<hr />
<h2 id="heading-the-limit-of-widget-composition">The Limit of Widget Composition</h2>
<p>Composing existing widgets is fast and reliable, but it has a ceiling. Situations where widget composition genuinely breaks down include:</p>
<ul>
<li>Drawing smooth curves or custom shapes that aren't expressible with existing widgets</li>
<li>Building custom charts, waveforms, or progress arcs</li>
<li>Laying out children in non-standard arrangements — radial menus, fisheye lists, anything that doesn't follow linear flow</li>
<li>Building game-like UIs where you need pixel-perfect control over what's drawn and when</li>
</ul>
<p>For these, Flutter gives you three layers of increasing power — and complexity.</p>
<hr />
<h2 id="heading-level-1-custompaint-and-custompainter">Level 1: CustomPaint and CustomPainter</h2>
<p><code>CustomPaint</code> is Flutter's first escape hatch from the widget system. It hands you a <code>Canvas</code> and a <code>Paint</code> object and gets out of the way. You describe what to draw; Flutter draws it.</p>
<p>Here's a wave background as a practical example:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WavePainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> paint = Paint()
      ..color = Colors.blueAccent
      ..style = PaintingStyle.fill;

    <span class="hljs-keyword">final</span> path = Path()
      ..moveTo(<span class="hljs-number">0</span>, size.height * <span class="hljs-number">0.8</span>)
      ..quadraticBezierTo(
        size.width * <span class="hljs-number">0.5</span>,
        size.height,
        size.width,
        size.height * <span class="hljs-number">0.8</span>,
      )
      ..lineTo(size.width, <span class="hljs-number">0</span>)
      ..lineTo(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
      ..close();

    canvas.drawPath(path, paint);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(CustomPainter oldDelegate) =&gt; <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>Using it in your widget tree:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WaveHeader</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> CustomPaint(
      painter: WavePainter(),
      child: Container(height: <span class="hljs-number">200</span>),
    );
  }
}
</code></pre>
<p><code>shouldRepaint</code> controls when Flutter redraws your painter — returning <code>false</code> here means it only draws once, which is fine for static shapes. For animated or interactive painters, you'd return <code>true</code> or compare old and new delegate values.</p>
<p>This approach handles any shape you can describe with a <code>Path</code> — curves, polygons, gradients, arbitrary fills. It's the right tool for anything visual that doesn't fit standard widget constraints.</p>
<hr />
<h2 id="heading-level-2-renderbox">Level 2: RenderBox</h2>
<p>When you need control over not just painting but also layout and hit testing, <code>RenderBox</code> is the next step. This is Flutter's rendering layer — the same level at which built-in widgets like <code>Container</code> and <code>Text</code> are actually implemented.</p>
<p>It sounds intimidating, but the structure is straightforward:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyRenderBox</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">RenderBox</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> performLayout() {
    size = constraints.constrain(Size(<span class="hljs-number">200</span>, <span class="hljs-number">100</span>));
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(PaintingContext context, Offset offset) {
    <span class="hljs-keyword">final</span> canvas = context.canvas;
    <span class="hljs-keyword">final</span> paint = Paint()..color = Colors.green;
    canvas.drawRect(offset &amp; size, paint);
  }
}
</code></pre>
<p>Wrapping it as a widget:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GreenBox</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LeafRenderObjectWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  RenderObject createRenderObject(BuildContext context) {
    <span class="hljs-keyword">return</span> MyRenderBox();
  }
}
</code></pre>
<p>Drop <code>GreenBox()</code> anywhere in your widget tree. It handles its own size and painting without delegating to any other widget.</p>
<p>You can also handle gestures at this layer:</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-built_in">bool</span> hitTestSelf(Offset position) =&gt; <span class="hljs-keyword">true</span>;

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> handleEvent(PointerEvent event, HitTestEntry entry) {
  <span class="hljs-keyword">if</span> (event <span class="hljs-keyword">is</span> PointerDownEvent) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Touched at <span class="hljs-subst">${event.position}</span>"</span>);
  }
}
</code></pre>
<p>This level is appropriate when <code>CustomPainter</code> isn't enough — when your component needs to participate in layout, own its size, or handle input in ways that don't map cleanly to the gesture widgets above.</p>
<hr />
<h2 id="heading-level-3-scenebuilder">Level 3: SceneBuilder</h2>
<p><code>SceneBuilder</code> sits at the very bottom of Flutter's rendering stack, below widgets and below <code>RenderBox</code>. It's how Flutter assembles the final frame before handing it to the GPU.</p>
<p>In practice, you'll rarely need this in a standard app. The cases where it makes sense are narrow:</p>
<ul>
<li>Building a plugin that integrates Flutter into another rendering engine</li>
<li>Working on ultra-high-performance rendering where you want to eliminate all widget overhead</li>
<li>Advanced graphics work involving custom shaders or hybrid compositions</li>
</ul>
<p>It's worth knowing it exists, but for most production apps — even complex ones — <code>CustomPainter</code> or <code>RenderBox</code> will take you as far as you need to go.</p>
<hr />
<h2 id="heading-useful-tools">Useful Tools</h2>
<p>A few things worth having in your toolkit when working at this level:</p>
<ul>
<li><strong>Flutter DevTools</strong> — inspect layout, painting, and performance. The "Highlight Repaints" toggle is particularly useful for spotting unnecessary redraws in CustomPainter work.</li>
<li><strong>ShaderMask and FragmentProgram</strong> — bring GPU shaders into your Flutter widgets. Useful for effects that are expensive to approximate with the Canvas API.</li>
<li><strong>AnimationController with CustomPaint</strong> — the standard pattern for animating custom drawings. Drive your painter's parameters from an animation value and call <code>setState</code> (or use a <code>Listenable</code>) to trigger repaints.</li>
<li><strong>Flame</strong> — a lightweight game engine built on Flutter. Worth considering for UIs that are fundamentally game-like in nature rather than document-like.</li>
</ul>
<hr />
<h2 id="heading-when-to-reach-for-each-layer">When to Reach for Each Layer</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Situation</td><td>Reach for</td></tr>
</thead>
<tbody>
<tr>
<td>Custom shapes, gradients, or visual effects</td><td><code>CustomPainter</code></td></tr>
<tr>
<td>Full control over layout, paint, and touch</td><td><code>RenderBox</code></td></tr>
<tr>
<td>Small tweaks to existing widget appearance</td><td>Compose or extend widgets</td></tr>
<tr>
<td>Graph-heavy or animated drawing</td><td><code>CustomPainter</code> with <code>AnimationController</code></td></tr>
<tr>
<td>Rendering without the widget system</td><td><code>SceneBuilder</code></td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-real-world-scenarios">Real-World Scenarios</h2>
<ul>
<li><strong>Fitness app progress rings</strong> — arcs drawn with <code>CustomPainter</code>, driven by animation values</li>
<li><strong>Voice recorder waveform</strong> — real-time canvas drawing updated from an audio stream</li>
<li><strong>Interactive mind map</strong> — <code>RenderBox</code> with custom hit testing for each node</li>
<li><strong>Design tool UI</strong> — combination of <code>RenderBox</code> for layout and <code>CustomPainter</code> for content</li>
</ul>
<hr />
<h2 id="heading-the-underlying-point">The Underlying Point</h2>
<p>Flutter is, at its core, a canvas. The widget system is a convenient abstraction built on top of that canvas, and it's the right starting point for almost everything. But when your design genuinely requires it, stepping below widgets is not as scary as it looks — and the control you gain is well worth the extra complexity.</p>
<p>Build that wavy onboarding screen. Draw that custom chart. Flutter has the tools for it.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Platform Channels Explained: MethodChannel, EventChannel and BasicMessageChannel]]></title><description><![CDATA[Flutter handles UI beautifully. Widgets compose, animations run smooth, and the same codebase works across Android and iOS without much friction. But there's a gap — Flutter's Dart layer has no direct access to platform APIs. It can't read the batter...]]></description><link>https://blog.anmolthedeveloper.com/flutter-platform-channels-explained-methodchannel-eventchannel-and-basicmessagechannel</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-platform-channels-explained-methodchannel-eventchannel-and-basicmessagechannel</guid><category><![CDATA[Android]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[native development]]></category><category><![CDATA[platform channels]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 15 May 2025 05:48:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747288009112/7229b3ef-ed0a-4278-9ffa-9431ab66089e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Flutter handles UI beautifully. Widgets compose, animations run smooth, and the same codebase works across Android and iOS without much friction. But there's a gap — Flutter's Dart layer has no direct access to platform APIs. It can't read the battery level, stream sensor data, or call a native SDK without some help.</p>
<p>Platform Channels are that help. They're Flutter's built-in bridge for passing messages between Dart and native code. There are three of them, each suited to a different communication pattern.</p>
<hr />
<h2 id="heading-what-is-a-platform-channel">What Is a Platform Channel?</h2>
<p>Think of Flutter and native code as two teams working in separate rooms. They can't walk into each other's spaces directly, but they can pass notes through a slot in the wall. Platform Channels are that slot.</p>
<p>Every channel has a name — a string identifier shared by both sides so they can find each other. Beyond that, the three channel types differ in how the conversation flows.</p>
<hr />
<h2 id="heading-methodchannel-ask-once-get-an-answer">MethodChannel: Ask Once, Get an Answer</h2>
<p><code>MethodChannel</code> is the most common type. Flutter sends a named method call, native code handles it and replies, and that's the end of the exchange.</p>
<p>It's the right tool any time you want to ask native code to do something and get a result back — check the battery, read a device ID, trigger a native dialog.</p>
<p><strong>Flutter (Dart):</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">const</span> platform = MethodChannel(<span class="hljs-string">'samples.flutter.dev/battery'</span>);

Future&lt;<span class="hljs-keyword">void</span>&gt; getBatteryLevel() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">int</span> result = <span class="hljs-keyword">await</span> platform.invokeMethod(<span class="hljs-string">'getBatteryLevel'</span>);
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'Battery is <span class="hljs-subst">$result</span>%.'</span>);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error: <span class="hljs-subst">$e</span>'</span>);
  }
}
</code></pre>
<p><strong>Android (Kotlin):</strong></p>
<pre><code class="lang-kotlin">MethodChannel(flutterEngine.dartExecutor.binaryMessenger, <span class="hljs-string">"samples.flutter.dev/battery"</span>)
  .setMethodCallHandler { call, result -&gt;
    <span class="hljs-keyword">if</span> (call.method == <span class="hljs-string">"getBatteryLevel"</span>) {
      <span class="hljs-keyword">val</span> batteryLevel = getBatteryLevel()
      result.success(batteryLevel)
    } <span class="hljs-keyword">else</span> {
      result.notImplemented()
    }
  }
</code></pre>
<p>Both sides reference the same channel name. Flutter calls <code>invokeMethod</code>, native handles it, and calls <code>result.success()</code> with the response. One request, one response, done.</p>
<hr />
<h2 id="heading-basicmessagechannel-two-way-messaging-without-a-command-structure">BasicMessageChannel: Two-Way Messaging Without a Command Structure</h2>
<p><code>BasicMessageChannel</code> is less formal. Either side can send a message at any point, and the other side can reply — or not. There's no method name, no command-response pairing. It's just a communication channel between two parties.</p>
<p>This suits situations where you want ongoing, loosely structured communication without the overhead of named method calls.</p>
<p><strong>Flutter (Dart):</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">const</span> channel = BasicMessageChannel&lt;<span class="hljs-built_in">String</span>&gt;(
  <span class="hljs-string">'samples.flutter.dev/message'</span>,
  StringCodec(),
);

<span class="hljs-keyword">void</span> sendMessage() {
  channel.send(<span class="hljs-string">'Hello from Flutter!'</span>);
}

<span class="hljs-keyword">void</span> receiveMessages() {
  channel.setMessageHandler((message) <span class="hljs-keyword">async</span> {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'From Native: <span class="hljs-subst">$message</span>'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-string">'Received.'</span>;
  });
}
</code></pre>
<p><strong>Android (Kotlin):</strong></p>
<pre><code class="lang-kotlin">channel = BasicMessageChannel(
  flutterEngine.dartExecutor.binaryMessenger,
  <span class="hljs-string">"samples.flutter.dev/message"</span>,
  StringCodec.INSTANCE
)

channel.setMessageHandler { message, reply -&gt;
  println(<span class="hljs-string">"Flutter says: <span class="hljs-variable">$message</span>"</span>)
  reply.reply(<span class="hljs-string">"Got it."</span>)
}
</code></pre>
<p>Both sides set a message handler and can send messages independently. The codec (<code>StringCodec</code> here) defines how messages are serialized — you can also use <code>StandardMessageCodec</code> for more complex data types.</p>
<hr />
<h2 id="heading-eventchannel-continuous-data-from-native-to-flutter">EventChannel: Continuous Data from Native to Flutter</h2>
<p><code>EventChannel</code> is built for streaming. Native code produces a continuous flow of data — sensor readings, location updates, audio levels — and Flutter subscribes to that stream. The data flows until either side cancels.</p>
<p><strong>Flutter (Dart):</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">const</span> eventChannel = EventChannel(<span class="hljs-string">'samples.flutter.dev/sensor'</span>);

<span class="hljs-keyword">void</span> listenToSensor() {
  eventChannel.receiveBroadcastStream().listen(
    (event) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Sensor update: <span class="hljs-subst">$event</span>'</span>);
    },
    onError: (error) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error: <span class="hljs-subst">$error</span>'</span>);
    },
  );
}
</code></pre>
<p><strong>Android (Kotlin):</strong></p>
<pre><code class="lang-kotlin">EventChannel(flutterEngine.dartExecutor.binaryMessenger, <span class="hljs-string">"samples.flutter.dev/sensor"</span>)
  .setStreamHandler(<span class="hljs-keyword">object</span> : EventChannel.StreamHandler {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onListen</span><span class="hljs-params">(arguments: <span class="hljs-type">Any</span>?, events: <span class="hljs-type">EventChannel</span>.<span class="hljs-type">EventSink</span>)</span></span> {
      startSendingSensorData(events)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCancel</span><span class="hljs-params">(arguments: <span class="hljs-type">Any</span>?)</span></span> {
      stopSensorUpdates()
    }
  })
</code></pre>
<p>When Flutter subscribes, <code>onListen</code> fires on the native side — that's when you start pushing data to the <code>EventSink</code>. When Flutter unsubscribes or the stream is cancelled, <code>onCancel</code> fires and you can clean up.</p>
<p>This is the right choice for any native data source that produces updates over time: accelerometer, GPS, microphone input, network connectivity changes.</p>
<hr />
<h2 id="heading-choosing-the-right-channel">Choosing the Right Channel</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Channel</td><td>Communication Pattern</td><td>Best For</td></tr>
</thead>
<tbody>
<tr>
<td>MethodChannel</td><td>Request → Response</td><td>One-off commands: battery level, device info, native dialogs</td></tr>
<tr>
<td>BasicMessageChannel</td><td>Free-form bi-directional</td><td>Loosely structured ongoing communication</td></tr>
<tr>
<td>EventChannel</td><td>Native → Flutter stream</td><td>Continuous data: sensors, location, audio</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-a-few-practical-notes">A Few Practical Notes</h2>
<p><strong>Channel names are just strings, but treat them seriously.</strong> Use a reverse-domain format like <code>com.yourapp.feature/channel</code> to avoid collisions, especially if you're using third-party plugins that also register channels.</p>
<p><strong>Error handling matters on both sides.</strong> On the Dart side, <code>invokeMethod</code> throws a <code>PlatformException</code> if native code calls <code>result.error()</code>. Catch it explicitly rather than letting it bubble up silently.</p>
<p><strong>Threading on Android.</strong> The <code>MethodCallHandler</code> runs on the main thread by default. If your native operation is blocking or IO-heavy, dispatch it to a background thread and call <code>result.success()</code> from there when you're done.</p>
<p><strong>iOS works the same way.</strong> The channel names and Dart code are identical; only the native implementation differs (Swift or Objective-C instead of Kotlin/Java). Structurally, the pattern is the same on both platforms.</p>
<hr />
<h2 id="heading-in-summary">In Summary</h2>
<p>Platform Channels are how Flutter reaches into the native platform when it needs to. <code>MethodChannel</code> handles one-off requests. <code>BasicMessageChannel</code> handles free-form conversation. <code>EventChannel</code> handles data streams.</p>
<p>Most Flutter apps only need <code>MethodChannel</code> for occasional platform calls and <code>EventChannel</code> for anything that streams. If you've been avoiding native integration because it seemed complex, the actual API surface is smaller than it looks — it's just message passing with a name attached.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter CustomPaint and GestureDetector: Draw Custom Widgets and Handle Touch Input]]></title><description><![CDATA[Most Flutter developers spend their time composing existing widgets — Container, Row, Column, Card. That's fine for the majority of UIs. But at some point, you'll get a design that no combination of built-in widgets can produce cleanly. A custom shap...]]></description><link>https://blog.anmolthedeveloper.com/flutter-custompaint-and-gesturedetector-draw-custom-widgets-and-handle-touch-input</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-custompaint-and-gesturedetector-draw-custom-widgets-and-handle-touch-input</guid><category><![CDATA[Android]]></category><category><![CDATA[canvas]]></category><category><![CDATA[CustomPaint]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Flutter Widgets]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 15 May 2025 05:33:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747287027281/b370e760-a726-45fd-852d-b257799ec05a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most Flutter developers spend their time composing existing widgets — <code>Container</code>, <code>Row</code>, <code>Column</code>, <code>Card</code>. That's fine for the majority of UIs. But at some point, you'll get a design that no combination of built-in widgets can produce cleanly. A custom shape, a hand-drawn arc, an interactive canvas where users can sketch.</p>
<p>That's where <code>CustomPaint</code> and <code>GestureDetector</code> come in. Together, they let you draw anything and respond to any touch input.</p>
<hr />
<h2 id="heading-what-custompaint-actually-does">What CustomPaint Actually Does</h2>
<p><code>CustomPaint</code> is Flutter's drawing surface. It gives your code direct access to a <code>Canvas</code> object — the same drawing API that Flutter itself uses internally. With it, you can draw lines, arcs, bezier curves, rectangles, circles, gradients, and arbitrary paths.</p>
<p>The key class is <code>CustomPainter</code>. You subclass it, implement two methods, and pass an instance to <code>CustomPaint</code>:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CirclePainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> paint = Paint()
      ..color = Colors.deepPurple
      ..style = PaintingStyle.fill;

    <span class="hljs-keyword">final</span> center = Offset(size.width / <span class="hljs-number">2</span>, size.height / <span class="hljs-number">2</span>);
    <span class="hljs-keyword">final</span> radius = size.width * <span class="hljs-number">0.4</span>;

    canvas.drawCircle(center, radius, paint);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(CirclePainter oldDelegate) =&gt; <span class="hljs-keyword">false</span>;
}
</code></pre>
<p>Using it:</p>
<pre><code class="lang-dart">CustomPaint(
  painter: CirclePainter(),
  size: <span class="hljs-keyword">const</span> Size(<span class="hljs-number">300</span>, <span class="hljs-number">300</span>),
)
</code></pre>
<p><code>shouldRepaint</code> determines whether Flutter redraws when the widget rebuilds. Return <code>false</code> for static visuals; return <code>true</code> (or compare old and new delegate properties) for anything that changes over time.</p>
<hr />
<h2 id="heading-drawing-more-complex-shapes-with-path">Drawing More Complex Shapes with Path</h2>
<p>The <code>Path</code> class is the most powerful drawing tool in the Canvas API. It lets you describe any shape as a sequence of moves, lines, and curves.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WavePainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> paint = Paint()
      ..color = Colors.teal
      ..style = PaintingStyle.fill;

    <span class="hljs-keyword">final</span> path = Path()
      ..moveTo(<span class="hljs-number">0</span>, size.height * <span class="hljs-number">0.6</span>)
      ..quadraticBezierTo(
        size.width * <span class="hljs-number">0.25</span>, size.height * <span class="hljs-number">0.45</span>,
        size.width * <span class="hljs-number">0.5</span>, size.height * <span class="hljs-number">0.6</span>,
      )
      ..quadraticBezierTo(
        size.width * <span class="hljs-number">0.75</span>, size.height * <span class="hljs-number">0.75</span>,
        size.width, size.height * <span class="hljs-number">0.6</span>,
      )
      ..lineTo(size.width, size.height)
      ..lineTo(<span class="hljs-number">0</span>, size.height)
      ..close();

    canvas.drawPath(path, paint);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(WavePainter oldDelegate) =&gt; <span class="hljs-keyword">false</span>;
}
</code></pre>
<p><code>quadraticBezierTo</code> takes a control point and an end point, producing a smooth curve. Chain multiple curves together to get a wave, a mountain outline, a custom app bar shape — anything your design requires.</p>
<hr />
<h2 id="heading-making-it-interactive-with-gesturedetector">Making It Interactive with GestureDetector</h2>
<p>A static drawing is useful, but the real power comes from combining <code>CustomPaint</code> with <code>GestureDetector</code>. You can let users draw, tap elements, or interact with anything on the canvas.</p>
<p>Here's a simple free-draw canvas — users drag their finger and it draws a stroke:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DrawingCanvas</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> DrawingCanvas({<span class="hljs-keyword">super</span>.key});

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_DrawingCanvasState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">DrawingCanvas</span>&gt; </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Offset?&gt; _points = [];

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> GestureDetector(
      onPanUpdate: (details) {
        setState(() {
          <span class="hljs-keyword">final</span> box = context.findRenderObject() <span class="hljs-keyword">as</span> RenderBox;
          _points.add(box.globalToLocal(details.globalPosition));
        });
      },
      onPanEnd: (_) {
        setState(() =&gt; _points.add(<span class="hljs-keyword">null</span>)); <span class="hljs-comment">// null signals a break between strokes</span>
      },
      child: CustomPaint(
        painter: StrokePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StrokePainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;Offset?&gt; points;

  StrokePainter(<span class="hljs-keyword">this</span>.points);

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> paint = Paint()
      ..color = Colors.black
      ..strokeWidth = <span class="hljs-number">3.0</span>
      ..strokeCap = StrokeCap.round;

    <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; points.length - <span class="hljs-number">1</span>; i++) {
      <span class="hljs-keyword">if</span> (points[i] != <span class="hljs-keyword">null</span> &amp;&amp; points[i + <span class="hljs-number">1</span>] != <span class="hljs-keyword">null</span>) {
        canvas.drawLine(points[i]!, points[i + <span class="hljs-number">1</span>]!, paint);
      }
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(StrokePainter oldDelegate) =&gt; oldDelegate.points != points;
}
</code></pre>
<p>A few things to note here: <code>onPanUpdate</code> fires continuously as the finger moves, providing position updates. <code>globalToLocal</code> converts screen coordinates to canvas-local coordinates, which is necessary when the canvas isn't at the top-left of the screen. The <code>null</code> sentinel between strokes means lifting and putting down the finger starts a new stroke.</p>
<hr />
<h2 id="heading-hit-testing-knowing-what-was-tapped">Hit Testing: Knowing What Was Tapped</h2>
<p>Sometimes you don't want to respond to taps on the whole canvas — you want to detect when the user taps a specific thing you drew. For that, <code>CustomPainter</code> has a <code>hitTest</code> method.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TappableCirclePainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-keyword">final</span> Offset center;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> radius;

  TappableCirclePainter({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.center, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.radius});

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> paint = Paint()..color = Colors.orange;
    canvas.drawCircle(center, radius, paint);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> hitTest(Offset position) {
    <span class="hljs-keyword">return</span> (position - center).distance &lt;= radius;
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(TappableCirclePainter oldDelegate) =&gt;
      oldDelegate.center != center || oldDelegate.radius != radius;
}
</code></pre>
<p>When <code>hitTest</code> returns <code>true</code>, the <code>GestureDetector</code> wrapping this <code>CustomPaint</code> will fire its tap callbacks. When it returns <code>false</code>, the tap passes through as if nothing was there. This gives you precise, geometry-aware hit detection on anything you've drawn.</p>
<hr />
<h2 id="heading-animating-your-drawings">Animating Your Drawings</h2>
<p>Static drawings are useful; animated ones are memorable. The standard approach is to drive a <code>CustomPainter</code> from an <code>AnimationController</code>:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PulsingCircle</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> PulsingCircle({<span class="hljs-keyword">super</span>.key});

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_PulsingCircleState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">PulsingCircle</span>&gt;
    <span class="hljs-title">with</span> <span class="hljs-title">SingleTickerProviderStateMixin</span> </span>{
  <span class="hljs-keyword">late</span> AnimationController _controller;
  <span class="hljs-keyword">late</span> Animation&lt;<span class="hljs-built_in">double</span>&gt; _radiusAnim;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _controller = AnimationController(
      vsync: <span class="hljs-keyword">this</span>,
      duration: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>),
    )..repeat(reverse: <span class="hljs-keyword">true</span>);

    _radiusAnim = Tween&lt;<span class="hljs-built_in">double</span>&gt;(begin: <span class="hljs-number">40</span>, end: <span class="hljs-number">80</span>).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> AnimatedBuilder(
      animation: _radiusAnim,
      builder: (context, _) {
        <span class="hljs-keyword">return</span> CustomPaint(
          painter: CircleRadiusPainter(radius: _radiusAnim.value),
          size: <span class="hljs-keyword">const</span> Size(<span class="hljs-number">200</span>, <span class="hljs-number">200</span>),
        );
      },
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CircleRadiusPainter</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">CustomPainter</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> radius;
  CircleRadiusPainter({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.radius});

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> paint(Canvas canvas, Size size) {
    <span class="hljs-keyword">final</span> center = Offset(size.width / <span class="hljs-number">2</span>, size.height / <span class="hljs-number">2</span>);
    canvas.drawCircle(center, radius, Paint()..color = Colors.indigo);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">bool</span> shouldRepaint(CircleRadiusPainter old) =&gt; old.radius != radius;
}
</code></pre>
<p><code>AnimatedBuilder</code> rebuilds only the subtree it wraps when the animation ticks — efficient and clean. Pass the animated value as a parameter to your painter, and return <code>true</code> from <code>shouldRepaint</code> whenever it changes.</p>
<hr />
<h2 id="heading-performance-considerations">Performance Considerations</h2>
<p>A few things to keep in mind as your custom painters get more complex:</p>
<p><code>shouldRepaint</code> is critical. Every time Flutter rebuilds the parent widget, it calls <code>shouldRepaint</code>. If you return <code>true</code> unconditionally, your painter redraws on every build — which is expensive. Compare the old and new delegate properties and return <code>false</code> when nothing meaningful has changed.</p>
<p>Use <code>repaintBoundary</code> for complex painters. Wrapping a <code>CustomPaint</code> in a <code>RepaintBoundary</code> widget tells Flutter to cache its layer separately, so it doesn't get redrawn when unrelated parts of the tree rebuild.</p>
<p>Avoid allocating <code>Paint</code> and <code>Path</code> objects inside <code>paint()</code>. These objects get created on every frame if they're constructed inside the method. Create them as fields on the painter (or use <code>const</code> where possible) and reuse them.</p>
<hr />
<h2 id="heading-what-you-can-build-with-this">What You Can Build With This</h2>
<ul>
<li>Custom progress indicators — arcs, segmented rings, waveform-style bars</li>
<li>Interactive drawing and annotation tools</li>
<li>Chart and data visualization components</li>
<li>Custom onboarding illustrations that animate into view</li>
<li>Game-like UI elements with touch interaction</li>
</ul>
<p>The Canvas API is lower-level than widgets, but it's not complicated once you get the mental model: you describe what to draw in <code>paint()</code>, control when it redraws with <code>shouldRepaint()</code>, and wire up interaction through <code>GestureDetector</code> or <code>hitTest</code>. That's really the whole thing.</p>
]]></content:encoded></item><item><title><![CDATA[How to Write Flutter Code Documentation That Actually Helps: A Practical Dart Doc Guide]]></title><description><![CDATA[The Method Named doMagic()
It was a Tuesday. I opened a project I hadn't touched in four months. My goal was to fix a simple bug — something I expected to take twenty minutes.
Instead I found a method named doMagic().
I stared at it. Why is it async?...]]></description><link>https://blog.anmolthedeveloper.com/dear-future-me-a-flutter-devs-guide-to-writing-code-docs-without-crying-later</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/dear-future-me-a-flutter-devs-guide-to-writing-code-docs-without-crying-later</guid><category><![CDATA[best practices]]></category><category><![CDATA[clean code]]></category><category><![CDATA[Dart]]></category><category><![CDATA[documentation]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Programming Tips]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Wed, 23 Apr 2025 06:13:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745388634398/ab3da2ba-ff8a-4420-a8e6-384bb889199b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-method-named-domagic">The Method Named doMagic()</h2>
<p>It was a Tuesday. I opened a project I hadn't touched in four months. My goal was to fix a simple bug — something I expected to take twenty minutes.</p>
<p>Instead I found a method named <code>doMagic()</code>.</p>
<p>I stared at it. Why is it <code>async</code>? Why does it mutate <code>context</code> and a global variable called <code>spookyGhost</code>? I checked for comments. There were none. Not even a half-hearted TODO.</p>
<p>Past me had written this code. Present me had no idea what it did.</p>
<p>That's the thing about code documentation — you don't feel the need for it until you desperately need it and it isn't there.</p>
<hr />
<h2 id="heading-clean-code-isnt-the-same-as-understandable-code">Clean Code Isn't the Same as Understandable Code</h2>
<p>Most developers, when pushed on this topic, will say: "I write clean code. It's self-documenting."</p>
<p>Here's a quick test of that:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> thing = getThing(userId, <span class="hljs-keyword">true</span>);
</code></pre>
<p>What's <code>thing</code>? What does <code>true</code> mean in this context? What does <code>getThing</code> actually retrieve?</p>
<p>If you can't answer those questions from the call site alone, the code isn't self-documenting — it just looks clean. Clean code tells you <em>how</em> something works. Documentation tells you <em>why</em> it exists and <em>when</em> to use it. You need both.</p>
<hr />
<h2 id="heading-dart-doc-basics">Dart Doc Basics</h2>
<p>Dart uses <code>///</code> for documentation comments. These aren't just for humans — DartDoc and IDEs parse them, and the output shows up in hover tooltips, generated API docs, and code completion.</p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Loads the user's preferred widget based on stored preferences.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// Typically called from the dashboard on first render. If no preferences</span></span>
<span class="hljs-comment">/// <span class="markdown">are found, defaults to the most popular widget for that region.</span></span>
Widget getThing(<span class="hljs-built_in">String</span> userId, <span class="hljs-built_in">bool</span> fallbackToDefault) { ... }
</code></pre>
<p>That <code>getThing</code> call makes sense now. The method name hasn't changed, but the context around it has.</p>
<p>One feature worth using: wrapping identifiers in square brackets creates cross-references in generated docs.</p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Uses [userId] to look up preferences.</span></span>
<span class="hljs-comment">/// <span class="markdown">Falls back if [fallbackToDefault] is true.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// See also: [UserPreferencesService]</span></span>
</code></pre>
<p>When rendered, <code>[userId]</code> becomes a link to its definition. It's a small touch that pays off when navigating a larger codebase.</p>
<hr />
<h2 id="heading-what-to-document-and-what-to-skip">What to Document and What to Skip</h2>
<p>The most common mistake developers make with documentation is writing it for the wrong things. Documenting obvious one-liners creates noise. Not documenting complex logic creates confusion.</p>
<p>Document when:</p>
<ul>
<li>The function or class is public and used across multiple files</li>
<li>You used a workaround that isn't immediately obvious from reading the code</li>
<li>The function has non-obvious behavior: side effects, ordering requirements, threading constraints</li>
<li>A parameter name isn't self-explanatory — especially boolean flags</li>
</ul>
<p>Skip documentation when:</p>
<ul>
<li>The code is a simple one-liner with a descriptive name</li>
<li>The function is private and used in only one place</li>
<li>The code is temporary or about to be deleted</li>
</ul>
<hr />
<h2 id="heading-a-documentation-structure-that-works">A Documentation Structure That Works</h2>
<p>The most useful docs follow a consistent structure: what it does, when to use it, and an example. Three layers, and you can read as much or as little as you need.</p>
<pre><code class="lang-dart"><span class="hljs-comment">/// <span class="markdown">Shows a profile card with an optional action button.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// Used on the user dashboard to highlight key account information.</span></span>
<span class="hljs-comment">/// <span class="markdown">If [onActionPressed] is null, the button is hidden automatically.</span></span>
<span class="hljs-comment">///
<span class="markdown">/// Example:</span></span>
<span class="hljs-comment">/// <span class="markdown"><span class="hljs-code">```dart</span></span></span>
<span class="hljs-comment">/// <span class="markdown"><span class="hljs-code">ProfileCard(</span></span></span>
<span class="hljs-comment">///   <span class="markdown"><span class="hljs-code">user: currentUser,</span></span></span>
<span class="hljs-comment">///   <span class="markdown"><span class="hljs-code">onActionPressed: () =&gt; navigateToSettings(),</span></span></span>
<span class="hljs-comment">/// <span class="markdown"><span class="hljs-code">)</span></span></span>
<span class="hljs-comment">///</span>
</code></pre>
<p>class ProfileCard extends StatelessWidget {
  final User user;
  final VoidCallback? onActionPressed;</p>
<p>  // ...
}
```</p>
<p>The first line answers "what does this do?" The middle lines answer "when and why would I use this?" The example shows actual usage. Someone new to the codebase can read just the first line and move on, or dig into the details if they need them.</p>
<hr />
<h2 id="heading-useful-things-most-developers-dont-know-about-dart-docs">Useful Things Most Developers Don't Know About Dart Docs</h2>
<p><strong>Markdown works inside <code>///</code></strong>. You can use <code>*italic*</code>, <code>**bold**</code>, bullet lists, and fenced code blocks. Docs don't have to be plain text.</p>
<p><strong>You can reference parameters inline.</strong> <code>[paramName]</code> in brackets links to the parameter definition in IDEs that support it.</p>
<p><strong><code>See also:</code> sections are useful.</strong> If your method is closely related to another method or class, noting that explicitly saves readers from having to search.</p>
<p><strong><code>/// {@macro myMacro}</code></strong> lets you reuse documentation across similar widgets or functions without copying and pasting. Useful for widget families that share common behavior.</p>
<p><strong>The return value deserves a line.</strong> If a function returns something non-obvious — a nullable type, an empty list on failure, a transformed version of the input — say so explicitly.</p>
<hr />
<h2 id="heading-the-practical-test">The Practical Test</h2>
<p>Ask a teammate to explain a method you wrote three months ago, without looking at anything other than the call site. If they can't do it in thirty seconds, you needed documentation.</p>
<p>This isn't about being thorough for the sake of it. It's about the fact that the person who will most benefit from your documentation is often you, six months later, at 11pm, debugging something that should have been obvious.</p>
<hr />
<h2 id="heading-a-simple-daily-habit">A Simple Daily Habit</h2>
<p>You don't need to document your entire codebase in one sitting. That's overwhelming and it won't happen.</p>
<p>A more realistic approach: pick one undocumented function each day. Write three lines about what it does, when to use it, and any edge cases worth knowing. After a month, you'll have documented thirty functions, and the habit will feel automatic.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Use <code>///</code> for Dart doc comments. Wrap identifiers in square brackets to create cross-references. Structure your docs in three layers: what it does, when to use it, and an example. Return value behavior and non-obvious side effects deserve explicit mention. And write docs like you're writing a note to yourself on a bad day — because at some point, that's exactly what they'll be.</p>
<p>The real magic was never in <code>doMagic()</code>. It was knowing what <code>doMagic()</code> was supposed to do.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Camera Tap to Focus: Implement DSLR-Style Focus Control with GestureDetector]]></title><description><![CDATA[The camera preview was live. The feature request was simple: tap anywhere on the screen and focus the camera there. Like every modern phone camera does when you tap a face or a subject before shooting.
I tapped. Nothing. The camera stayed focused whe...]]></description><link>https://blog.anmolthedeveloper.com/tap-to-focus-like-a-pro-turning-your-camera-app-into-a-dslr-with-flutter</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/tap-to-focus-like-a-pro-turning-your-camera-app-into-a-dslr-with-flutter</guid><category><![CDATA[Android]]></category><category><![CDATA[camera]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Examples]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 10 Apr 2025 07:08:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746600154298/191b6cc2-4663-42a2-b758-106668e8f51e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The camera preview was live. The feature request was simple: tap anywhere on the screen and focus the camera there. Like every modern phone camera does when you tap a face or a subject before shooting.</p>
<p>I tapped. Nothing. The camera stayed focused wherever it felt like. I tapped again. Same blur, same indifference.</p>
<p>That's when I went digging into the <code>camera</code> package to figure out why, and how to actually wire this up properly.</p>
<hr />
<h2 id="heading-what-needs-to-happen">What Needs to Happen</h2>
<p>Tap-to-focus requires three things in sequence:</p>
<ol>
<li>Capture where the user tapped on screen</li>
<li>Convert that screen position into normalized coordinates the camera understands</li>
<li>Tell the camera controller to focus at that point</li>
</ol>
<p>Each step is straightforward on its own. The tricky part is understanding why each one is necessary.</p>
<hr />
<h2 id="heading-the-full-implementation">The Full Implementation</h2>
<pre><code class="lang-dart">GestureDetector(
  onTapDown: (TapDownDetails details) {
    <span class="hljs-keyword">final</span> RenderBox box = context.findRenderObject() <span class="hljs-keyword">as</span> RenderBox;
    <span class="hljs-keyword">final</span> Offset localPosition = box.globalToLocal(details.globalPosition);

    <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> dx = localPosition.dx / box.size.width;
    <span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> dy = localPosition.dy / box.size.height;

    _controller.setFocusPoint(Offset(dx, dy));
    _controller.setFocusMode(FocusMode.auto);
  },
  child: AspectRatio(
    aspectRatio: <span class="hljs-number">3</span> / <span class="hljs-number">4</span>,
    child: CameraPreview(_controller),
  ),
),
</code></pre>
<p>Let's walk through what each part is doing.</p>
<hr />
<h2 id="heading-why-ontapdown-instead-of-ontap">Why onTapDown Instead of onTap</h2>
<p><code>onTap</code> fires after the finger lifts, and it doesn't include position data in a form that's easy to work with for this use case. <code>onTapDown</code> fires the moment the finger touches the screen and gives you a <code>TapDownDetails</code> object containing <code>globalPosition</code> — the exact screen coordinates of the touch. For camera focus, you want the position immediately, so <code>onTapDown</code> is the right handler.</p>
<hr />
<h2 id="heading-getting-the-widgets-position-with-renderbox">Getting the Widget's Position with RenderBox</h2>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> RenderBox box = context.findRenderObject() <span class="hljs-keyword">as</span> RenderBox;
</code></pre>
<p>The touch position from <code>onTapDown</code> is in global screen coordinates — relative to the top-left corner of the entire screen. But the camera preview widget probably doesn't start at (0, 0) on screen. There's an app bar, padding, or other widgets above it.</p>
<p><code>RenderBox</code> knows exactly where the widget sits on screen and how large it is. You need it to convert the global position into a position that's local to the camera preview widget.</p>
<hr />
<h2 id="heading-converting-global-to-local-coordinates">Converting Global to Local Coordinates</h2>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> Offset localPosition = box.globalToLocal(details.globalPosition);
</code></pre>
<p><code>globalToLocal</code> translates the tap position from screen space into the widget's own coordinate space. After this, <code>localPosition</code> tells you where the user tapped relative to the top-left corner of the camera preview — not the screen.</p>
<hr />
<h2 id="heading-normalizing-the-coordinates">Normalizing the Coordinates</h2>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> dx = localPosition.dx / box.size.width;
<span class="hljs-keyword">final</span> <span class="hljs-built_in">double</span> dy = localPosition.dy / box.size.height;
</code></pre>
<p>The camera's <code>setFocusPoint</code> method expects normalized coordinates — values between 0.0 and 1.0 — where (0, 0) is the top-left and (1, 1) is the bottom-right of the preview. Dividing by the widget's width and height converts pixel positions into that normalized format.</p>
<p>This is the step developers most often skip or get wrong. If you pass raw pixel coordinates to <code>setFocusPoint</code>, the camera either ignores them or focuses in the wrong place entirely.</p>
<hr />
<h2 id="heading-telling-the-camera-to-focus">Telling the Camera to Focus</h2>
<pre><code class="lang-dart">_controller.setFocusPoint(Offset(dx, dy));
_controller.setFocusMode(FocusMode.auto);
</code></pre>
<p><code>setFocusPoint</code> sets where the camera should focus. <code>setFocusMode(FocusMode.auto)</code> tells the camera to actually trigger autofocus at that point. You need both — the point alone doesn't trigger a focus action, and setting the mode without a point falls back to whatever default behavior the camera had before.</p>
<hr />
<h2 id="heading-adding-a-visual-focus-indicator">Adding a Visual Focus Indicator</h2>
<p>Functional tap-to-focus is good. Tap-to-focus with a visible focus ring is noticeably better — it tells the user their tap was registered and shows exactly where the camera is focusing.</p>
<p>Here's a simple implementation using an overlay:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CameraWithFocus</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> CameraController controller;
  <span class="hljs-keyword">const</span> CameraWithFocus({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.controller});

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_CameraWithFocusState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">CameraWithFocus</span>&gt; </span>{
  Offset? _focusPoint;
  <span class="hljs-built_in">bool</span> _showFocusRing = <span class="hljs-keyword">false</span>;

  Future&lt;<span class="hljs-keyword">void</span>&gt; _onTapDown(TapDownDetails details) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> box = context.findRenderObject() <span class="hljs-keyword">as</span> RenderBox;
    <span class="hljs-keyword">final</span> local = box.globalToLocal(details.globalPosition);
    <span class="hljs-keyword">final</span> dx = local.dx / box.size.width;
    <span class="hljs-keyword">final</span> dy = local.dy / box.size.height;

    <span class="hljs-keyword">await</span> widget.controller.setFocusPoint(Offset(dx, dy));
    <span class="hljs-keyword">await</span> widget.controller.setFocusMode(FocusMode.auto);

    setState(() {
      _focusPoint = local;
      _showFocusRing = <span class="hljs-keyword">true</span>;
    });

    Future.delayed(<span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">1</span>), () {
      <span class="hljs-keyword">if</span> (mounted) setState(() =&gt; _showFocusRing = <span class="hljs-keyword">false</span>);
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> GestureDetector(
      onTapDown: _onTapDown,
      child: Stack(
        children: [
          AspectRatio(
            aspectRatio: <span class="hljs-number">3</span> / <span class="hljs-number">4</span>,
            child: CameraPreview(widget.controller),
          ),
          <span class="hljs-keyword">if</span> (_showFocusRing &amp;&amp; _focusPoint != <span class="hljs-keyword">null</span>)
            Positioned(
              left: _focusPoint!.dx - <span class="hljs-number">30</span>,
              top: _focusPoint!.dy - <span class="hljs-number">30</span>,
              child: Container(
                width: <span class="hljs-number">60</span>,
                height: <span class="hljs-number">60</span>,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.yellowAccent, width: <span class="hljs-number">2</span>),
                  borderRadius: BorderRadius.circular(<span class="hljs-number">4</span>),
                ),
              ),
            ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The ring appears at the tap location and disappears after a second. The yellow border on a slightly rounded rectangle is a common camera UI pattern — familiar to users from the default phone camera app.</p>
<hr />
<h2 id="heading-one-thing-worth-knowing-about-platform-behavior">One Thing Worth Knowing About Platform Behavior</h2>
<p><code>setFocusPoint</code> and <code>setFocusMode</code> are async operations that communicate with the native camera layer. On Android, focus response is generally fast. On iOS, there can occasionally be a small delay. Wrapping both calls in <code>await</code> is the correct approach — don't fire-and-forget them, especially if you're also updating UI state based on the result.</p>
<p>Also, not all camera hardware supports manual focus point selection. On devices where it isn't supported, the <code>camera</code> package may throw a <code>CameraException</code>. Wrapping the focus calls in a try-catch is worth doing in production code.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>The implementation is compact, but each line has a specific reason for being there. <code>onTapDown</code> gets the position immediately. <code>RenderBox</code> translates it from screen space to widget space. Dividing by width and height normalizes it for the camera API. <code>setFocusPoint</code> and <code>setFocusMode</code> apply it. Add a visual indicator on top and the feature feels complete — not just functional.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter and Android Lifecycle Explained: initState, didChangeAppLifecycleState and More]]></title><description><![CDATA[If you've come to Flutter from native Android, one of the first things you'll notice is that the lifecycle methods you relied on — onCreate, onResume, onPause, onStop, onDestroy — don't exist in the same form. Flutter manages lifecycle differently, a...]]></description><link>https://blog.anmolthedeveloper.com/flutter-and-android-lifecycle-explained-initstate-didchangeapplifecyclestate-and-more</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-and-android-lifecycle-explained-initstate-didchangeapplifecyclestate-and-more</guid><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[App Lifecycle]]></category><category><![CDATA[Dart]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[lifecycle]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 27 Feb 2025 07:35:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740641640906/424a2e48-0cf6-4c3b-936b-75b2e2af4b12.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've come to Flutter from native Android, one of the first things you'll notice is that the lifecycle methods you relied on — <code>onCreate</code>, <code>onResume</code>, <code>onPause</code>, <code>onStop</code>, <code>onDestroy</code> — don't exist in the same form. Flutter manages lifecycle differently, and understanding the mapping between the two systems is essential for building apps that handle background/foreground transitions, resource cleanup, and state restoration correctly.</p>
<hr />
<h2 id="heading-androids-activity-lifecycle">Android's Activity Lifecycle</h2>
<p>Android apps are built around the <code>Activity</code> class, and the OS communicates state changes through a set of well-defined callbacks:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>When it fires</td></tr>
</thead>
<tbody>
<tr>
<td><code>onCreate()</code></td><td>Activity is first created</td></tr>
<tr>
<td><code>onStart()</code></td><td>Activity becomes visible</td></tr>
<tr>
<td><code>onResume()</code></td><td>User can interact with the activity</td></tr>
<tr>
<td><code>onPause()</code></td><td>Activity is partially obscured or losing focus</td></tr>
<tr>
<td><code>onStop()</code></td><td>Activity is no longer visible</td></tr>
<tr>
<td><code>onDestroy()</code></td><td>Activity is being destroyed</td></tr>
</tbody>
</table>
</div><p>These fire in sequence as the user navigates into and out of the activity. The most commonly used pair is <code>onResume</code> and <code>onPause</code> — when the app comes to the foreground and when it's about to leave it.</p>
<hr />
<h2 id="heading-flutters-widget-lifecycle">Flutter's Widget Lifecycle</h2>
<p>Flutter replaces the activity-centric model with a widget-centric one. A <code>StatefulWidget</code> goes through its own lifecycle:</p>
<p><strong><code>initState()</code></strong> — called once when the widget is inserted into the tree. This is the equivalent of <code>onCreate</code>. Initialize controllers, set up listeners, and fetch initial data here.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  _controller = AnimationController(vsync: <span class="hljs-keyword">this</span>);
  _loadData();
}
</code></pre>
<p><strong><code>didChangeDependencies()</code></strong> — called immediately after <code>initState</code> and again whenever the widget's inherited dependencies change. Use this when setup depends on <code>InheritedWidget</code> values like <code>Theme</code> or <code>MediaQuery</code>.</p>
<p><strong><code>build()</code></strong> — called whenever Flutter needs to render the widget. This can be called many times, so keep it free of side effects.</p>
<p><strong><code>didUpdateWidget()</code></strong> — called when the parent rebuilds and passes new configuration to this widget. Useful for reacting to external prop changes.</p>
<p><strong><code>dispose()</code></strong> — called when the widget is permanently removed from the tree. This is where you release resources — cancel timers, close streams, dispose controllers. The equivalent of <code>onDestroy</code>.</p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> dispose() {
  _controller.dispose();
  _subscription.cancel();
  <span class="hljs-keyword">super</span>.dispose();
}
</code></pre>
<hr />
<h2 id="heading-observing-app-level-lifecycle-in-flutter">Observing App-Level Lifecycle in Flutter</h2>
<p>Widget lifecycle handles what happens to a specific screen. But for app-level events — the user pressing home, switching apps, receiving a phone call — Flutter uses <code>WidgetsBindingObserver</code>.</p>
<p>Add the mixin to your <code>StatefulWidget</code>'s state class, register it in <code>initState</code>, and override <code>didChangeAppLifecycleState</code>:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; <span class="hljs-title">with</span> <span class="hljs-title">WidgetsBindingObserver</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    WidgetsBinding.instance.addObserver(<span class="hljs-keyword">this</span>);
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    WidgetsBinding.instance.removeObserver(<span class="hljs-keyword">this</span>);
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didChangeAppLifecycleState(AppLifecycleState state) {
    <span class="hljs-keyword">switch</span> (state) {
      <span class="hljs-keyword">case</span> AppLifecycleState.resumed:
        <span class="hljs-comment">// App is in foreground and interactive</span>
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> AppLifecycleState.inactive:
        <span class="hljs-comment">// App is losing focus (e.g., phone call, app switcher)</span>
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> AppLifecycleState.paused:
        <span class="hljs-comment">// App is in background and not visible</span>
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> AppLifecycleState.detached:
        <span class="hljs-comment">// Engine is running but no views attached</span>
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> AppLifecycleState.hidden:
        <span class="hljs-comment">// All views are hidden (added in newer Flutter versions)</span>
        <span class="hljs-keyword">break</span>;
    }
  }
}
</code></pre>
<p>Always call <code>removeObserver</code> in <code>dispose</code>. Failing to do so causes memory leaks — the observer will continue receiving callbacks even after the widget is gone.</p>
<hr />
<h2 id="heading-mapping-android-to-flutter">Mapping Android to Flutter</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Android</td><td>Flutter equivalent</td><td>When</td></tr>
</thead>
<tbody>
<tr>
<td><code>onCreate</code></td><td><code>initState</code></td><td>Widget first created</td></tr>
<tr>
<td><code>onStart</code> / <code>onResume</code></td><td><code>AppLifecycleState.resumed</code></td><td>App comes to foreground</td></tr>
<tr>
<td><code>onPause</code></td><td><code>AppLifecycleState.inactive</code></td><td>App losing focus</td></tr>
<tr>
<td><code>onStop</code></td><td><code>AppLifecycleState.paused</code></td><td>App goes to background</td></tr>
<tr>
<td><code>onDestroy</code></td><td><code>dispose</code></td><td>Widget permanently removed</td></tr>
</tbody>
</table>
</div><p>One important nuance: Flutter's state names don't map one-to-one to Android's names. When Android calls <code>Activity.onPause</code>, Flutter enters <code>inactive</code> — not <code>paused</code>. When Android calls <code>Activity.onStop</code>, Flutter enters <code>paused</code>. This mismatch trips up developers coming from native Android, so it's worth keeping this table close.</p>
<hr />
<h2 id="heading-practical-use-cases">Practical Use Cases</h2>
<p><strong>Pause video playback when the app goes to the background:</strong></p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> didChangeAppLifecycleState(AppLifecycleState state) {
  <span class="hljs-keyword">if</span> (state == AppLifecycleState.paused) {
    _videoController.pause();
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (state == AppLifecycleState.resumed) {
    _videoController.play();
  }
}
</code></pre>
<p><strong>Release the camera when the app is not visible:</strong></p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> didChangeAppLifecycleState(AppLifecycleState state) {
  <span class="hljs-keyword">if</span> (state == AppLifecycleState.inactive) {
    _cameraController.dispose();
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (state == AppLifecycleState.resumed) {
    _initCamera();
  }
}
</code></pre>
<p><strong>Refresh stale data when returning to the foreground:</strong></p>
<pre><code class="lang-dart"><span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> didChangeAppLifecycleState(AppLifecycleState state) {
  <span class="hljs-keyword">if</span> (state == AppLifecycleState.resumed) {
    _refreshUserSession();
  }
}
</code></pre>
<hr />
<h2 id="heading-using-the-newer-applifecyclelistener">Using the Newer AppLifecycleListener</h2>
<p>Flutter also provides <code>AppLifecycleListener</code>, a newer class that offers named callbacks for each transition rather than a single switch statement. For new code, this tends to be cleaner:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AppLifecycleListener _listener;

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  _listener = AppLifecycleListener(
    onResume: () =&gt; _onResumed(),
    onPause: () =&gt; _onPaused(),
    onInactive: () =&gt; _onInactive(),
    onDetach: () =&gt; _onDetached(),
  );
}

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> dispose() {
  _listener.dispose();
  <span class="hljs-keyword">super</span>.dispose();
}
</code></pre>
<p>The behavior is the same as <code>WidgetsBindingObserver</code>, but the API is more explicit and easier to read at a glance.</p>
<hr />
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p><strong>Forgetting to remove the observer.</strong> If you add an observer in <code>initState</code> but don't remove it in <code>dispose</code>, the widget keeps receiving callbacks after it's been removed from the tree. This can cause exceptions or unexpected behavior.</p>
<p><strong>Doing heavy work in <code>build</code>.</strong> Unlike <code>onCreate</code>, <code>build</code> can be called many times. Database queries, network calls, and expensive computations don't belong there — they belong in <code>initState</code> or triggered by state changes.</p>
<p><strong>Treating <code>inactive</code> like <code>paused</code>.</strong> <code>inactive</code> fires for brief interruptions — phone calls, the notification shade, the app switcher — where the app is still partially visible. <code>paused</code> is when the app has genuinely moved to the background. The right action (pause media vs. release camera) depends on which state you're actually in.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Flutter's lifecycle is widget-first rather than activity-first, but all the same events exist — they're just expressed differently. <code>initState</code> and <code>dispose</code> bookend a widget's life. <code>WidgetsBindingObserver</code> and <code>AppLifecycleListener</code> handle app-level foreground/background transitions. Keep the Android-to-Flutter mapping in mind, always clean up in <code>dispose</code>, and use the named callbacks in <code>AppLifecycleListener</code> for clarity in new code.</p>
]]></content:encoded></item><item><title><![CDATA[Android App Not Installed Error: Causes, Fixes and ADB Debugging Guide]]></title><description><![CDATA[It was late. A developer had just finished building a new feature, transferred the APK to their device, and tapped Install. Instead of the success screen, they got: "App not installed."
They tried again. Same result. No explanation, no error code — j...]]></description><link>https://blog.anmolthedeveloper.com/the-mysterious-case-of-the-missing-app-solving-the-app-not-installed-error</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/the-mysterious-case-of-the-missing-app-solving-the-app-not-installed-error</guid><category><![CDATA[adb]]></category><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[apk]]></category><category><![CDATA[debugging]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Wed, 26 Feb 2025 07:56:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740556518298/d27165b3-8826-44bc-9747-7d6255276417.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It was late. A developer had just finished building a new feature, transferred the APK to their device, and tapped Install. Instead of the success screen, they got: "App not installed."</p>
<p>They tried again. Same result. No explanation, no error code — just a flat refusal from the OS.</p>
<p>This is one of Android's more frustrating error messages because it's generic. It doesn't tell you what went wrong. But the actual causes are predictable, and once you know them, the fix is usually quick.</p>
<hr />
<h2 id="heading-the-most-common-cause-signing-conflict">The Most Common Cause: Signing Conflict</h2>
<p>If any version of your app is already installed on the device — even a debug build — and the APK you're trying to install is signed with a different certificate, Android will refuse the installation entirely.</p>
<p>Android uses signing certificates to verify that updates to an app come from the same source as the original install. A mismatched certificate looks like a completely different app trying to take over the same package name. Android blocks it.</p>
<p>To fix it, uninstall the existing version first:</p>
<pre><code class="lang-bash">adb uninstall com.yourpackage.name
</code></pre>
<p>If you have multiple devices connected, specify which one:</p>
<pre><code class="lang-bash">adb -s &lt;device_id&gt; uninstall com.yourpackage.name
</code></pre>
<p>Then reinstall:</p>
<pre><code class="lang-bash">adb install path/to/your-app.apk
</code></pre>
<p>This situation comes up often when switching between debug and release builds, when moving between machines (since Android Studio generates a debug keystore per machine), or when a team member builds the APK using a different signing config.</p>
<p>Going forward, the cleanest habit is to uninstall before installing when you know the signing config has changed.</p>
<hr />
<h2 id="heading-checking-which-devices-are-connected">Checking Which Devices Are Connected</h2>
<p>If you're not sure what device IDs are available:</p>
<pre><code class="lang-bash">adb devices
</code></pre>
<p>This lists all connected devices and emulators. Use the ID from the output in the <code>-s</code> flag.</p>
<hr />
<h2 id="heading-version-code-downgrade">Version Code Downgrade</h2>
<p>Android won't install an APK whose <code>versionCode</code> is lower than the currently installed version. This is a deliberate restriction — the OS prevents rolling back to older versions without an explicit uninstall.</p>
<p>If you're testing an older build or an earlier branch:</p>
<pre><code class="lang-bash">adb install -d path/to/your-app.apk
</code></pre>
<p>The <code>-d</code> flag allows downgrades. Without it, the install will fail silently if the version code is lower.</p>
<hr />
<h2 id="heading-apk-corruption-or-incomplete-transfer">APK Corruption or Incomplete Transfer</h2>
<p>If the APK file was partially downloaded, copied over an unstable connection, or corrupted during transfer, the package installer will reject it.</p>
<p>Check the file size — compare it against the original build output. If they differ, rebuild or re-download and try again.</p>
<p>You can also verify the APK's signature is intact before installing:</p>
<pre><code class="lang-bash">apksigner verify path/to/your-app.apk
</code></pre>
<p>If the APK is unsigned or the signature is broken, this will tell you.</p>
<hr />
<h2 id="heading-insufficient-storage">Insufficient Storage</h2>
<p>The device needs enough free space to unpack and install the APK. This is rarer than the other causes, but it still happens, especially on older or lower-end devices. Check available storage in Settings and clear space if needed.</p>
<hr />
<h2 id="heading-unknown-sources-not-enabled">"Unknown Sources" Not Enabled</h2>
<p>When installing APKs outside of the Play Store, Android requires explicit permission to install from unknown sources. On Android 8 and above, this permission is per-app (granted to the specific app doing the installing, like a file manager or browser). On older versions, it's a global toggle.</p>
<p>If the install fails and the device prompts about unknown sources, grant the permission to the relevant app and try again.</p>
<hr />
<h2 id="heading-getting-more-detail-with-adb">Getting More Detail with ADB</h2>
<p>The on-device error message gives you nothing useful. The ADB output gives you a specific error code:</p>
<pre><code class="lang-bash">adb install -r path/to/your-app.apk
</code></pre>
<p>The <code>-r</code> flag reinstalls while keeping app data. The output will include codes like <code>INSTALL_FAILED_UPDATE_INCOMPATIBLE</code> (signing mismatch), <code>INSTALL_FAILED_VERSION_DOWNGRADE</code> (version code too low), or <code>INSTALL_FAILED_INSUFFICIENT_STORAGE</code> (no space). These codes are specific enough to point directly to the fix.</p>
<hr />
<h2 id="heading-checking-if-the-app-is-already-installed">Checking If the App Is Already Installed</h2>
<p>Sometimes the app is installed under a secondary user profile or work profile and isn't visible in the main settings, but still causes a conflict. You can check:</p>
<pre><code class="lang-bash">adb shell pm list packages | grep com.yourpackage.name
</code></pre>
<p>If it shows up, it's installed somewhere on the device. Uninstall for all users:</p>
<pre><code class="lang-bash">adb shell pm uninstall --user 0 com.yourpackage.name
</code></pre>
<p>Replace <code>0</code> with the appropriate user ID if needed.</p>
<hr />
<h2 id="heading-a-useful-workflow-for-development">A Useful Workflow for Development</h2>
<p>When testing during development, using <code>adb install -r</code> keeps the app's data intact while replacing the binary. For situations where signing has changed, always uninstall first. And when you can't figure out why an install is failing, run it via ADB and read the error code — it's almost always more useful than anything the device UI shows you.</p>
<hr />
<h2 id="heading-quick-reference">Quick Reference</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Cause</td><td>Fix</td></tr>
</thead>
<tbody>
<tr>
<td>Signing mismatch</td><td>Uninstall existing app, reinstall</td></tr>
<tr>
<td>Lower version code</td><td>Use <code>adb install -d</code> to allow downgrade</td></tr>
<tr>
<td>Corrupted APK</td><td>Rebuild or re-download, verify with <code>apksigner</code></td></tr>
<tr>
<td>No storage space</td><td>Free up space on device</td></tr>
<tr>
<td>Unknown sources blocked</td><td>Grant install permission to the relevant app</td></tr>
<tr>
<td>Unknown cause</td><td>Run <code>adb install</code> and read the error code</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[How to Detect Fake GPS and Mock Location in Android Apps: A Developer's Security Guide]]></title><description><![CDATA[Android's developer options include a feature called "Allow mock locations," which lets any designated app feed fake GPS coordinates to the entire device. For developers, this is useful — you can simulate a delivery route, test geofenced notification...]]></description><link>https://blog.anmolthedeveloper.com/how-to-detect-fake-gps-and-mock-location-in-android-apps-a-developers-security-guide</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/how-to-detect-fake-gps-and-mock-location-in-android-apps-a-developers-security-guide</guid><category><![CDATA[mock-location]]></category><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[android-security]]></category><category><![CDATA[fraud prevention]]></category><category><![CDATA[gps]]></category><category><![CDATA[location ]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Wed, 26 Feb 2025 07:28:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740554595968/885f7e71-80fe-4d60-bf13-7ee8c918fec4.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Android's developer options include a feature called "Allow mock locations," which lets any designated app feed fake GPS coordinates to the entire device. For developers, this is useful — you can simulate a delivery route, test geofenced notifications, or verify location-based logic without physically moving anywhere.</p>
<p>But the same mechanism can be misused. In apps that depend on real-world location data — delivery platforms, attendance systems, ride-sharing, banking — a user can install a GPS spoofing app, set a fake location, and your app will receive that fake data as if it were real. The device has no way to automatically tell the difference unless you check for it explicitly.</p>
<p>This article covers how to detect mock location at the app level in Android, using both modern and legacy approaches.</p>
<hr />
<h2 id="heading-how-mock-location-works">How Mock Location Works</h2>
<p>On Android 6.0 and above, mock location isn't a global setting — it's granted to a specific app. The user goes to Developer Options, selects "Select mock location app," and chooses a spoofing app from the list. Once set, that app can feed arbitrary coordinates to <code>LocationManager</code> and <code>FusedLocationProviderClient</code>.</p>
<p>On older Android versions (5.1 and below), it was a simpler global toggle in Developer Options.</p>
<hr />
<h2 id="heading-detecting-mock-location">Detecting Mock Location</h2>
<p>The most reliable approach is to check the <code>Location</code> object's mock flag before using the coordinates.</p>
<p><strong>For Android 12 and above (API level 31+):</strong></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> locationManager = getSystemService(Context.LOCATION_SERVICE) <span class="hljs-keyword">as</span> LocationManager
<span class="hljs-keyword">val</span> locationListener = <span class="hljs-keyword">object</span> : LocationListener {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onLocationChanged</span><span class="hljs-params">(location: <span class="hljs-type">Location</span>)</span></span> {
        <span class="hljs-keyword">if</span> (location.isMock) {
            <span class="hljs-comment">// Location is from a mock provider — reject or flag it</span>
            handleMockLocation()
        } <span class="hljs-keyword">else</span> {
            processRealLocation(location)
        }
    }
}

locationManager.requestLocationUpdates(
    LocationManager.GPS_PROVIDER,
    <span class="hljs-number">1000L</span>,
    <span class="hljs-number">0f</span>,
    locationListener
)
</code></pre>
<p><code>location.isMock</code> is the modern, direct way to check this. It returns <code>true</code> if the location was provided by a mock provider.</p>
<p><strong>For Android below API 31:</strong></p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Suppress(<span class="hljs-meta-string">"DEPRECATION"</span>)</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isMockLocation</span><span class="hljs-params">(location: <span class="hljs-type">Location</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
        location.isMock
    } <span class="hljs-keyword">else</span> {
        location.isFromMockProvider
    }
}
</code></pre>
<p><code>isFromMockProvider</code> is deprecated as of API 31 but still works on older devices. Using the version check ensures correct behavior across the full Android version range.</p>
<hr />
<h2 id="heading-using-fused-location-provider">Using Fused Location Provider</h2>
<p>If you're using <code>FusedLocationProviderClient</code> (which is the recommended approach for most apps), the same check applies to the <code>Location</code> objects you receive in your callback:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> fusedLocationClient = LocationServices.getFusedLocationProviderClient(<span class="hljs-keyword">this</span>)

<span class="hljs-keyword">val</span> locationRequest = LocationRequest.Builder(
    Priority.PRIORITY_HIGH_ACCURACY,
    <span class="hljs-number">1000L</span>
).build()

<span class="hljs-keyword">val</span> locationCallback = <span class="hljs-keyword">object</span> : LocationCallback() {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onLocationResult</span><span class="hljs-params">(result: <span class="hljs-type">LocationResult</span>)</span></span> {
        <span class="hljs-keyword">for</span> (location <span class="hljs-keyword">in</span> result.locations) {
            <span class="hljs-keyword">if</span> (isMockLocation(location)) {
                <span class="hljs-comment">// Handle mock location</span>
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-comment">// Use real location</span>
            }
        }
    }
}

fusedLocationClient.requestLocationUpdates(
    locationRequest,
    locationCallback,
    Looper.getMainLooper()
)
</code></pre>
<hr />
<h2 id="heading-checking-if-mock-location-is-enabled-at-all">Checking If Mock Location Is Enabled at All</h2>
<p>For stricter applications, you may want to reject any session where the device even has mock locations enabled — regardless of whether the current location reading is mocked. You can check this at startup:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isMockLocationEnabled</span><span class="hljs-params">(context: <span class="hljs-type">Context</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
        <span class="hljs-keyword">val</span> locationManager = context.getSystemService(Context.LOCATION_SERVICE) <span class="hljs-keyword">as</span> LocationManager
        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &amp;&amp;
            Settings.Secure.getInt(
                context.contentResolver,
                Settings.Secure.ALLOW_MOCK_LOCATION, <span class="hljs-number">0</span>
            ) != <span class="hljs-number">0</span>
    } <span class="hljs-keyword">else</span> {
        Settings.Secure.getInt(
            context.contentResolver,
            Settings.Secure.ALLOW_MOCK_LOCATION, <span class="hljs-number">0</span>
        ) != <span class="hljs-number">0</span>
    }
}
</code></pre>
<p>Note: On API 23 and above, <code>ALLOW_MOCK_LOCATION</code> in <code>Settings.Secure</code> is always 0 because mock location is no longer a global toggle — it's per-app. For API 23+, the per-location <code>isMock</code> check is more relevant.</p>
<hr />
<h2 id="heading-detecting-whether-a-mock-provider-app-is-active">Detecting Whether a Mock Provider App Is Active</h2>
<p>On API 23 and above, you can check whether any app is currently registered as the mock location provider:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">hasMockLocationApp</span><span class="hljs-params">(context: <span class="hljs-type">Context</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.M) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    <span class="hljs-keyword">val</span> locationManager = context.getSystemService(Context.LOCATION_SERVICE) <span class="hljs-keyword">as</span> LocationManager
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">try</span> {
        locationManager.addTestProvider(
            LocationManager.GPS_PROVIDER,
            <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">false</span>,
            Criteria.POWER_LOW, Criteria.ACCURACY_FINE
        )
        locationManager.removeTestProvider(LocationManager.GPS_PROVIDER)
        <span class="hljs-literal">false</span> <span class="hljs-comment">// No mock provider was active; we were able to add one</span>
    } <span class="hljs-keyword">catch</span> (e: SecurityException) {
        <span class="hljs-literal">true</span> <span class="hljs-comment">// A mock provider is already registered — another app owns it</span>
    }
}
</code></pre>
<p>This works by trying to add a test provider. If another app has already claimed the mock provider slot, Android throws a <code>SecurityException</code>. That exception tells you a mock location app is active.</p>
<hr />
<h2 id="heading-what-to-do-when-mock-location-is-detected">What to Do When Mock Location Is Detected</h2>
<p>The right response depends on your app's use case.</p>
<p>For a delivery or attendance app where fake location directly enables fraud, you might block the action entirely and show an error:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">if</span> (isMockLocation(location)) {
    showError(<span class="hljs-string">"Your location appears to be mocked. Please disable any GPS spoofing apps and try again."</span>)
    <span class="hljs-keyword">return</span>
}
</code></pre>
<p>For lower-stakes applications, you might log the detection server-side for review without blocking the user in the moment:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">if</span> (isMockLocation(location)) {
    reportSuspiciousActivity(userId, location)
}
processLocation(location)
</code></pre>
<p>Avoid silently doing nothing. If you're checking for mock location, you should have a plan for what happens when you find it.</p>
<hr />
<h2 id="heading-limitations">Limitations</h2>
<p>App-level detection has limits. Rooted devices can use tools like Xposed or Magisk modules to hide mock location flags entirely, making <code>isMock</code> return <code>false</code> even for spoofed coordinates. For applications where location integrity is critical, combine app-level checks with server-side validation — look for impossible speed (jumping between distant locations in seconds), trajectory anomalies, or coordinates that don't match cell tower or Wi-Fi positioning data.</p>
<p>No single check is foolproof. Defense in depth — multiple signals checked at different layers — gives you the most reliable protection.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p><code>location.isMock</code> (API 31+) and <code>location.isFromMockProvider</code> (pre-API 31) are the primary tools for detecting fake GPS at the app level. Check them on every location update before using the coordinates. For stricter security requirements, also check whether a mock provider app is active at startup. Pair app-level detection with server-side anomaly detection for the strongest coverage.</p>
]]></content:encoded></item><item><title><![CDATA[Android Location Guide: GPS, Wi-Fi, Cell Towers and the Fused Location Provider API]]></title><description><![CDATA[Every Android app that uses location is making a decision it might not even realize it's making: which location provider to use. GPS, network-based location, Wi-Fi, passive — each has different accuracy characteristics, different power costs, and dif...]]></description><link>https://blog.anmolthedeveloper.com/android-location-guide-gps-wi-fi-cell-towers-and-the-fused-location-provider-api</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/android-location-guide-gps-wi-fi-cell-towers-and-the-fused-location-provider-api</guid><category><![CDATA[fused-location-provider]]></category><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[Android tutorial]]></category><category><![CDATA[gps]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[location ]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Tue, 18 Feb 2025 10:50:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739875590660/50763ef9-dc52-4f42-ab78-4f1b3fc310c8.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every Android app that uses location is making a decision it might not even realize it's making: which location provider to use. GPS, network-based location, Wi-Fi, passive — each has different accuracy characteristics, different power costs, and different availability depending on the environment. Most developers reach for <code>FusedLocationProviderClient</code> and move on, but understanding what's happening underneath makes your implementation choices more deliberate and your debugging faster.</p>
<hr />
<h2 id="heading-how-android-determines-location">How Android Determines Location</h2>
<p>Android can derive a device's position from several sources simultaneously:</p>
<p><strong>GPS (Global Positioning System)</strong> — communicates with satellites. High accuracy (within a few meters in open conditions), but slow to acquire a fix, doesn't work well indoors, and draws significant battery power.</p>
<p><strong>Wi-Fi positioning</strong> — estimates location based on the signal strengths of nearby Wi-Fi networks, matched against a database of known networks and their physical locations. Fast, works indoors, moderate accuracy (tens of meters in cities), lower power consumption than GPS.</p>
<p><strong>Cell tower triangulation</strong> — uses signal strengths from nearby cell towers. Fast acquisition, works everywhere with coverage, but accuracy is coarse — anywhere from a few hundred meters to several kilometers depending on tower density.</p>
<p><strong>Sensors</strong> — accelerometers, gyroscopes, and barometers can assist location tracking by filling in gaps between GPS fixes (dead reckoning). Not a standalone source, but used by the system to smooth out position updates.</p>
<p>In practice, Android uses all of these together and blends the results to produce the best estimate it can.</p>
<hr />
<h2 id="heading-the-traditional-approach-locationmanager">The Traditional Approach: LocationManager</h2>
<p><code>LocationManager</code> is the lower-level API that's been in Android since API 1. It gives you direct access to each location provider independently.</p>
<p><strong>GPS provider — most accurate, indoors unreliable:</strong></p>
<pre><code class="lang-java">LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

<span class="hljs-keyword">if</span> (ActivityCompat.checkSelfPermission(<span class="hljs-keyword">this</span>, Manifest.permission.ACCESS_FINE_LOCATION)
    == PackageManager.PERMISSION_GRANTED) {
    locationManager.requestLocationUpdates(
        LocationManager.GPS_PROVIDER,
        <span class="hljs-number">5000</span>,   <span class="hljs-comment">// minimum time between updates (ms)</span>
        <span class="hljs-number">10</span>,     <span class="hljs-comment">// minimum distance between updates (meters)</span>
        locationListener
    );
}
</code></pre>
<p><strong>Network provider — Wi-Fi and cell towers:</strong></p>
<pre><code class="lang-java">locationManager.requestLocationUpdates(
    LocationManager.NETWORK_PROVIDER,
    <span class="hljs-number">5000</span>,
    <span class="hljs-number">10</span>,
    locationListener
);
</code></pre>
<p><strong>Passive provider — piggybacks on other apps' location requests:</strong></p>
<pre><code class="lang-java">locationManager.requestLocationUpdates(
    LocationManager.PASSIVE_PROVIDER,
    <span class="hljs-number">0</span>,
    <span class="hljs-number">0</span>,
    locationListener
);
</code></pre>
<p>The passive provider doesn't trigger any location requests on its own. It receives updates when other apps or the system request them. This is useful for background scenarios where you want location updates without contributing to battery drain yourself.</p>
<p><strong>Listening for updates:</strong></p>
<pre><code class="lang-java">LocationListener locationListener = <span class="hljs-keyword">new</span> LocationListener() {
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onLocationChanged</span><span class="hljs-params">(Location location)</span> </span>{
        <span class="hljs-keyword">double</span> latitude = location.getLatitude();
        <span class="hljs-keyword">double</span> longitude = location.getLongitude();
        <span class="hljs-keyword">float</span> accuracy = location.getAccuracy(); <span class="hljs-comment">// meters</span>
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onProviderEnabled</span><span class="hljs-params">(String provider)</span> </span>{}

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onProviderDisabled</span><span class="hljs-params">(String provider)</span> </span>{}
};
</code></pre>
<p><code>LocationManager</code> gives you granular control, but managing multiple providers simultaneously, handling their different characteristics, and balancing accuracy against battery life is work you have to do yourself.</p>
<hr />
<h2 id="heading-the-modern-approach-fused-location-provider">The Modern Approach: Fused Location Provider</h2>
<p><code>FusedLocationProviderClient</code> is Google's abstraction over the raw providers. It fuses GPS, Wi-Fi, and cellular data internally and gives you a single, battery-efficient stream of location updates. You describe what you need — accuracy level, update frequency — and the system figures out which providers to use and when.</p>
<p><strong>Setup:</strong></p>
<pre><code class="lang-java">FusedLocationProviderClient fusedLocationClient =
    LocationServices.getFusedLocationProviderClient(<span class="hljs-keyword">this</span>);
</code></pre>
<p><strong>Getting the last known location:</strong></p>
<pre><code class="lang-java"><span class="hljs-keyword">if</span> (ActivityCompat.checkSelfPermission(<span class="hljs-keyword">this</span>, Manifest.permission.ACCESS_FINE_LOCATION)
    == PackageManager.PERMISSION_GRANTED) {

    fusedLocationClient.getLastLocation()
        .addOnSuccessListener(<span class="hljs-keyword">this</span>, location -&gt; {
            <span class="hljs-keyword">if</span> (location != <span class="hljs-keyword">null</span>) {
                <span class="hljs-keyword">double</span> lat = location.getLatitude();
                <span class="hljs-keyword">double</span> lon = location.getLongitude();
            }
        });
}
</code></pre>
<p><code>getLastLocation()</code> returns immediately with a cached value — it's fast but can be null if the device hasn't obtained a location recently. For most "show current location" use cases, this is the right starting point.</p>
<p><strong>Requesting continuous updates:</strong></p>
<pre><code class="lang-java">LocationRequest locationRequest = <span class="hljs-keyword">new</span> LocationRequest.Builder(
    Priority.PRIORITY_HIGH_ACCURACY,
    <span class="hljs-number">10000</span>  <span class="hljs-comment">// interval in milliseconds</span>
)
.setMinUpdateIntervalMillis(<span class="hljs-number">5000</span>)
.build();

LocationCallback locationCallback = <span class="hljs-keyword">new</span> LocationCallback() {
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onLocationResult</span><span class="hljs-params">(LocationResult locationResult)</span> </span>{
        <span class="hljs-keyword">if</span> (locationResult == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span>;
        <span class="hljs-keyword">for</span> (Location location : locationResult.getLocations()) {
            <span class="hljs-comment">// Process each update</span>
        }
    }
};

fusedLocationClient.requestLocationUpdates(
    locationRequest,
    locationCallback,
    Looper.getMainLooper()
);
</code></pre>
<p><strong>Priority levels:</strong></p>
<ul>
<li><code>PRIORITY_HIGH_ACCURACY</code> — uses GPS when available, highest accuracy, highest power cost</li>
<li><code>PRIORITY_BALANCED_POWER_ACCURACY</code> — uses Wi-Fi and cell, roughly city-block accuracy, lower power</li>
<li><code>PRIORITY_LOW_POWER</code> — city-level accuracy, minimal power</li>
<li><code>PRIORITY_PASSIVE</code> — no active requests, receives updates from other apps</li>
</ul>
<p><strong>Always stop updates when you don't need them:</strong></p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onStop</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">super</span>.onStop();
    fusedLocationClient.removeLocationUpdates(locationCallback);
}
</code></pre>
<p>Failing to remove updates is one of the most common causes of unexpected battery drain in location-aware apps.</p>
<hr />
<h2 id="heading-permissions">Permissions</h2>
<p>Location requires explicit runtime permissions. There are two relevant permission levels:</p>
<ul>
<li><code>ACCESS_COARSE_LOCATION</code> — city-level accuracy, uses Wi-Fi and cell</li>
<li><code>ACCESS_FINE_LOCATION</code> — precise accuracy, uses GPS</li>
</ul>
<p>Declare them in <code>AndroidManifest.xml</code>:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_FINE_LOCATION"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_COARSE_LOCATION"</span> /&gt;</span>
</code></pre>
<p>And request them at runtime before making any location calls. Using the Activity Result API (recommended):</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> locationPermissionRequest = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { permissions -&gt;
    <span class="hljs-keyword">when</span> {
        permissions[Manifest.permission.ACCESS_FINE_LOCATION] == <span class="hljs-literal">true</span> -&gt; {
            <span class="hljs-comment">// Fine location granted</span>
        }
        permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == <span class="hljs-literal">true</span> -&gt; {
            <span class="hljs-comment">// Coarse location only</span>
        }
        <span class="hljs-keyword">else</span> -&gt; {
            <span class="hljs-comment">// No location permission</span>
        }
    }
}

locationPermissionRequest.launch(
    arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )
)
</code></pre>
<p>For background location (tracking while the app is not in the foreground), you also need <code>ACCESS_BACKGROUND_LOCATION</code>, which requires additional justification in the Play Store review process.</p>
<hr />
<h2 id="heading-choosing-between-locationmanager-and-fusedlocationproviderclient">Choosing Between LocationManager and FusedLocationProviderClient</h2>
<p>For most apps, <code>FusedLocationProviderClient</code> is the right choice. It handles provider selection automatically, optimizes battery usage, and produces more consistent results than managing raw providers yourself.</p>
<p><code>LocationManager</code> makes sense when:</p>
<ul>
<li>Your app runs on devices without Google Play Services (some older or specialized Android devices)</li>
<li>You need direct, unfiltered GPS output for precision-critical applications</li>
<li>You're building a tool that needs to reason about individual providers separately</li>
</ul>
<hr />
<h2 id="heading-reading-the-location-object">Reading the Location Object</h2>
<p>Regardless of which API you use, location updates arrive as <code>Location</code> objects. The most commonly used properties:</p>
<pre><code class="lang-java">location.getLatitude()      <span class="hljs-comment">// degrees, -90 to +90</span>
location.getLongitude()     <span class="hljs-comment">// degrees, -180 to +180</span>
location.getAccuracy()      <span class="hljs-comment">// estimated horizontal accuracy in meters</span>
location.getAltitude()      <span class="hljs-comment">// meters above WGS 84 ellipsoid (if available)</span>
location.getSpeed()         <span class="hljs-comment">// meters per second (if available)</span>
location.getBearing()       <span class="hljs-comment">// direction of travel in degrees (if available)</span>
location.getTime()          <span class="hljs-comment">// UTC timestamp in milliseconds</span>
location.getProvider()      <span class="hljs-comment">// which provider produced this update</span>
</code></pre>
<p><code>getAccuracy()</code> is particularly useful — it tells you the radius of the circle (in meters) within which the device's actual location falls with 68% confidence. A location with 5-meter accuracy is a confident GPS fix. A location with 1500-meter accuracy is a rough cell-tower estimate. Use the accuracy value to decide whether a given update is precise enough for your use case.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Android location works by blending multiple data sources — GPS, Wi-Fi, cell towers, and sensors. <code>LocationManager</code> gives you direct access to individual providers. <code>FusedLocationProviderClient</code> abstracts all of that into a single battery-efficient API where you describe the accuracy level you need. For most applications, start with <code>FusedLocationProviderClient</code> using <code>PRIORITY_BALANCED_POWER_ACCURACY</code>, switch to <code>PRIORITY_HIGH_ACCURACY</code> only when precision matters, and always remove update callbacks when the app is not actively displaying or using location data.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Web App Production Build: How to Build, Serve and Deploy to Any Hosting Platform]]></title><description><![CDATA[Flutter's web support has matured considerably, and deploying a Flutter app to the web follows a straightforward path once you understand the tooling. This guide walks through the full process — enabling web support, building a production release, se...]]></description><link>https://blog.anmolthedeveloper.com/flutter-web-app-production-build-how-to-build-serve-and-deploy-to-any-hosting-platform</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/flutter-web-app-production-build-how-to-build-serve-and-deploy-to-any-hosting-platform</guid><category><![CDATA[Dart]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[flutter web]]></category><category><![CDATA[hosting]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Sun, 16 Feb 2025 18:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739864953258/3f958574-c665-4581-b986-bdae1c53a1ab.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Flutter's web support has matured considerably, and deploying a Flutter app to the web follows a straightforward path once you understand the tooling. This guide walks through the full process — enabling web support, building a production release, serving it locally, and deploying it to a hosting platform.</p>
<hr />
<h2 id="heading-step-1-enable-flutter-web-support">Step 1: Enable Flutter Web Support</h2>
<p>First, verify that your Flutter installation has web support enabled:</p>
<pre><code class="lang-bash">flutter doctor
</code></pre>
<p>If web isn't listed as a supported platform, enable it:</p>
<pre><code class="lang-bash">flutter config --enable-web
</code></pre>
<p>You only need to do this once per Flutter installation.</p>
<hr />
<h2 id="heading-step-2-build-the-production-release">Step 2: Build the Production Release</h2>
<p>Navigate to your Flutter project directory and run:</p>
<pre><code class="lang-bash">flutter build web
</code></pre>
<p>This compiles your app into a set of static web assets — HTML, JavaScript, CSS, and assets — optimized for production. The output lands in the <code>build/web/</code> directory.</p>
<p>For a build that uses CanvasKit (better rendering fidelity, slightly larger download):</p>
<pre><code class="lang-bash">flutter build web --web-renderer canvaskit
</code></pre>
<p>For a smaller initial download that uses the browser's built-in HTML renderer:</p>
<pre><code class="lang-bash">flutter build web --web-renderer html
</code></pre>
<p>If you're not sure which to use, start with the default (which is <code>canvaskit</code> for release builds) and test on your target devices.</p>
<hr />
<h2 id="heading-step-3-serve-the-app-locally-before-deploying">Step 3: Serve the App Locally Before Deploying</h2>
<p>Flutter doesn't include a built-in web server, but you have several options for local testing.</p>
<p><strong>Quick test in Chrome (debug mode):</strong></p>
<pre><code class="lang-bash">flutter run -d chrome
</code></pre>
<p>For a build closer to production performance:</p>
<pre><code class="lang-bash">flutter run -d chrome --profile
</code></pre>
<p><strong>Serving the production build with Python:</strong></p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> build/web
python3 -m http.server 8000
</code></pre>
<p>Open <code>http://localhost:8000</code> in your browser.</p>
<p><strong>Serving with Node.js:</strong></p>
<pre><code class="lang-bash">npm install -g http-server
<span class="hljs-built_in">cd</span> build/web
http-server -p 8000
</code></pre>
<p><strong>Serving with Dart's dhttpd:</strong></p>
<pre><code class="lang-bash">dart pub global activate dhttpd
dhttpd --path build/web --port 8000
</code></pre>
<p>Any of these will serve the <code>build/web</code> folder as a static site. The Python option requires no additional installs if Python 3 is already on your system, which makes it the quickest option for a one-off check.</p>
<hr />
<h2 id="heading-step-4-deploy-to-a-hosting-platform">Step 4: Deploy to a Hosting Platform</h2>
<p>The <code>build/web/</code> directory contains everything needed to host your app — it's a standard set of static files. Any static hosting platform will work.</p>
<p><strong>Firebase Hosting</strong> (recommended if you're already using Firebase):</p>
<pre><code class="lang-bash">firebase init hosting
<span class="hljs-comment"># Point the public directory to build/web</span>
firebase deploy
</code></pre>
<p><strong>Vercel:</strong></p>
<pre><code class="lang-bash">npm install -g vercel
<span class="hljs-built_in">cd</span> build/web
vercel
</code></pre>
<p><strong>Netlify:</strong>
Drag and drop the <code>build/web</code> folder into the Netlify dashboard, or use the CLI:</p>
<pre><code class="lang-bash">npm install -g netlify-cli
netlify deploy --dir=build/web --prod
</code></pre>
<p><strong>GitHub Pages:</strong>
Copy the contents of <code>build/web</code> into your <code>gh-pages</code> branch, or use a GitHub Action to automate it on each push.</p>
<p><strong>AWS S3 + CloudFront:</strong>
Upload <code>build/web</code> to an S3 bucket configured for static website hosting, then point CloudFront at it for CDN distribution.</p>
<hr />
<h2 id="heading-a-few-things-worth-knowing">A Few Things Worth Knowing</h2>
<p><strong>The base href matters.</strong> If you're deploying to a subdirectory rather than the root of a domain, you'll need to set the base href at build time:</p>
<pre><code class="lang-bash">flutter build web --base-href /my-app/
</code></pre>
<p>Without this, routing and asset loading will break when the app isn't served from the root path.</p>
<p><strong>Routing on the server.</strong> Flutter web uses client-side routing by default. If a user navigates directly to a deep link (e.g., <code>yoursite.com/profile/123</code>), the server needs to serve <code>index.html</code> for that path rather than returning a 404. Most hosting platforms have a configuration option for this — in Firebase it's a rewrite rule, in Netlify it's a <code>_redirects</code> file.</p>
<p><strong>Service worker caching.</strong> Flutter web builds include a service worker for offline support. If you deploy an update and users aren't seeing it, they may have a cached version. The service worker handles cache invalidation automatically, but it can take a page refresh or two to propagate.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Enable web support, run <code>flutter build web</code>, verify locally with a simple HTTP server, and deploy the <code>build/web</code> folder to any static hosting provider. The whole process takes a few minutes once it's set up, and subsequent deployments are even faster.</p>
]]></content:encoded></item><item><title><![CDATA[How to Publish a Flutter App on Google Play Store and Apple App Store: Step-by-Step Guide]]></title><description><![CDATA[Building the app is the easy part. Getting it into the hands of users — navigating developer consoles, signing configurations, store policies, and review processes — is where first-time publishers tend to get stuck. This guide covers the complete pub...]]></description><link>https://blog.anmolthedeveloper.com/ship-happens-a-flutter-devs-no-bs-guide-to-publishing-on-play-store-and-app-store</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/ship-happens-a-flutter-devs-no-bs-guide-to-publishing-on-play-store-and-app-store</guid><category><![CDATA[Android]]></category><category><![CDATA[app development]]></category><category><![CDATA[app store]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[google play]]></category><category><![CDATA[iOS]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[publishing]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Thu, 30 Jan 2025 18:30:00 GMT</pubDate><content:encoded><![CDATA[<p>Building the app is the easy part. Getting it into the hands of users — navigating developer consoles, signing configurations, store policies, and review processes — is where first-time publishers tend to get stuck. This guide covers the complete publishing process for both Google Play and the Apple App Store, from a clean Flutter project to a live listing.</p>
<hr />
<h2 id="heading-part-1-google-play-store">Part 1: Google Play Store</h2>
<h3 id="heading-prepare-your-app">Prepare Your App</h3>
<p><strong>App identity</strong></p>
<p>Update your app name in <code>android/app/src/main/AndroidManifest.xml</code>:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">application</span> <span class="hljs-attr">android:label</span>=<span class="hljs-string">"YourAppName"</span>&gt;</span>
</code></pre>
<p>Add a proper launcher icon using <code>flutter_launcher_icons</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># pubspec.yaml</span>
<span class="hljs-attr">dev_dependencies:</span>
  <span class="hljs-attr">flutter_launcher_icons:</span> <span class="hljs-string">^0.13.0</span>

<span class="hljs-attr">flutter_icons:</span>
  <span class="hljs-attr">android:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">ios:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">image_path:</span> <span class="hljs-string">"assets/icon.png"</span>
</code></pre>
<pre><code class="lang-bash">flutter pub run flutter_launcher_icons
</code></pre>
<p>Set your version number in <code>pubspec.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-number">1.0</span><span class="hljs-number">.0</span><span class="hljs-string">+1</span>
</code></pre>
<p>The number before <code>+</code> is the user-visible version. The number after is the internal build number — increment this with every upload to the Play Store.</p>
<p><strong>Signing</strong></p>
<p>Google Play requires a signed AAB. Generate a keystore:</p>
<pre><code class="lang-bash">keytool -genkey -v -keystore ~/my-release-key.jks \
  -keyalg RSA -keysize 2048 -validity 10000 -<span class="hljs-built_in">alias</span> my-key
</code></pre>
<p>Create a <code>key.properties</code> file in the <code>android/</code> directory (and add it to <code>.gitignore</code> — never commit this):</p>
<pre><code>storePassword=yourpassword
keyPassword=yourpassword
keyAlias=my-key
storeFile=../my-release-key.jks
</code></pre><p>Reference it in <code>android/app/build.gradle</code>:</p>
<pre><code class="lang-groovy">def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(file("key.properties")))

android {
    signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}
</code></pre>
<p><strong>Build the AAB:</strong></p>
<pre><code class="lang-bash">flutter build appbundle
</code></pre>
<p>The output is at <code>build/app/outputs/bundle/release/app-release.aab</code>. This is what you upload — not an APK.</p>
<hr />
<h3 id="heading-google-play-console">Google Play Console</h3>
<p>Create a developer account at play.google.com/console. The one-time registration fee is $25.</p>
<p>Create a new app and fill in the basics — title, default language, free or paid, app or game.</p>
<p><strong>Store listing</strong> — you'll need:</p>
<ul>
<li>App title (50 characters max)</li>
<li>Short description (80 characters max)</li>
<li>Full description (up to 4000 characters)</li>
<li>App icon at 512x512 pixels</li>
<li>Feature graphic at 1024x500 pixels</li>
<li>At least 2 screenshots per device type</li>
<li>A privacy policy URL</li>
</ul>
<p><strong>Data safety</strong> — declare what data your app collects and why. Be accurate. Google cross-checks this against your app's behavior.</p>
<p><strong>Content rating</strong> — complete the questionnaire. It takes about 5 minutes.</p>
<p><strong>App access</strong> — if your app requires login to use, provide test credentials for reviewers. Apps without this often get rejected during review.</p>
<p><strong>Upload and release:</strong></p>
<p>Go to Production &gt; Create new release, upload your <code>.aab</code> file, add release notes, and submit for review.</p>
<p>Google typically reviews new apps in 1–7 days. Updates are usually faster, sometimes a few hours.</p>
<hr />
<h3 id="heading-common-rejection-reasons-android">Common Rejection Reasons (Android)</h3>
<ul>
<li>App requires login but no test account was provided</li>
<li>Trademarked terms in the app name</li>
<li>Data safety form doesn't match actual app behavior</li>
<li>Broken functionality or crashes in the review build</li>
<li>Missing privacy policy for apps that handle personal data</li>
</ul>
<hr />
<h2 id="heading-part-2-apple-app-store">Part 2: Apple App Store</h2>
<h3 id="heading-apple-developer-account">Apple Developer Account</h3>
<p>Register at developer.apple.com. The annual fee is $99 USD. Two-factor authentication is required.</p>
<p>Sign into Xcode with your Apple ID and accept the developer agreement before attempting any builds.</p>
<hr />
<h3 id="heading-prepare-your-ios-app">Prepare Your iOS App</h3>
<p>Update the version in <code>pubspec.yaml</code> — Xcode will pull from this.</p>
<p>Update your app name in <code>ios/Runner/Info.plist</code>:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>CFBundleDisplayName<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>YourAppName<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
</code></pre>
<p>Add permission descriptions for any sensitive APIs your app uses:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSCameraUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app uses the camera to scan barcodes.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSLocationWhenInUseUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>Your location is used to find nearby results.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
</code></pre>
<p>Apple will reject apps that use sensitive APIs without a usage description string.</p>
<p>For splash screens and icons:</p>
<pre><code class="lang-bash">flutter pub add flutter_native_splash
flutter pub run flutter_native_splash:create
</code></pre>
<hr />
<h3 id="heading-build-and-upload-via-xcode">Build and Upload via Xcode</h3>
<p>Open <code>ios/Runner.xcworkspace</code> in Xcode (not the <code>.xcodeproj</code> file).</p>
<p>Set your Bundle Identifier, Version, Build number, and Development Team under the Signing &amp; Capabilities tab.</p>
<p>Then go to <strong>Product &gt; Archive</strong>. This compiles a release build and adds it to the Xcode Organizer. From the Organizer, click <strong>Distribute App &gt; App Store Connect</strong> and follow the prompts. Xcode handles the upload.</p>
<hr />
<h3 id="heading-app-store-connect">App Store Connect</h3>
<p>At appstoreconnect.apple.com, create a new app with your Bundle ID and fill in:</p>
<ul>
<li>App name and subtitle</li>
<li>Description and promotional text</li>
<li>Keywords (100 characters, comma-separated)</li>
<li>Screenshots — iPhone 6.5" and 5.5" are required; iPad if your app supports it</li>
<li>App icon at 1024x1024 pixels (no rounded corners — the store adds those)</li>
<li>Privacy policy URL</li>
<li>Support URL</li>
<li>App privacy labels (what data you collect and how it's used)</li>
</ul>
<p>Select the build you uploaded via Xcode, complete all required fields, and submit for review.</p>
<p>Apple typically reviews within 1–3 days. Expect detailed feedback if anything is rejected — Apple's rejection messages are usually specific.</p>
<hr />
<h3 id="heading-common-rejection-reasons-ios">Common Rejection Reasons (iOS)</h3>
<ul>
<li>Placeholder content or lorem ipsum in screenshots or description</li>
<li>Broken functionality on smaller screen sizes (especially iPhone SE)</li>
<li>Missing or inaccurate privacy labels</li>
<li>Requesting permissions not actually used by the app</li>
<li>UI that doesn't follow platform conventions</li>
</ul>
<hr />
<h2 id="heading-publishing-updates">Publishing Updates</h2>
<p><strong>Android:</strong> Increment both version name and build number, rebuild the AAB, create a new release in Play Console, upload, and submit.</p>
<p><strong>iOS:</strong> Increment the build number in Xcode (the version visible to users can stay the same for minor updates), archive, upload via Xcode, and select the new build in App Store Connect.</p>
<hr />
<h2 id="heading-automation">Automation</h2>
<p>Once you've done the manual process a few times, automation becomes worth the setup cost.</p>
<p><strong>fastlane</strong> handles both platforms — it can build, sign, and upload to both stores from the command line. It has a Flutter-compatible setup.</p>
<p><strong>Codemagic</strong> is a CI/CD platform built specifically for Flutter. It handles signing, building, and publishing, triggered by pushes to your repository.</p>
<p><strong>GitHub Actions</strong> can run <code>flutter build appbundle</code> and upload to the Play Store using the <code>r0adkll/upload-google-play</code> action.</p>
<hr />
<h2 id="heading-before-you-submit-a-quick-checklist">Before You Submit — A Quick Checklist</h2>
<ul>
<li><code>debugShowCheckedModeBanner</code> is set to <code>false</code></li>
<li>App icon and splash screen are production-quality</li>
<li>All permission usage descriptions are accurate and non-generic</li>
<li>The app has been tested on real devices, not just simulators</li>
<li>Your signing credentials are backed up securely somewhere other than your development machine</li>
<li>Privacy policy is live at a publicly accessible URL</li>
<li>Release notes are written for humans, not just "bug fixes"</li>
</ul>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Android publishing is faster and cheaper to start ($25 one-time). Apple's process is more strict and more expensive ($99/year), but the review feedback is usually actionable. Both stores reward apps that are complete, well-documented, and honest about what they do. Get the manual process right first, then automate it.</p>
]]></content:encoded></item><item><title><![CDATA[How to Send Mock SMS to Android Emulator Using ADB: Complete Testing Guide]]></title><description><![CDATA[When building Android apps that handle SMS — OTP verification, message parsing, two-factor authentication flows — testing on a physical device means you're dependent on actually receiving messages, which slows things down. The Android emulator solves...]]></description><link>https://blog.anmolthedeveloper.com/how-to-send-mock-sms-using-adb-on-an-android-emulator</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/how-to-send-mock-sms-using-adb-on-an-android-emulator</guid><category><![CDATA[adb]]></category><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[Android Emulator]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[debugging]]></category><category><![CDATA[sms]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Sat, 25 Jan 2025 08:06:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740276927293/0e1e160c-8d70-44ed-85be-8c9c68f76653.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building Android apps that handle SMS — OTP verification, message parsing, two-factor authentication flows — testing on a physical device means you're dependent on actually receiving messages, which slows things down. The Android emulator solves this cleanly: ADB lets you inject mock SMS messages directly into a running emulator, so you can test your SMS handling logic without a SIM card or a real number.</p>
<p>This guide walks through setting up an emulator if you don't have one, starting it, and sending SMS messages via ADB.</p>
<hr />
<h2 id="heading-setting-up-an-emulator">Setting Up an Emulator</h2>
<p>If you already have an emulator configured, skip ahead to the next section.</p>
<p><strong>Install Android Studio</strong> from developer.android.com. During setup, make sure the Android Virtual Device (AVD) Manager component is selected.</p>
<p><strong>Create a virtual device:</strong></p>
<ol>
<li>Open Android Studio and go to <strong>Tools &gt; Device Manager</strong></li>
<li>Click <strong>Create Device</strong></li>
<li>Choose a hardware profile — Pixel 5 or Pixel 6 work well for most testing</li>
<li>Select a system image (Android 13 or 14 recommended) and download it if it's not already available</li>
<li>Adjust RAM and storage if needed, then click <strong>Finish</strong></li>
</ol>
<hr />
<h2 id="heading-starting-the-emulator">Starting the Emulator</h2>
<p><strong>From Android Studio:</strong></p>
<p>Open Device Manager and click the play button next to your emulator. Wait for it to fully boot before proceeding.</p>
<p><strong>From the command line:</strong></p>
<p>List your available emulators:</p>
<pre><code class="lang-bash">&lt;Android SDK path&gt;/emulator/emulator -list-avds
</code></pre>
<p>Start one by name:</p>
<pre><code class="lang-bash">&lt;Android SDK path&gt;/emulator/emulator -avd Pixel_5_API_33
</code></pre>
<p>Replace <code>Pixel_5_API_33</code> with the name from your AVD list.</p>
<p><strong>Verify the emulator is running:</strong></p>
<pre><code class="lang-bash">adb devices
</code></pre>
<p>You should see output like:</p>
<pre><code>List <span class="hljs-keyword">of</span> devices attached
emulator<span class="hljs-number">-5554</span>   device
</code></pre><p>If the status shows <code>offline</code> instead of <code>device</code>, wait a few more seconds for the emulator to finish booting.</p>
<hr />
<h2 id="heading-sending-a-mock-sms">Sending a Mock SMS</h2>
<p>Once the emulator is running, sending a test SMS is a single command:</p>
<pre><code class="lang-bash">adb emu sms send &lt;phone_number&gt; <span class="hljs-string">"&lt;message&gt;"</span>
</code></pre>
<p>For example:</p>
<pre><code class="lang-bash">adb emu sms send 5551234567 <span class="hljs-string">"Your OTP is 849201"</span>
</code></pre>
<p>The phone number can be any string — it doesn't need to be a real or valid number. The message can be anything, including structured formats your app might parse.</p>
<p><strong>Verify it arrived:</strong></p>
<p>Open the Messages app on the emulator. The SMS should appear immediately from the number you specified.</p>
<hr />
<h2 id="heading-if-you-have-multiple-devices-connected">If You Have Multiple Devices Connected</h2>
<p>If <code>adb devices</code> shows more than one device or emulator, target a specific one with the <code>-s</code> flag:</p>
<pre><code class="lang-bash">adb -s emulator-5554 emu sms send 5551234567 <span class="hljs-string">"Your OTP is 849201"</span>
</code></pre>
<p>Use the exact device ID from the <code>adb devices</code> output.</p>
<hr />
<h2 id="heading-useful-adb-commands-for-emulator-testing">Useful ADB Commands for Emulator Testing</h2>
<p><strong>List connected devices:</strong></p>
<pre><code class="lang-bash">adb devices
</code></pre>
<p><strong>Open a shell on the emulator:</strong></p>
<pre><code class="lang-bash">adb shell
</code></pre>
<p><strong>Install an APK:</strong></p>
<pre><code class="lang-bash">adb install path/to/your-app.apk
</code></pre>
<p><strong>Uninstall an app:</strong></p>
<pre><code class="lang-bash">adb uninstall com.your.package.name
</code></pre>
<p><strong>Clear app data (reset app state):</strong></p>
<pre><code class="lang-bash">adb shell pm clear com.your.package.name
</code></pre>
<p><strong>Restart ADB if devices aren't showing:</strong></p>
<pre><code class="lang-bash">adb kill-server
adb start-server
</code></pre>
<hr />
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p><strong>"emulator: command not found"</strong></p>
<p>The emulator binary isn't in your PATH. Either add <code>&lt;Android SDK&gt;/emulator/</code> to your PATH, or use the full path to the binary each time.</p>
<p><strong>Emulator not appearing in <code>adb devices</code></strong></p>
<p>Make sure the emulator has fully booted — the home screen should be visible. If it still doesn't appear, restart the ADB server with <code>adb kill-server</code> followed by <code>adb start-server</code>.</p>
<p><strong>SMS not appearing in the Messages app</strong></p>
<p>Some system images handle SMS simulation better than others. If you're on an older image, try updating to a more recent Android version. Also check that the Messages app is set as the default SMS app on the emulator.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Start the emulator, confirm it's listed with <code>adb devices</code>, then run <code>adb emu sms send &lt;number&gt; "&lt;message&gt;"</code>. The SMS appears in the emulator's Messages app immediately. This works for any SMS content — OTPs, structured messages, conversation threads — and makes SMS-related feature testing significantly faster than relying on real messages.</p>
]]></content:encoded></item><item><title><![CDATA[Flutter Background Location Tracking When App Is Killed: Using Android Foreground Service]]></title><description><![CDATA[Background location tracking in Flutter is one of those problems that looks straightforward until you actually try to build it for production. The pub.dev ecosystem has packages that promise to handle it, but the well-maintained ones come with licens...]]></description><link>https://blog.anmolthedeveloper.com/how-i-achieved-flutter-background-location-tracking-even-when-app-is-being-killed</link><guid isPermaLink="true">https://blog.anmolthedeveloper.com/how-i-achieved-flutter-background-location-tracking-even-when-app-is-being-killed</guid><category><![CDATA[Android]]></category><category><![CDATA[android development]]></category><category><![CDATA[background services]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Flutter Tutorial]]></category><category><![CDATA[Foreground Service]]></category><category><![CDATA[location ]]></category><category><![CDATA[Mobile Development]]></category><dc:creator><![CDATA[Anmol Singh Tuteja]]></dc:creator><pubDate>Wed, 30 Oct 2024 17:47:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740276717319/792e5c5e-0f5d-45c8-890b-538df88b53c1.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Background location tracking in Flutter is one of those problems that looks straightforward until you actually try to build it for production. The pub.dev ecosystem has packages that promise to handle it, but the well-maintained ones come with licensing fees — around $375 per app per year at the time I was working on this. For a commercial project, that's a reasonable cost. For a self-funded one, it's not.</p>
<p>So I built it using Android's native foreground service directly. This is a writeup of how I got there and why the approach works where most alternatives fail.</p>
<hr />
<h2 id="heading-why-most-packages-fail-when-the-app-is-killed">Why Most Packages Fail When the App Is Killed</h2>
<p>The root cause is threading. Most Flutter location packages run their location logic on the Flutter engine's main UI thread. When Android kills the app — whether because the user swiped it away, the OS reclaimed memory, or the device restarted — that thread stops. The location tracking stops with it.</p>
<p>A foreground service is different. It runs on its own thread, independent of the app's lifecycle. Android treats foreground services with higher priority than background processes, which means the OS is far less likely to terminate them under memory pressure. They also persist across app restarts because they're registered with Android as running services, not just background tasks attached to an activity.</p>
<p>The other key insight: any work you do from the service thread — API calls, Firebase writes, database updates — needs to happen within that thread. If you try to hand off data back to Flutter and then push it to Firebase from the Dart side, you reintroduce the dependency on the Flutter engine being alive. The service has to own the full chain: collect location, push to backend, done.</p>
<hr />
<h2 id="heading-the-approach">The Approach</h2>
<p>The architecture I landed on has three parts:</p>
<p><strong>An Android foreground service</strong> that runs independently, requests location updates from <code>FusedLocationProviderClient</code>, and pushes coordinates to Firebase Realtime Database directly from the native layer. It displays a persistent notification showing the current coordinates — both to meet Android's foreground service requirements (a visible notification is mandatory) and as a useful debugging tool during development.</p>
<p><strong>A Flutter-to-native bridge</strong> using a <code>MethodChannel</code> to start and stop the service. From the Flutter side, you call <code>startService</code> or <code>stopService</code>. The native side registers or kills the foreground service accordingly.</p>
<p><strong>The Flutter UI layer</strong> which shows the current state (tracking active/inactive) and lets the user control it. The UI doesn't receive location updates directly — it reads from Firebase if it needs to display current position, keeping the UI and the tracking service decoupled.</p>
<hr />
<h2 id="heading-why-a-foreground-service-specifically">Why a Foreground Service Specifically</h2>
<p>Android has several service types. A regular background service gets killed aggressively on newer Android versions — sometimes within minutes. A <code>JobScheduler</code> or <code>WorkManager</code> task runs on a schedule but isn't appropriate for continuous real-time tracking. A foreground service with a visible notification is the only option Android explicitly supports for long-running, user-aware background work.</p>
<p>The notification requirement isn't optional. Android will throw an exception if you try to run a foreground service without starting it with a notification. The notification tells the user "this app is actively doing something in the background" — which is the right contract to have with the user for location tracking anyway.</p>
<hr />
<h2 id="heading-integrating-with-flutter">Integrating with Flutter</h2>
<p>The <code>MethodChannel</code> wiring is the same pattern as any other Flutter-to-native communication. On the Dart side:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">const</span> _channel = MethodChannel(<span class="hljs-string">'com.yourapp/location_service'</span>);

Future&lt;<span class="hljs-keyword">void</span>&gt; startTracking() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> _channel.invokeMethod(<span class="hljs-string">'startLocationService'</span>);
}

Future&lt;<span class="hljs-keyword">void</span>&gt; stopTracking() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> _channel.invokeMethod(<span class="hljs-string">'stopLocationService'</span>);
}
</code></pre>
<p>On the Android (Kotlin) side, handle these calls in <code>MainActivity</code> and start or stop the foreground service in response. The service itself is a standard Android <code>Service</code> subclass that calls <code>startForeground()</code> in <code>onStartCommand</code>.</p>
<hr />
<h2 id="heading-firebase-writes-from-the-service-thread">Firebase Writes From the Service Thread</h2>
<p>This is where most implementations fall apart. The temptation is to collect location in the native service, pass it back to Flutter via a method channel, and then write to Firebase from Dart. Don't do this.</p>
<p>When the app is killed, the Flutter engine is gone. The method channel is gone. Any pending location updates sitting in a queue waiting to be passed to Dart disappear.</p>
<p>Instead, initialize the Firebase Android SDK directly in the service class and write to Realtime Database or Firestore from there. The Firebase Android SDK works perfectly from a foreground service. Your location data reaches Firebase whether or not the Flutter UI is running.</p>
<hr />
<h2 id="heading-permissions">Permissions</h2>
<p>Background location tracking requires explicit permissions. In <code>AndroidManifest.xml</code>:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_FINE_LOCATION"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_BACKGROUND_LOCATION"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.FOREGROUND_SERVICE"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.FOREGROUND_SERVICE_LOCATION"</span> /&gt;</span>
</code></pre>
<p><code>ACCESS_BACKGROUND_LOCATION</code> was added in Android 10 and must be requested separately from fine location — the user has to explicitly grant "Allow all the time" rather than just "Allow while using the app." Google Play also requires justification for this permission during app review.</p>
<p>On Android 14 and above, <code>FOREGROUND_SERVICE_LOCATION</code> is additionally required.</p>
<hr />
<h2 id="heading-what-this-approach-doesnt-cover">What This Approach Doesn't Cover</h2>
<p>iOS is a different story entirely. iOS doesn't have foreground services in the Android sense. Continuous background location on iOS requires a different entitlement (<code>Location updates</code> background mode) and has stricter restrictions on what the app can do while backgrounded. This approach is Android-specific.</p>
<p>For cross-platform background location with iOS support, you'll need either a paid package or a separate native iOS implementation.</p>
<hr />
<h2 id="heading-the-result">The Result</h2>
<p>The foreground service approach works reliably across the Android versions I tested — from Android 9 through Android 14. The app can be killed by the user, the phone can restart, and as long as the service was running when those events happened, it continues running afterward (Android restarts foreground services automatically after a device reboot if <code>START_STICKY</code> is returned from <code>onStartCommand</code>).</p>
<p>The notification is a small UX cost, but for apps doing delivery tracking, attendance, or any use case where the user knows the app is tracking them, it's the right expectation to set. It's also honest — the user can see exactly when tracking is active.</p>
<p>If you're building something that genuinely needs persistent background location on Android and the package licensing fees aren't feasible, the native foreground service approach is worth the setup cost. It's more code, but it's reliable in a way that Flutter-layer solutions typically aren't.</p>
]]></content:encoded></item></channel></rss>