<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thanasis Traitsis</title>
    <description>The latest articles on DEV Community by Thanasis Traitsis (@thanasistraitsis).</description>
    <link>https://dev.to/thanasistraitsis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1172284%2F885a4a20-d044-4c78-8e9f-7267bf830fdb.png</url>
      <title>DEV Community: Thanasis Traitsis</title>
      <link>https://dev.to/thanasistraitsis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thanasistraitsis"/>
    <language>en</language>
    <item>
      <title>Building Local Notifications in Flutter...Properly</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Mon, 08 Jun 2026 21:34:37 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/building-local-notifications-in-flutterproperly-26o7</link>
      <guid>https://dev.to/thanasistraitsis/building-local-notifications-in-flutterproperly-26o7</guid>
      <description>&lt;p&gt;Have you ever added local notifications to a Flutter app and felt like you were just... copying lines without really knowing what you were doing? &lt;code&gt;flutter_local_notifications&lt;/code&gt;, a manifest entry, a permission request, a scheduled call. It works. Sometimes. On some devices. With some Android versions. And when it doesn't, &lt;strong&gt;you have no idea why&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've been there too. Recently, while working on a side project of mine called &lt;a href="https://github.com/Thanasis-Traitsis/my_eyes_flutter" rel="noopener noreferrer"&gt;My Eyes&lt;/a&gt; (an optical health tracker), I wanted to remind users to replace their contact lenses a few days before expiry. Sounded simple. &lt;em&gt;Schedule a notification 27 days from now,&lt;/em&gt; done. But the more I dug into it, the more I realized I didn't actually understand any of it. What does the operating system do with that schedule? Why are there three different permissions on Android and they all look similar? What's the difference between a "notification" and a "channel"? Why does the package force me to use &lt;code&gt;TZDateTime&lt;/code&gt; instead of a normal &lt;code&gt;DateTime&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;So I went down the rabbit hole. And the deeper I went, the more I realized that most notification tutorials out there don't really teach you how notifications work. They teach you how to copy-paste the API. That's not enough. Because the moment something goes wrong, and trust me, it will, you need the mental model to debug it. This article is the long-form version of everything I learned. We're going to cover the &lt;strong&gt;local notification system in Flutter&lt;/strong&gt;, from the very first moment your app says &lt;em&gt;"hey OS, I want to schedule this"&lt;/em&gt; to the moment the user taps the notification and lands inside your app. We will talk about the native side, the permissions, the channels, the payloads, the architecture, the tests. Everything.&lt;/p&gt;

&lt;p&gt;One thing to set straight upfront. &lt;strong&gt;There are two kinds of notifications:&lt;/strong&gt; local (scheduled by your app, fired by the device, no internet needed) and remote/push (sent from a server through FCM or APNs). &lt;strong&gt;Today we're only covering local.&lt;/strong&gt; Push deserves its own deep dive, and I'll write that one as a part 2. So if you came here for backend-triggered notifications, the foundation we lay today will make that follow-up much easier. But the post itself is 100% local.&lt;/p&gt;

&lt;p&gt;Here's everything we're going to cover in this article:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How Local Notifications Actually Work&lt;/strong&gt; — the mental model every Flutter developer needs before writing a single line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Three Packages and Why Each One Exists&lt;/strong&gt; — why you need all three, not just one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions: What Each One Actually Does&lt;/strong&gt; — the two gates most tutorials only half-explain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channels: Giving Users Control Over What They Hear&lt;/strong&gt; — Android 8+ and what iOS does instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designing the NotificationService&lt;/strong&gt; — &lt;strong&gt;this is where the code starts. If you're already comfortable with the concepts, jump straight here.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiring It Up: Inside init()&lt;/strong&gt; — the startup sequence that has to run before anything else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling, Payloads, and Cancelling&lt;/strong&gt; — the three methods that make the whole thing work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Tap: Where the Stream Earns Its Keep&lt;/strong&gt; — handling taps across all three app states&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You know me by now... we're not just going to make it work. We're going to understand &lt;strong&gt;why&lt;/strong&gt; it works &lt;strong&gt;the way&lt;/strong&gt; it does. Let's begin.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Section 1: How Local Notifications Actually Work
&lt;/h2&gt;

&lt;p&gt;Before we touch a single line of code, we need to clear up the biggest misconception most Flutter developers have about notifications. And it's not a small one. It's the kind that, once you understand it, you are truly aware of the code you write. Most developers think the notification system works like a timer running inside their app. You schedule something, your code waits, the time arrives, your code fires the notification. Simple, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's not what happens at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you "schedule a notification," your app doesn't keep track of anything. It doesn't wait. It doesn't hold a timer. What it actually does is &lt;strong&gt;hand a piece of paper to the operating system&lt;/strong&gt; and walk away. That paper says: &lt;em&gt;"Hey OS, please show this text, with this title, at this exact moment in time, with this sound, with this icon. Here's an ID in case I need to cancel it later. Thanks."&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;And then your app is done. The OS takes the paper, files it in its own internal scheduler, and your app has zero responsibility from that point on.&lt;/p&gt;

&lt;h4&gt;
  
  
  Wait...so the OS is doing all the work?
&lt;/h4&gt;

&lt;p&gt;Yes. Exactly that. Both Android and iOS have a &lt;strong&gt;system-level service&lt;/strong&gt; that runs continuously in the background, owned by the operating system itself, not by any individual app. On Android it's called &lt;code&gt;AlarmManager&lt;/code&gt;. On iOS it's &lt;code&gt;UNUserNotificationCenter&lt;/code&gt;. These services are running on your phone right now, holding scheduled notifications for dozens of apps you've installed.&lt;/p&gt;

&lt;p&gt;When the scheduled moment arrives, the OS looks at its internal list, sees that something is due to fire, and displays the notification. &lt;strong&gt;Your app doesn't even need to be running.&lt;/strong&gt; It can be killed. It can be in the background for hours. The phone can be on airplane mode. None of it matters. The OS is the one with the alarm clock, not your code.&lt;/p&gt;

&lt;h4&gt;
  
  
  So what does our app actually do?
&lt;/h4&gt;

&lt;p&gt;Three things, and only three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tell the OS what to schedule&lt;/strong&gt; (the title, body, time, icon, sound, payload, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the OS to cancel something&lt;/strong&gt; previously scheduled, using the ID we gave it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React to taps&lt;/strong&gt; when the user opens a notification and lands inside our app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  The bridge between Dart and the OS
&lt;/h4&gt;

&lt;p&gt;Now here's the obvious question: &lt;em&gt;how does our Dart code talk to a native OS service?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dart doesn't speak &lt;code&gt;AlarmManager&lt;/code&gt;. It doesn't speak &lt;code&gt;UNUserNotificationCenter&lt;/code&gt;. These are native APIs, written in Kotlin/Java on Android and Swift/Objective-C on iOS. So we need a translator. Something that takes our Dart instructions and turns them into the right native calls. That translator is exactly what the &lt;code&gt;flutter_local_notifications&lt;/code&gt; package does. But more about packages are on the next section...&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 2: The Three Packages and Why Each One Exists
&lt;/h2&gt;

&lt;p&gt;So we agreed that our app is just a thin layer that hands instructions to the OS. Now the question is, what do we actually need on the Dart side to do that?&lt;/p&gt;

&lt;p&gt;The honest answer is &lt;strong&gt;three packages.&lt;/strong&gt; Not one. Not two. Three. And I know what you're thinking, &lt;em&gt;"three packages just to show a notification? Really?"&lt;/em&gt; Yes, really. But by the end of this section, you'll understand why each one is non-negotiable. Each one solves a specific problem the other two can't.&lt;/p&gt;

&lt;h4&gt;
  
  
  Package 1: &lt;a href="https://pub.dev/packages/flutter_local_notifications" rel="noopener noreferrer"&gt;flutter_local_notifications&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;This is the one that does the actual work. We already talked about it in Section 1 — it's the bridge between your Dart code and the native OS notification service. When you call something like &lt;code&gt;_plugin.zonedSchedule(...)&lt;/code&gt; in Dart, this package's native side picks it up and calls &lt;code&gt;AlarmManager&lt;/code&gt; on Android or &lt;code&gt;UNUserNotificationCenter&lt;/code&gt; on iOS.&lt;/p&gt;

&lt;p&gt;So far so good. This is the package you'd reach for first, and it's the only one you'll directly interact with in your code. You'll call &lt;code&gt;initialize&lt;/code&gt;, you'll call &lt;code&gt;zonedSchedule&lt;/code&gt;, you'll call &lt;code&gt;cancel&lt;/code&gt;. All of it goes through this package. It's the workhorse. But here's the thing. The moment you try to actually schedule a notification, you hit a wall. The package refuses to accept a plain Dart &lt;code&gt;DateTime&lt;/code&gt;. It demands something called a &lt;strong&gt;&lt;code&gt;TZDateTime&lt;/code&gt;&lt;/strong&gt;. And that's where the second package comes in.&lt;/p&gt;

&lt;h4&gt;
  
  
  Package 2: &lt;a href="https://pub.dev/packages/timezone" rel="noopener noreferrer"&gt;timezone&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Wait, why is this even a problem? Why isn't &lt;code&gt;DateTime&lt;/code&gt; enough? Because &lt;code&gt;DateTime&lt;/code&gt; &lt;strong&gt;lies to you about time.&lt;/strong&gt; That sounds dramatic, but stick with me.&lt;/p&gt;

&lt;p&gt;When you write &lt;code&gt;DateTime(2027, 1, 28, 9, 0)&lt;/code&gt;, what you're actually saying is "9:00 AM on January 28, 2027", in the timezone the device is currently in. But the device's timezone isn't a fixed thing. The user could travel. Daylight Saving Time could shift. The device could be in one zone when you schedule the notification, and in a completely different one when it's supposed to fire 27 days later.&lt;/p&gt;

&lt;p&gt;So "9:00 AM" becomes ambiguous. &lt;strong&gt;Is it 9:00 AM in Athens, where the user was when they scheduled it? Or 9:00 AM in Tokyo, where they happen to be a week later?&lt;/strong&gt; Without a timezone attached to the time, there's no correct answer.&lt;/p&gt;

&lt;p&gt;This is exactly why &lt;code&gt;flutter_local_notifications&lt;/code&gt; refuses to use plain &lt;code&gt;DateTime&lt;/code&gt;. It would be a bug waiting to happen, especially around DST changes. So it forces you to use &lt;code&gt;TZDateTime&lt;/code&gt;, which is a timezone-aware datetime. It carries its zone with it. &lt;strong&gt;"9:00 AM in Europe/Athens"&lt;/strong&gt; is unambiguous, no matter where the device is when the alarm fires.&lt;/p&gt;

&lt;p&gt;The timezone package provides this &lt;code&gt;TZDateTime&lt;/code&gt; class, plus the entire &lt;a href="https://www.iana.org/time-zones" rel="noopener noreferrer"&gt;IANA timezone database&lt;/a&gt; (every timezone on Earth, with every historical DST rule), compiled into the package. When you initialize it, your app loads that database into memory once, and from then on you can work with any timezone in the world with full precision.&lt;/p&gt;

&lt;p&gt;So now we have: the package that talks to the OS (&lt;code&gt;flutter_local_notifications&lt;/code&gt;), and the package that knows about every timezone in existence (&lt;code&gt;timezone&lt;/code&gt;). But we're still missing something. &lt;strong&gt;What's the device's current timezone?&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Package 3: &lt;a href="https://pub.dev/packages/flutter_timezone" rel="noopener noreferrer"&gt;flutter_timezone&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;The timezone package knows about Athens. It knows about Tokyo. It knows about every IANA zone ever defined. But it doesn't know &lt;strong&gt;which one your device is currently in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To answer that question, we need to ask the OS directly. &lt;em&gt;"Hey OS, what timezone are you currently set to?"&lt;/em&gt; And that's what &lt;code&gt;flutter_timezone&lt;/code&gt; does. It's a tiny package that makes one platform-channel call to Android or iOS and returns a string like &lt;code&gt;"Europe/Athens"&lt;/code&gt;. That's all. That's the whole package. So, to summarize everything, let's look at the three packages side by side:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;flutter_local_notifications&lt;/strong&gt; → the bridge to the OS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;timezone&lt;/strong&gt; → the library of every timezone rule in the world&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;flutter_timezone&lt;/strong&gt; → asks the device which timezone it's currently in&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one solves a problem the others can't. The first one alone is useless because it can't accept naive times. The second one alone is useless because it doesn't know what "local" means. The third one alone is useless because it's just a string-fetcher with no scheduling capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 3: Permissions: What Each One Actually Does
&lt;/h2&gt;

&lt;p&gt;This is the section most notification tutorials either skip entirely or get completely wrong. And the consequence is a specific, frustrating bug. Your code is correct. The package is initialized. The schedule call succeeds. And yet, &lt;strong&gt;the notification never appears.&lt;/strong&gt; No error. No crash. Nothing.&lt;/p&gt;

&lt;p&gt;Almost every time, the answer is permissions.&lt;/p&gt;

&lt;p&gt;But here's where people get confused. When they hear "notification permissions," they think of one thing, the dialog that pops up and asks &lt;em&gt;"Allow My App to send you notifications?"&lt;/em&gt; That dialog is real, and it matters. But it's only &lt;strong&gt;half the story.&lt;/strong&gt; There are actually &lt;strong&gt;two completely separate permission gates&lt;/strong&gt; standing between your code and a notification that fires at the right moment. And most tutorials only tell you about one of them.&lt;/p&gt;

&lt;h4&gt;
  
  
  POST_NOTIFICATIONS: the visibility gate
&lt;/h4&gt;

&lt;p&gt;The first gate controls a simple question: &lt;strong&gt;can your app show notifications in the user's tray at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the permission that controls the visible banner, the sound, the lock screen entry, everything the user actually &lt;em&gt;sees&lt;/em&gt;. Without it, your notifications are scheduled correctly, they even fire internally, but they are silently discarded before ever reaching the screen.&lt;/p&gt;

&lt;p&gt;Now here's where Android's history makes things complicated. The rule has changed depending on which version of Android your user is running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android 12 and below&lt;/strong&gt; → notification display was granted automatically at install. No prompt, no dialog. Your app could show notifications the moment it was installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android 13 and above&lt;/strong&gt; → &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; became a &lt;strong&gt;runtime permission&lt;/strong&gt;. Your app must explicitly ask the user for it. The user sees a dialog. If they tap &lt;em&gt;"Don't allow"&lt;/em&gt;, the OS won't show the dialog again. The user has to go into Settings manually to re-enable it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same notification code behaves completely differently depending on the device. On an old device, it just works. On a modern device, it's blocked until the user explicitly grants it. &lt;strong&gt;If you skip this permission on Android 13+, your notifications will silently fail on the majority of active Android devices today.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One golden rule about &lt;strong&gt;when&lt;/strong&gt; to request this permission: &lt;strong&gt;never ask at app launch.&lt;/strong&gt; Ask at the exact moment the feature becomes relevant, when the user takes an action that would benefit from notifications. That moment of context is what turns a cold permission prompt into something the user actually understands and is likely to accept.&lt;/p&gt;

&lt;h4&gt;
  
  
  USE_EXACT_ALARM vs SCHEDULE_EXACT_ALARM: the timing gate
&lt;/h4&gt;

&lt;p&gt;The second gate is completely separate from the first. It doesn't control &lt;strong&gt;whether&lt;/strong&gt; a notification appears. It controls &lt;strong&gt;whether it appears at the precise moment you specified.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without an exact-alarm permission, Android's battery optimizer is free to batch your alarm with other apps' alarms and fire them all together at a convenient moment. Your &lt;em&gt;"fire at 9:00 AM"&lt;/em&gt; might fire at 9:47 AM. Or later. On Doze mode (Android's deep sleep state) the delay can be hours.&lt;/p&gt;

&lt;p&gt;For a time-sensitive reminder, this is unacceptable. So Android introduced exact-alarm permissions to let apps opt into precise scheduling. But here's the thing: there are &lt;strong&gt;two versions of this permission&lt;/strong&gt;, and the difference is not about capability. They do the exact same thing at runtime. The difference is about &lt;strong&gt;how the permission is granted, and whether the user can take it back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt; is the older one, introduced in Android 12. It's granted automatically at install on most devices, but the user can &lt;strong&gt;revoke it&lt;/strong&gt; at any time from &lt;em&gt;Settings → Apps → Your App → Alarms &amp;amp; Reminders&lt;/em&gt;. If they revoke it and your code tries to schedule an exact alarm, it throws a &lt;code&gt;SecurityException&lt;/code&gt;. You need to check &lt;code&gt;canScheduleExactAlarms()&lt;/code&gt; before every scheduling call.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;USE_EXACT_ALARM&lt;/code&gt; is the newer one, introduced in Android 13. It's granted at install and &lt;strong&gt;cannot be revoked&lt;/strong&gt;. The user has no toggle for it in Settings. But Google Play enforces a strict policy: this permission is only for apps whose core function is timed reminders, like alarm clocks, calendar apps, medication trackers etc. If you use it for something that doesn't qualify, your listing can be removed.&lt;/p&gt;

&lt;p&gt;The cleanest approach is to declare &lt;strong&gt;both&lt;/strong&gt; in your &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, using the &lt;code&gt;maxSdkVersion&lt;/code&gt; attribute to scope each one to the right Android version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.POST_NOTIFICATIONS"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.SCHEDULE_EXACT_ALARM"&lt;/span&gt;
    &lt;span class="na"&gt;android:maxSdkVersion=&lt;/span&gt;&lt;span class="s"&gt;"32"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.USE_EXACT_ALARM"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maxSdkVersion="32"&lt;/code&gt; tells Android: &lt;em&gt;"Only consider this permission on API 32 and below."&lt;/em&gt; On Android 13+ (API 33+), the OS sees &lt;code&gt;USE_EXACT_ALARM&lt;/code&gt; instead. On Android 12, it falls back to &lt;code&gt;SCHEDULE_EXACT_ALARM&lt;/code&gt;. One manifest. Both versions covered.&lt;/p&gt;

&lt;h4&gt;
  
  
  RECEIVE_BOOT_COMPLETED: The forgotten one
&lt;/h4&gt;

&lt;p&gt;There's a third permission worth knowing about, and it's the one most developers discover the hard way. It's called &lt;code&gt;RECEIVE_BOOT_COMPLETED&lt;/code&gt;, and it solves a very specific quirk of Android.&lt;/p&gt;

&lt;p&gt;When an Android device reboots, &lt;strong&gt;the OS forgets every alarm that was ever scheduled by every app.&lt;/strong&gt; &lt;code&gt;AlarmManager&lt;/code&gt; is completely wiped. This means a user who restarts their phone will silently stop receiving reminders, with no indication that anything went wrong. Everything looked fine. Then the phone rebooted. And now, nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RECEIVE_BOOT_COMPLETED&lt;/code&gt; gives your app permission to receive a signal the moment the device finishes booting. &lt;code&gt;flutter_local_notifications&lt;/code&gt; uses this automatically to reschedule everything it had previously queued. You don't write any special code, you just declare the permission, and the plugin handles the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.RECEIVE_BOOT_COMPLETED"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS doesn't have this problem. The OS persists scheduled local notifications across reboots automatically. Just another quiet asymmetry between the two platforms.&lt;/p&gt;

&lt;h4&gt;
  
  
  What about iOS?
&lt;/h4&gt;

&lt;p&gt;Compared to Android, iOS is almost refreshingly simple. There is &lt;strong&gt;one permission for local notifications&lt;/strong&gt;, and it controls everything, the banner, the sound, the badge count. No exact-alarm distinction, no boot signal, no version fragmentation.&lt;/p&gt;

&lt;p&gt;You don't declare anything in &lt;code&gt;Info.plist&lt;/code&gt;. You don't add any entitlements. &lt;strong&gt;You just ask for permission in code at the right moment&lt;/strong&gt;, and the OS shows this prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"[Your App] Would Like to Send You Notifications"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Allow or Don't Allow. That decision sticks. If they tap &lt;em&gt;Don't Allow&lt;/em&gt;, the OS won't show the prompt again, same as Android 13+, same "ask at the right moment" rule applies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Two gates. Both must be open.
&lt;/h4&gt;

&lt;p&gt;This is the most important takeaway from this entire section:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; controls whether the notification &lt;strong&gt;appears at all.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The exact-alarm permission controls whether it appears &lt;strong&gt;at the right time.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are not alternatives. You don't choose between them. &lt;strong&gt;You need both.&lt;/strong&gt; If you only have &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;, your notifications will show, but potentially an hour late. If you only have the exact-alarm permission, your alarms fire precisely but are silently discarded before the user ever sees them.&lt;/p&gt;

&lt;p&gt;Think of it like a locked door with two deadbolts. Both must be open. One is not enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep that in mind as we move into the next section.&lt;/strong&gt; Because before we write a single line of Dart, there's one more native concept we need to understand, and it's the one that gives users control over the notifications they receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 4: Channels: Giving Users Control Over What They Hear
&lt;/h2&gt;

&lt;p&gt;If you've ever gone into your phone's settings and turned off notifications for a specific &lt;em&gt;category&lt;/em&gt; of an app (say, disabling the promotional ones but keeping the important alerts) you just used a &lt;code&gt;notification channel&lt;/code&gt;. Even if you didn't know it.&lt;/p&gt;

&lt;p&gt;Channels are an &lt;strong&gt;Android-only concept&lt;/strong&gt;, introduced in Android 8.0. iOS has nothing equivalent, and we'll get to why in a moment. But on Android, every single notification your app shows must belong to a channel. &lt;strong&gt;Without a channel, your notification won't display at all on Android 8 and above.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Why do channels exist?
&lt;/h4&gt;

&lt;p&gt;Before Android 8, notification settings were all-or-nothing. Either your app could send notifications, or it couldn't. There was no middle ground. If you wanted to mute the promotional emails from an app but still receive the security alerts, you had two options: put up with everything, or turn off all notifications and miss the things that actually mattered. &lt;strong&gt;Channels&lt;/strong&gt; were Android's solution to that problem. They hand &lt;strong&gt;granular control back to the user.&lt;/strong&gt; Instead of one on/off switch per app, the user gets one switch &lt;em&gt;per category.&lt;/em&gt; Your app declares the categories&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Reminders" &lt;/li&gt;
&lt;li&gt;"Promotions" &lt;/li&gt;
&lt;li&gt;"Security Alerts", &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;and the user decides which ones they want to hear from.&lt;/p&gt;

&lt;h4&gt;
  
  
  So what exactly is a channel?
&lt;/h4&gt;

&lt;p&gt;A channel is a named category that carries a set of display settings. When you create one, you define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An ID:&lt;/strong&gt; a unique string that identifies this channel internally. &lt;code&gt;"reminders"&lt;/code&gt;, &lt;code&gt;"order_updates"&lt;/code&gt;, &lt;code&gt;"security"&lt;/code&gt;. This is what you reference when scheduling a notification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A name:&lt;/strong&gt; the user-facing label. This is what appears in the app's notification settings screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A description:&lt;/strong&gt; a short explanation of what this channel is for. Also visible to the user in Settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An importance level:&lt;/strong&gt; this controls &lt;em&gt;how prominently&lt;/em&gt; the notification is displayed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The importance level deserves a closer look, because it's the thing that most directly shapes the user experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Importance.min&lt;/code&gt; → appears silently in the shade, no icon in the status bar&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Importance.low&lt;/code&gt; → appears quietly, no sound, status bar icon shows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Importance.defaultImportance&lt;/code&gt; → sound plays, status bar icon shows, but no pop-up banner&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Importance.high&lt;/code&gt; → sound plays, status bar icon shows, &lt;strong&gt;and&lt;/strong&gt; a heads-up banner slides down at the top of the screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Importance.max&lt;/code&gt; → same as high, reserved for urgent alerts like incoming calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a reminder app, &lt;code&gt;Importance.high&lt;/code&gt; is the right choice. The user opted into these reminders, they should be hard to miss.&lt;/p&gt;

&lt;h4&gt;
  
  
  The rule nobody tells you about
&lt;/h4&gt;

&lt;p&gt;Here's the part that catches everyone off guard. Once you create a channel and the user has seen it in their settings, &lt;strong&gt;you cannot change its properties.&lt;/strong&gt; Not the importance. Not the sound. Not the vibration. Those settings now belong to the user.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you create a channel with &lt;code&gt;Importance.high&lt;/code&gt; and later decide it should be &lt;code&gt;Importance.low&lt;/code&gt;, calling &lt;code&gt;createNotificationChannel&lt;/code&gt; again with the same ID does absolutely nothing. The channel already exists, and the OS ignores your updated settings entirely. The only way to change it is to create a &lt;em&gt;new channel with a different ID&lt;/em&gt;, which leaves the old one as a dead orphan in the user's settings until they clear it manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This is why you need to think about your channels before you ship.&lt;/strong&gt; Pick your IDs carefully. Pick your importance carefully. Once your users have the app installed, those decisions are locked.&lt;/p&gt;

&lt;p&gt;The good news: creating a channel that already exists is completely safe. It's a no-op. Calling &lt;code&gt;createNotificationChannel&lt;/code&gt; every time your app starts is fine, and actually the recommended approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  What about iOS?
&lt;/h4&gt;

&lt;p&gt;iOS doesn't have channels because Apple took a completely different philosophy. Instead of letting apps declare categories, &lt;strong&gt;Apple gives users control through Focus modes, per-app notification summaries, and system-level notification management.&lt;/strong&gt; When you create channels in your Flutter code, they are completely ignored on iOS. The plugin handles this transparently, the channel creation call simply does nothing on the iOS side. You write the code once, and the platform figures out what to do with it.&lt;/p&gt;

&lt;p&gt;What iOS &lt;em&gt;does&lt;/em&gt; enforce is a hard limit: &lt;strong&gt;you can have at most 64 pending local notifications scheduled at any given time.&lt;/strong&gt; Schedule a 65th and the OS quietly drops the oldest one. For most apps this never becomes an issue. But it's worth knowing, if your app ever needs to schedule dozens of reminders per user, you'll need to be deliberate about which ones matter most. Maybe this is &lt;code&gt;"your useless tip of the day"&lt;/code&gt;...anyway 😅.&lt;/p&gt;

&lt;h4&gt;
  
  
  Channels and your app
&lt;/h4&gt;

&lt;p&gt;For our demo app, we need exactly one channel. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationChannel&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;'reminders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// id&lt;/span&gt;
  &lt;span class="s"&gt;'Reminders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                           &lt;span class="c1"&gt;// name&lt;/span&gt;
  &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="s"&gt;'Scheduled reminders.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's an interesting architectural question. What happens when your app grows and you need three, four, five channels? Do they all live scattered across different files? Do you hardcode their IDs as strings in multiple places and hope nothing ever gets out of sync?&lt;/p&gt;

&lt;p&gt;This is exactly the kind of problem that compounds quietly as a codebase grows. And it's what we'll address in the next section, when we design the &lt;code&gt;NotificationService&lt;/code&gt;, a single class that owns your entire notification system, keeps your channels in one place, and makes sure the rest of your app never needs to know how any of it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's where we're going next.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 5: Designing the NotificationService
&lt;/h2&gt;

&lt;p&gt;Let's talk about a mistake that's very easy to make. You just learned about channels, permissions, and the OS scheduler. Now you want to write the scheduling code. And the natural instinct is: &lt;em&gt;"I have a button. When it's pressed, I schedule a notification. So the scheduling code goes... near the button."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And just like that, your notification logic is buried inside a widget. Or inside a cubit that happens to need notifications. Or scattered across three different files because three different features all need reminders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the first thing we're going to get right.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  One class, one responsibility
&lt;/h4&gt;

&lt;p&gt;The rule is simple. All notification logic like initialization, channel creation, permission requests, scheduling, cancelling, tap handling, lives in &lt;strong&gt;exactly one class.&lt;/strong&gt; We'll call it &lt;code&gt;NotificationService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This class lives in your &lt;code&gt;core/services/&lt;/code&gt; folder. Not inside any specific feature. Not next to your screens or your cubits. In &lt;code&gt;core/&lt;/code&gt;, because it's a horizontal capability, something any feature in your app can use without that feature having any ownership over it.&lt;/p&gt;

&lt;p&gt;Think about what this means in practice. When you add a second feature that also needs reminders, it doesn't write its own scheduling code. It calls &lt;code&gt;NotificationService&lt;/code&gt;. When you add a third feature, same thing. The notification system doesn't grow. The &lt;em&gt;callers&lt;/em&gt; grow. &lt;strong&gt;There's only ever one place in your codebase that knows how to talk to the OS notification service.&lt;/strong&gt; That makes it easy to test, easy to update, and impossible to get out of sync with itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NotificationService&lt;/code&gt; is registered as a &lt;strong&gt;singleton&lt;/strong&gt; in your dependency injection setup. One instance, created once, alive for the entire lifetime of the app. The plugin it wraps maintains internal state that shouldn't be duplicated. Creating multiple instances would be a bug.&lt;/p&gt;

&lt;h4&gt;
  
  
  The most important design decision: primitives only
&lt;/h4&gt;

&lt;p&gt;Here's the design decision that separates a good notification service from a great one. And it's the one most people get wrong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What if I just pass my whole data object into the service? It already has the title, the date, everything I need."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Don't. &lt;strong&gt;The &lt;code&gt;NotificationService&lt;/code&gt; should not know anything about your app's domain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It shouldn't know about users, orders, tasks, products, or whatever your app is about. If it did, it would become a service that can only ever serve &lt;em&gt;that one feature.&lt;/em&gt; The moment a second feature needs notifications, you're stuck, either you bloat the service with another domain type, or you duplicate the scheduling code elsewhere. Neither is good.&lt;/p&gt;

&lt;p&gt;The clean solution is to make the service speak in &lt;strong&gt;primitives only:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;id&lt;/code&gt; as an &lt;code&gt;int&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;fireAt&lt;/code&gt; as a plain &lt;code&gt;DateTime&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;title&lt;/code&gt; as a &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;body&lt;/code&gt; as a &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;payload&lt;/code&gt; as a &lt;code&gt;String?&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. The service doesn't know what a "task" is or what an "order" is. It receives a time and some text, and it schedules them. The responsibility of translating &lt;em&gt;"this order ships on January 28"&lt;/em&gt; into &lt;em&gt;"schedule an id=42 notification for January 25 at 9:00 AM with this title and body"&lt;/em&gt; belongs to the feature, not the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The translator lives outside the service. The service just executes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what makes &lt;code&gt;NotificationService&lt;/code&gt; truly reusable across every feature you'll ever build. Today and in the future.&lt;/p&gt;

&lt;h4&gt;
  
  
  The channels enum
&lt;/h4&gt;

&lt;p&gt;Remember the problem we ended Section 4 with? Channels scattered across files, IDs hardcoded as strings in multiple places, one typo away from a silent bug.&lt;/p&gt;

&lt;p&gt;The solution is an enum. One enum, one file, one source of truth for every channel your app will ever declare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;reminders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="s"&gt;'reminders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'Reminders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="s"&gt;'Scheduled reminders from the app.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Importance&lt;/span&gt; &lt;span class="n"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every channel in your app is a typed value. When you reference &lt;code&gt;AppNotificationChannel.reminders&lt;/code&gt;, your IDE autocompletes it. If you rename it, every reference updates. If you delete it, the compiler tells you everywhere it was used. &lt;strong&gt;A typo in a channel ID is now a compile error, not a silent runtime failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding a new channel in the future is one new enum value. That's it.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;NotificationService&lt;/code&gt;, the &lt;code&gt;scheduleReminder&lt;/code&gt; method accepts a channel directly from this enum. The service uses the channel's id, name, description, and importance without knowing anything else about where the notification is coming from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleReminder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service doesn't decide which channel a notification belongs to. The caller does. The service just uses what it's given.&lt;/p&gt;

&lt;h4&gt;
  
  
  A quick preview: the stream
&lt;/h4&gt;

&lt;p&gt;There's one more thing &lt;code&gt;NotificationService&lt;/code&gt; owns that we haven't discussed yet: &lt;strong&gt;tap handling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the user taps a notification, something in your app needs to react (navigate to a screen, refresh some data, show a dialog). But taps can happen in any app state: while the app is open, while it's in the background, or when the app was completely closed and the tap is what launched it. Wiring all three of those scenarios into one place requires a mechanism that multiple parts of your app can subscribe to independently.&lt;/p&gt;

&lt;p&gt;That mechanism is a &lt;code&gt;Stream&lt;/code&gt;. The &lt;code&gt;NotificationService&lt;/code&gt; exposes a &lt;code&gt;Stream&amp;lt;String?&amp;gt;&lt;/code&gt; called &lt;code&gt;onTap&lt;/code&gt;. Whenever a notification is tapped, the payload string is pushed into this stream. Any part of your app that cares subscribes to it and reacts.&lt;/p&gt;

&lt;p&gt;We'll go deep on how streams work, and why this is the right choice here, in Section 8. For now, just know it exists, and it's one of the most important pieces of the whole system.&lt;/p&gt;

&lt;h4&gt;
  
  
  The skeleton
&lt;/h4&gt;

&lt;p&gt;Here's what &lt;code&gt;NotificationService&lt;/code&gt; looks like before we fill in any implementation. Read it slowly. Notice what's there and, just as importantly, what &lt;em&gt;isn't:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@singleton&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FlutterLocalNotificationsPlugin&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;FlutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;StreamController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_tapPayloads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;StreamController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Timezone setup, plugin initialization,&lt;/span&gt;
    &lt;span class="c1"&gt;// channel registration, cold-start handling.&lt;/span&gt;
    &lt;span class="c1"&gt;// Coming in Section 6.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleReminder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what this class does &lt;em&gt;not&lt;/em&gt; contain. No app-specific logic. No domain knowledge. No hardcoded strings. &lt;strong&gt;It receives instructions, executes them, and reports back.&lt;/strong&gt; That's the whole job.&lt;/p&gt;

&lt;p&gt;In the next section, we fill in every method. Starting with &lt;code&gt;init()&lt;/code&gt;, the most important call in the whole system, and the one that has to happen before anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 6: Wiring It Up: Inside init()
&lt;/h2&gt;

&lt;p&gt;We have a skeleton. We understand why it's designed the way it is. Now it's time to make it actually work.&lt;/p&gt;

&lt;p&gt;Everything starts with &lt;code&gt;init()&lt;/code&gt;. This is the method that bootstraps the entire notification system, timezone database, plugin, channels, tap callbacks, cold-start handling. It has to run &lt;strong&gt;once, before anything else in your app tries to schedule or display a notification.&lt;/strong&gt; That makes its placement in &lt;code&gt;main.dart&lt;/code&gt; critical.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where init() belongs in your startup sequence
&lt;/h4&gt;

&lt;p&gt;Your &lt;code&gt;main()&lt;/code&gt; function is a sequence. Each step depends on the previous one being complete. &lt;code&gt;NotificationService.init()&lt;/code&gt; slot belongs &lt;strong&gt;after Flutter is initialized, but before your app runs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;notificationService:&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;await&lt;/code&gt; is not optional. &lt;code&gt;init()&lt;/code&gt; opens the timezone database, makes platform-channel calls to native code, and creates the OS notification channel. None of that is instantaneous. If you forget the &lt;code&gt;await&lt;/code&gt; and call &lt;code&gt;runApp&lt;/code&gt; while &lt;code&gt;init()&lt;/code&gt; is still running, the first scheduling attempt will hit an uninitialized plugin and crash.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WidgetsFlutterBinding.ensureInitialized()&lt;/code&gt; must come first because the plugin uses platform channels, and platform channels require the Flutter engine to be fully bootstrapped before they can fire. If you've ever seen the &lt;em&gt;"Binding has not yet been initialized"&lt;/em&gt; error, this is what it's protecting against.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: The timezone setup
&lt;/h4&gt;

&lt;p&gt;The first thing &lt;code&gt;init()&lt;/code&gt; does is set up the timezone database. Three lines, three distinct jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1 — Timezone setup&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeTimeZones&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;localTimezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterTimezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocalTimezone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLocalLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localTimezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tz.initializeTimeZones()&lt;/code&gt; loads the entire IANA timezone database into memory. Every timezone on Earth. Every historical DST rule. This runs synchronously and needs to happen before anything else touches timezone-related code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FlutterTimezone.getLocalTimezone()&lt;/code&gt; makes a platform-channel call to the OS and gets back a &lt;code&gt;TimezoneInfo&lt;/code&gt; object. We then access its &lt;code&gt;.identifier&lt;/code&gt; property to get a string like &lt;code&gt;"Europe/Athens"&lt;/code&gt; or &lt;code&gt;"America/New_York"&lt;/code&gt;. This is async because it crosses the native bridge, even though the OS answers almost instantly, the hop itself is asynchronous.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tz.setLocalLocation(tz.getLocation(localTimezone.identifier))&lt;/code&gt; tells the timezone package: &lt;em&gt;"this is the local zone, treat it as the default when I construct TZDateTime instances."&lt;/em&gt; After this line, every &lt;code&gt;TZDateTime.now()&lt;/code&gt; call in the app is anchored to the correct local zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The order matters.&lt;/strong&gt; Database first, query device second, set local third. Swap any of them and you'll get cryptic null errors that are painful to trace.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Initializing the plugin
&lt;/h4&gt;

&lt;p&gt;Next, we set up &lt;code&gt;flutter_local_notifications&lt;/code&gt; with platform-specific settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 2 — Plugin initialization&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'@mipmap/ic_launcher'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DarwinInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;requestAlertPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;requestBadgePermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;requestSoundPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;initSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;settings:&lt;/span&gt; &lt;span class="n"&gt;initSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onDidReceiveNotificationResponse:&lt;/span&gt; &lt;span class="n"&gt;_onTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AndroidInitializationSettings&lt;/code&gt; takes the name of the notification icon resource. &lt;code&gt;'@mipmap/ic_launcher'&lt;/code&gt; points to your app's launcher icon as a fallback. It works, but Android renders it as a white silhouette in the status bar. For production, you'd generate a proper monochrome white icon and reference it as &lt;code&gt;'@drawable/ic_notification'&lt;/code&gt;. For our demo, the launcher icon is fine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DarwinInitializationSettings&lt;/code&gt; is the iOS equivalent. Notice all three permission flags are set to &lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt;. This is intentional. By default, calling &lt;code&gt;initialize()&lt;/code&gt; on iOS would immediately trigger the permission prompt. &lt;strong&gt;We don't want that.&lt;/strong&gt; We want to ask at a moment that makes sense to the user, not blindly at app launch. Setting these flags to false defers the prompt entirely until we explicitly call &lt;code&gt;requestPermissions()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onDidReceiveNotificationResponse: _onTap&lt;/code&gt; is where we register our tap callback. When the user taps a notification while the app is running or in the background, the plugin calls &lt;code&gt;_onTap&lt;/code&gt;, which pushes the payload into our stream. We'll come back to this in Section 8.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Registering channels
&lt;/h4&gt;

&lt;p&gt;Remember the channels enum from Section 5? This is where it earns its place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Step 3 — Register Android channels&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_registerChannels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;androidPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolvePlatformSpecificImplementation&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AndroidFlutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;androidPlugin&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;androidPlugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AndroidNotificationChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resolvePlatformSpecificImplementation&lt;/code&gt; returns the Android-specific plugin on Android, and &lt;code&gt;null&lt;/code&gt; on iOS. The &lt;code&gt;null&lt;/code&gt; check is our platform guard, if we're on iOS, we return immediately. iOS has no channels, so there's nothing to create.&lt;/p&gt;

&lt;p&gt;On Android, we iterate every value in &lt;code&gt;AppNotificationChannel&lt;/code&gt; and create each one. Adding a new channel in the future means adding one enum value. The registration loop handles it automatically. &lt;strong&gt;No code change needed here, ever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As we noted in Section 4, &lt;code&gt;createNotificationChannel&lt;/code&gt; is idempotent. Calling it with an ID that already exists does nothing. So running this on every app launch is completely safe, and it means channels are always in sync with whatever the enum declares.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: The cold-start scenario
&lt;/h4&gt;

&lt;p&gt;This is the one most tutorials miss entirely, and it's the source of a very specific bug: &lt;em&gt;"I tapped the notification and the app opened, but nothing happened."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's what's going on. When the app is &lt;strong&gt;completely terminated&lt;/strong&gt; and the user taps a notification, the OS launches the app fresh. The notification tap happened before our &lt;code&gt;onDidReceiveNotificationResponse&lt;/code&gt; callback was even registered, so the plugin catches it separately, holding it for us to retrieve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_handleLaunchPayload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;launchDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNotificationAppLaunchDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;launchDetails&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;didNotificationLaunchApp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;launchDetails&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;notificationResponse&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getNotificationAppLaunchDetails()&lt;/code&gt; asks the plugin: &lt;em&gt;"was this app launched by a notification tap?"&lt;/em&gt; If yes, we get the payload from the launch details and push it into our stream.&lt;/p&gt;

&lt;p&gt;But notice the &lt;code&gt;addPostFrameCallback&lt;/code&gt; wrapper. &lt;strong&gt;We can't push the payload immediately.&lt;/strong&gt; At the moment &lt;code&gt;init()&lt;/code&gt; runs, the widget tree hasn't been built yet, which means whoever is subscribed to &lt;code&gt;onTap&lt;/code&gt; for navigation purposes might not exist yet. By deferring the push to after the first frame is rendered, we guarantee that subscribers are ready to receive the event by the time it arrives.&lt;/p&gt;

&lt;p&gt;Without this, cold-start taps would arrive before anything could handle them. The stream would fire into the void. The user would land on the home screen with no indication that the notification wanted to take them somewhere specific.&lt;/p&gt;

&lt;h4&gt;
  
  
  Putting it all together
&lt;/h4&gt;

&lt;p&gt;Here's the complete &lt;code&gt;init()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Timezone setup&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeTimeZones&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;localTimezone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterTimezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocalTimezone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setLocalLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localTimezone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Plugin initialization&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'@mipmap/ic_launcher'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DarwinInitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;requestAlertPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;requestBadgePermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;requestSoundPermission:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;initSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InitializationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;androidSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;iosSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;settings:&lt;/span&gt; &lt;span class="n"&gt;initSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onDidReceiveNotificationResponse:&lt;/span&gt; &lt;span class="n"&gt;_onTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Register channels&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_registerChannels&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Handle cold-start tap&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_handleLaunchPayload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it top to bottom. It flows like a script: prepare the timezone context, wake up the plugin, declare the channels, check if we were launched by a tap. Each step depends only on what came before it. Each step is named clearly enough that you don't need to read the implementation to understand what it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's what good initialization code looks like.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now the foundation is in place. In the next section, we finally schedule something, and that's where &lt;code&gt;TZDateTime&lt;/code&gt;, notification IDs, payloads, and the actual scheduling call all come together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 7: Scheduling, Payloads, and Cancelling
&lt;/h2&gt;

&lt;p&gt;The foundation is in place. Now we write the three methods that make the whole thing actually useful.&lt;/p&gt;

&lt;h4&gt;
  
  
  Requesting permissions
&lt;/h4&gt;

&lt;p&gt;We covered &lt;em&gt;what&lt;/em&gt; these permissions are in Section 3. Here's &lt;em&gt;how&lt;/em&gt; you request them at the right moment — when the user takes an action that benefits from notifications, not at app launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;androidPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolvePlatformSpecificImplementation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AndroidFlutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;androidPlugin&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;androidPlugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestNotificationsPermission&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;iosPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolvePlatformSpecificImplementation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOSFlutterLocalNotificationsPlugin&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iosPlugin&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;iosPlugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;alert:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;badge:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;sound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;?? false&lt;/code&gt; is important. Some edge cases return &lt;code&gt;null&lt;/code&gt;, we treat any non-explicit grant as a denial to be safe. The method returns a &lt;code&gt;bool&lt;/code&gt; so the caller can decide what to do next: show an explanation, open Settings, or proceed quietly.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scheduling a notification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scheduleReminder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AppNotificationChannel&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tzFireAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TZDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;androidDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;channelDescription:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;importance:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;priority:&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;iosDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DarwinNotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;presentAlert:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;presentBadge:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;presentSound:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zonedSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;scheduledDate:&lt;/span&gt; &lt;span class="n"&gt;tzFireAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;notificationDetails:&lt;/span&gt; &lt;span class="n"&gt;NotificationDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;android:&lt;/span&gt; &lt;span class="n"&gt;androidDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;iOS:&lt;/span&gt; &lt;span class="n"&gt;iosDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;payload:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;androidScheduleMode:&lt;/span&gt; &lt;span class="n"&gt;AndroidScheduleMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exactAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth highlighting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tz.TZDateTime.from(fireAt, tz.local)&lt;/code&gt; is the conversion we talked about in Section 2. The caller passes a plain &lt;code&gt;DateTime&lt;/code&gt;. We convert it internally to a timezone-aware &lt;code&gt;TZDateTime&lt;/code&gt; anchored to the device's local zone. The caller never needs to know this happened.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle&lt;/code&gt; is the line that connects to &lt;code&gt;USE_EXACT_ALARM&lt;/code&gt; from Section 3. It tells Android: fire this alarm at the exact moment specified, even if the device is in deep sleep. Without it, the exact-alarm permission you declared is essentially unused, the alarm would still be inexact.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;presentAlert: true&lt;/code&gt; on iOS ensures the notification appears as a banner even when the app is in the foreground. Without it, iOS silently suppresses foreground notifications.&lt;/p&gt;

&lt;p&gt;One behaviour worth knowing: if you call &lt;code&gt;scheduleReminder&lt;/code&gt; with an &lt;code&gt;id&lt;/code&gt; that already has a pending notification, &lt;strong&gt;the old one is replaced.&lt;/strong&gt; No duplicates. This makes reschedules effortless, cancel-then-reschedule becomes just reschedule.&lt;/p&gt;

&lt;h4&gt;
  
  
  The payload and its limits
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;payload&lt;/code&gt; is a single &lt;code&gt;String?&lt;/code&gt; that travels with the notification and comes back to you when the user taps it. It's opaque to the service. We don't open it, we just carry it.&lt;/p&gt;

&lt;p&gt;If you need to pass structured data (a screen to navigate to, an item ID, a notification type), encode it as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;'route'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'/reminder'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'123'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And decode it when the tap arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The hard limit is 4KB for the entire notification payload.&lt;/strong&gt; That's roughly 4,000 characters, plenty for routing data and IDs, not nearly enough for full objects or images. The right pattern is: send the ID, fetch the data when the user taps. Never try to fit your entire model into a notification payload.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Notification IDs
&lt;/h4&gt;

&lt;p&gt;Every notification needs an &lt;code&gt;int&lt;/code&gt; ID. You use it to cancel or replace a specific notification later. The simplest reliable strategy: derive it deterministically from the data you already have.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'some-unique-key'&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hashCode&lt;/code&gt; gives you a stable integer for any string. &lt;code&gt;abs()&lt;/code&gt; removes the possibility of a negative value. &lt;code&gt;% 100000&lt;/code&gt; keeps it in a safe range. &lt;strong&gt;The same key always produces the same ID&lt;/strong&gt;, which means you can always cancel or replace a notification without having stored the ID separately.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cancelling
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_plugin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass the same ID you scheduled with, and the notification disappears from the OS queue. If no notification with that ID exists, this is a no-op. &lt;strong&gt;It never throws.&lt;/strong&gt; Cancel freely.&lt;/p&gt;

&lt;p&gt;Next up: the tap. What happens when the user actually presses the notification, and how the stream handles every possible app state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 8: The Tap: Where the Stream Earns Its Keep
&lt;/h2&gt;

&lt;p&gt;We've built the scheduler. Now we need to handle what happens on the other end: the user taps a notification, and your app needs to react. Navigate somewhere. Refresh some data. Show a dialog.&lt;/p&gt;

&lt;p&gt;Sounds simple. &lt;strong&gt;It isn't.&lt;/strong&gt; Because the tap can arrive in three completely different situations.&lt;/p&gt;

&lt;h4&gt;
  
  
  The three app states
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;The app is open (foreground).&lt;/strong&gt; The user taps the notification banner. The plugin fires &lt;code&gt;onDidReceiveNotificationResponse&lt;/code&gt; immediately. Our &lt;code&gt;_onTap&lt;/code&gt; callback picks it up and pushes the payload into the stream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app is in the background.&lt;/strong&gt; Same as foreground. The user taps, the OS brings the app forward, the plugin fires the callback, the stream gets the payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app was completely closed (terminated).&lt;/strong&gt; The user taps the notification. The OS launches the app fresh. But this time, &lt;code&gt;onDidReceiveNotificationResponse&lt;/code&gt; never fires, the callback wasn't registered yet when the tap happened. Instead, the plugin holds the launch payload, and &lt;code&gt;_handleLaunchPayload()&lt;/code&gt; (from Section 6) retrieves it after the first frame is rendered and pushes it into the stream.&lt;/p&gt;

&lt;p&gt;Three states. Three different paths. &lt;strong&gt;One stream that handles all of them the same way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is exactly why the stream exists. Without it, you'd need three separate wiring points in your app, each handling the tap slightly differently, each easy to forget. With it, any part of your app subscribes once and receives taps from every state automatically.&lt;/p&gt;

&lt;h4&gt;
  
  
  How the stream works
&lt;/h4&gt;

&lt;p&gt;A &lt;code&gt;StreamController&lt;/code&gt; is a pipe with two ends. The &lt;strong&gt;write end&lt;/strong&gt; is private, only &lt;code&gt;NotificationService&lt;/code&gt; can push events into it. The &lt;strong&gt;read end&lt;/strong&gt; is what we expose publicly via the &lt;code&gt;onTap&lt;/code&gt; getter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;StreamController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_tapPayloads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;StreamController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.broadcast()&lt;/code&gt; part means multiple listeners can subscribe simultaneously without conflict. Your router subscribes. Your analytics service subscribes. Neither knows the other exists. &lt;strong&gt;Both receive the event when a tap arrives.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;_onTap&lt;/code&gt; fires (foreground/background) or &lt;code&gt;_handleLaunchPayload&lt;/code&gt; runs (terminated), they both do the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;_tapPayloads&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. The event travels down the stream to every active subscriber.&lt;/p&gt;

&lt;h4&gt;
  
  
  Subscribing: where and when
&lt;/h4&gt;

&lt;p&gt;Subscribe as early as possible in your app's lifecycle — before any notification could realistically be tapped. &lt;code&gt;main.dart&lt;/code&gt;, right after &lt;code&gt;init()&lt;/code&gt;, is the right place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'route'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Navigate to the right screen based on the route.&lt;/span&gt;
      &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Navigating to: &lt;/span&gt;&lt;span class="si"&gt;$route&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;notificationService:&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;listen&lt;/code&gt; call returns a &lt;code&gt;StreamSubscription&lt;/code&gt;. In a real app you'd store this and cancel it on cleanup. In our demo, the service is alive for the app's entire lifetime, so it's fine to let it run.&lt;/p&gt;

&lt;p&gt;Notice the &lt;code&gt;addPostFrameCallback&lt;/code&gt; from Section 6 handling the terminated case. By the time the post-frame callback fires, &lt;code&gt;main()&lt;/code&gt; has already returned, &lt;code&gt;runApp&lt;/code&gt; has already built the widget tree, and your &lt;code&gt;listen&lt;/code&gt; is already active. The payload arrives to a ready subscriber. &lt;strong&gt;The timing is what makes it work.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The full picture
&lt;/h4&gt;

&lt;p&gt;Let's trace a complete notification lifecycle one final time, from scheduling to tap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User presses the button → &lt;code&gt;scheduleReminder()&lt;/code&gt; hands instructions to the OS&lt;/li&gt;
&lt;li&gt;10 seconds pass → OS fires the alarm, &lt;code&gt;AlarmManager&lt;/code&gt; / &lt;code&gt;UNUserNotificationCenter&lt;/code&gt; displays the notification&lt;/li&gt;
&lt;li&gt;User taps it → OS invokes the plugin's native callback&lt;/li&gt;
&lt;li&gt;Plugin crosses the platform channel back to Dart → &lt;code&gt;_onTap()&lt;/code&gt; fires&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_onTap()&lt;/code&gt; calls &lt;code&gt;_tapPayloads.add(payload)&lt;/code&gt; → the stream emits an event&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;listen&lt;/code&gt; handler receives the payload → decodes it → navigates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Six steps. Your code only owns steps 1, 4, 5, and 6.&lt;/strong&gt; Steps 2 and 3 belong entirely to the OS, which is exactly what Section 1 told us from the very beginning.&lt;/p&gt;

&lt;p&gt;That's the whole system. From a button press to a tap handler, built on three packages, two platform permissions, one service, and one stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you have it! What started as a simple &lt;em&gt;"schedule a notification"&lt;/em&gt; turned into a full expedition through operating system schedulers, permission layers, notification channels, timezone databases, and stream controllers. I know that's a lot. But every single piece earned its place.&lt;/p&gt;

&lt;p&gt;Let's recap what we actually learned. The &lt;strong&gt;OS owns the schedule&lt;/strong&gt;. Your app just hands instructions over and walks away. There are &lt;strong&gt;two completely separate permission gates&lt;/strong&gt;, one that controls whether notifications appear at all, and one that controls whether they appear at the right time. Mixing them up is the root cause of half the notification bugs you'll ever see. A &lt;strong&gt;&lt;code&gt;NotificationService&lt;/code&gt; that speaks in primitives&lt;/strong&gt; keeps your notification system reusable across every feature you'll ever build, not just the one that needed reminders today. And a &lt;strong&gt;broadcast stream&lt;/strong&gt; gives every part of your app a unified way to react to taps, regardless of which state the app was in when the user pressed it.&lt;/p&gt;

&lt;p&gt;Here's the bigger picture though. Notifications are not a feature. They're infrastructure. And like any infrastructure, the cost of getting them wrong is subtle, a reminder that fires an hour late, a tap that opens the wrong screen, a permission that silently blocks everything on a new Android device. Getting them right doesn't take magic. It just takes understanding what's actually happening underneath the code you write.&lt;/p&gt;

&lt;p&gt;The companion repository for this article is available on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_notifications" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone it, run it, and use it as a reference as you build your own implementation.&lt;/p&gt;

&lt;p&gt;This was Part 1, local notifications only. Part 2 will cover remote push notifications: how FCM and APNs work, how a server triggers a notification on a user's device, and how you handle the payload when the notification arrives. Everything we built today is the foundation for that. When you understand the local side, the remote side becomes much easier to reason about.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee! ☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, go ahead and experiment, dig into the source, and don't be afraid to follow the code wherever it leads. Happy coding, Flutter friends!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Context Behind the Context: How Flutter Navigation Really Works</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Mon, 30 Mar 2026 14:37:51 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/the-context-behind-the-context-how-flutter-navigation-really-works-4j4</link>
      <guid>https://dev.to/thanasistraitsis/the-context-behind-the-context-how-flutter-navigation-really-works-4j4</guid>
      <description>&lt;p&gt;Have you ever wondered how navigation works in Flutter? I mean, you've typed &lt;code&gt;Navigator.of(context).push(...)&lt;/code&gt; dozens of times. It works, you ship it, you move on. But have you ever stopped to ask, &lt;em&gt;how actually does this work? what exactly is context, and why does Navigator need it at all?&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;This is what we are going to cover in this article. We will explore the navigation system of Flutter and how everything works from the moment you "click" on the button from Screen A, until you land on Screen B.  You know me...we will uncover every little secret that is hidden underneath the navigation system. And that's why, we need to start by explaining a few things about the Flutter's Widget Tree in general.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 1: The Widget Tree and the others...
&lt;/h2&gt;

&lt;p&gt;Most Flutter developers carry a quiet misconception: that &lt;code&gt;BuildContext&lt;/code&gt; is just a handle to their widget. A reference. A fancy &lt;strong&gt;&lt;code&gt;this&lt;/code&gt;&lt;/strong&gt;. Well... it isn't. And that misunderstanding is the root cause of some of the most confusing navigation bugs you'll ever encounter. &lt;/p&gt;

&lt;p&gt;A Widget in Flutter is &lt;strong&gt;immutable&lt;/strong&gt;. It's a lightweight description of what you want the UI to look like. &lt;strong&gt;Cheap to create, cheap to discard.&lt;/strong&gt; Flutter rebuilds widget trees constantly. But if widgets were the real runtime objects, that would be expensive and lossy.&lt;/p&gt;

&lt;p&gt;So Flutter doesn't use them as runtime objects. Instead, it maintains three parallel trees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Widget tree:&lt;/strong&gt; your code. Immutable descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Element tree:&lt;/strong&gt; the live runtime instances. This is where &lt;code&gt;state&lt;/code&gt; lives and where &lt;code&gt;BuildContext&lt;/code&gt; comes from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The RenderObject tree:&lt;/strong&gt; the layout and painting engine.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The most important line in Flutter's source code is this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Element&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;BuildContext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;BuildContext&lt;/code&gt; you pass around is an &lt;strong&gt;element&lt;/strong&gt;, NOT a reference to one element. It's the live runtime node, sitting at a specific position in that tree. So we need to be very careful on how we use it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Context is positional, not global
&lt;/h4&gt;

&lt;p&gt;When your &lt;code&gt;build&lt;/code&gt; method receives a &lt;code&gt;BuildContext&lt;/code&gt;, Flutter is handing you your element's location in the tree. That location is the key, so we can start our journey looking upwards in the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyButton&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 'context' is this element's position in the tree.&lt;/span&gt;
    &lt;span class="c1"&gt;// Navigator.of() will walk *upward* from here.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Go'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Navigator.of(context)&lt;/code&gt; doesn't search the whole app. It starts at this node and walks up the element tree until it finds a &lt;code&gt;NavigatorState&lt;/code&gt; ancestor. The context is the starting point of that walk, &lt;strong&gt;which means passing the wrong context, or one from above the Navigator, breaks everything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is also why you can't fabricate a context. There's no &lt;code&gt;BuildContext()&lt;/code&gt; constructor. It has to be a real, mounted element that exists in the live tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 2: How Flutter Finds the Navigator
&lt;/h2&gt;

&lt;p&gt;In the previous section, we established that &lt;code&gt;BuildContext&lt;/code&gt; is a &lt;strong&gt;position&lt;/strong&gt; in the element tree, and that &lt;code&gt;Navigator.of(context)&lt;/code&gt; starts at that position and walks upward looking for a navigation ancestor. Time to make that concrete. What exactly is it looking for, and what happens when it finds it?&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;MaterialApp&lt;/code&gt; initialises, it quietly inserts a &lt;strong&gt;&lt;code&gt;Navigator&lt;/code&gt;&lt;/strong&gt; widget near the top of the tree. Like any StatefulWidget, Navigator is just the blueprint. The real runtime object is its state class: &lt;code&gt;NavigatorState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NavigatorState&lt;/code&gt; is what actually matters. It owns the route stack, the ordered list of screens your user has navigated through, and it exposes the methods you call to manipulate it: &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pop&lt;/code&gt;, &lt;code&gt;pushReplacement&lt;/code&gt;, and so on. When you call &lt;code&gt;Navigator.of(context)&lt;/code&gt;, you're not getting the widget. You're getting the &lt;strong&gt;NavigatorState.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The ancestor walk
&lt;/h4&gt;

&lt;p&gt;Under the hood, &lt;code&gt;Navigator.of(context&lt;/code&gt;) calls a single method on the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAncestorStateOfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a linear walk. Starting from the element that owns the context, Flutter climbs the element tree one node at a time, checking each ancestor to see if it holds a &lt;code&gt;NavigatorState&lt;/code&gt;. The moment it finds one, it stops and returns it. If it reaches the root without finding one, it throws.&lt;/p&gt;

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

&lt;p&gt;The walk is &lt;strong&gt;O(depth)&lt;/strong&gt;, it visits every node between the call site and the &lt;code&gt;Navigator&lt;/code&gt;. But this is happening in microseconds so you don't have to worry about any performance issue no matter how "deep" your starting context lives.&lt;/p&gt;

&lt;h4&gt;
  
  
  What NavigatorState actually owns
&lt;/h4&gt;

&lt;p&gt;Once the walk completes and returns the &lt;code&gt;NavigatorState&lt;/code&gt;, you have access to the object that manages your entire navigation history. At its core, &lt;code&gt;NavigatorState&lt;/code&gt; maintains two things. The &lt;strong&gt;Route Stack&lt;/strong&gt; and the &lt;strong&gt;Overlay Stack&lt;/strong&gt;. Understanding what each of these does, and how they work together, is exactly what Section 3 is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 3: The Route Stack, the Overlay, and their communication
&lt;/h2&gt;

&lt;p&gt;Before we jump into the actual Route Stack, we need to talk about what lives inside it. When you call &lt;code&gt;Navigator.of(context).push(...)&lt;/code&gt;, you don't pass a widget directly. You pass a &lt;code&gt;Route&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Open second screen'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;---- This one!&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SecondScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Route&lt;/code&gt; is the object that wraps your screen. It carries three things: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the builder that produces your screen's widget tree&lt;/li&gt;
&lt;li&gt;the transition animation that plays when the route enters and exits&lt;/li&gt;
&lt;li&gt;the settings, things like the route name and any arguments passed to it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Think of it this way: if your screen is an actor, the &lt;code&gt;Route&lt;/code&gt; is the contract. It defines &lt;strong&gt;when&lt;/strong&gt; the actor enters, &lt;strong&gt;how&lt;/strong&gt; they enter, and &lt;strong&gt;what&lt;/strong&gt; they bring with them.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Route Stack
&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;Route Stack&lt;/strong&gt; is a plain ordered list of &lt;code&gt;Route&lt;/code&gt; objects living inside &lt;code&gt;NavigatorState&lt;/code&gt;. The last item in the list is always what the user sees. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;push&lt;/code&gt; appends to it &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pop&lt;/code&gt; removes from the end &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pushReplacement&lt;/code&gt; removes the last item and appends a new one in a single operation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what that list looks like after a typical user journey:&lt;/p&gt;

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

&lt;p&gt;Notice that &lt;code&gt;HomeRoute&lt;/code&gt; and &lt;code&gt;ProfileRoute&lt;/code&gt; are never destroyed when something is pushed on top of them. They stay mounted in the stack the entire time. This is intentional, it's what makes the back swipe gesture feel instant, and it's what lets you &lt;code&gt;pop&lt;/code&gt; back without any rebuild cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But be very careful.&lt;/strong&gt; You don't want to overload your applications with hundreds of Routes. It's perfect when you navigate a couple of screens ahead, but if you use &lt;code&gt;push&lt;/code&gt; for every navigation you perform, you will end up with performance issues due to the infinite stacks of routes you keep alive during a session.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Overlay
&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;Overlay&lt;/strong&gt; is NavigatorState's rendering layer. While the Route Stack is the &lt;em&gt;logical list&lt;/em&gt; of where you are in the app, the Overlay is the &lt;em&gt;visual layer&lt;/em&gt; that actually puts pixels on screen.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;Route&lt;/code&gt; in the stack has a corresponding &lt;code&gt;OverlayEntry&lt;/code&gt;, which is a slot in the Overlay that renders that route's widget tree. The Overlay stacks these entries in z-order, so the topmost route in the stack always renders on top of everything else.&lt;/p&gt;

&lt;p&gt;This separation is what makes transition animations work. When you push a new route, both the outgoing and incoming routes have active OverlayEntry objects simultaneously. Flutter animates between them at the Overlay level. The old screen slides or fades out while the new one slides or fades in, and only once the animation completes does the old entry become inactive.&lt;/p&gt;

&lt;h4&gt;
  
  
  What push() actually does
&lt;/h4&gt;

&lt;p&gt;Now we can trace the full picture. When you write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SettingsScreen&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NavigatorState&lt;/code&gt; executes five steps in sequence:&lt;/p&gt;

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

&lt;p&gt;The footnote at the bottom is worth highlighting: &lt;code&gt;pop&lt;/code&gt; is literally this sequence in reverse. The animation controller plays backwards, the old entry reactivates, and the top route gets removed from &lt;code&gt;_history&lt;/code&gt;. No rebuilds, no reconstruction. Just the same entries, animated the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 4: Where Context-Based Navigation Breaks Down
&lt;/h2&gt;

&lt;p&gt;Everything we've covered so far assumes one thing: that you have a valid, mounted &lt;code&gt;BuildContext&lt;/code&gt; at the exact moment you want to navigate. In simple apps, that assumption holds. &lt;strong&gt;In real apps, it breaks.&lt;/strong&gt; And it breaks in ways that are subtle enough to slip past code review and only surface in production.&lt;/p&gt;

&lt;p&gt;There are two situations where Flutter developers hit this wall repeatedly. I am pretty sure that you've been there before, and it's probably one of the reasons you are here reading this blog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The broken pattern — seen in both scenarios&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthCubit&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Cubit&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AuthCubit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthInitial&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// Scenario 1: context passed into business logic&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// async gap&lt;/span&gt;

    &lt;span class="c1"&gt;// By the time we resume here, three things may have gone wrong:&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. The widget that passed this context was disposed during the await&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. The element this context points to is now detached from the tree&lt;/span&gt;
    &lt;span class="c1"&gt;// 3. Navigator.of(context) starts a walk from a ghost node — crash&lt;/span&gt;
    &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushReplacementNamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/home'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Scenario 2: context passed into a service&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_clearSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Same problem, this context came from somewhere in the widget tree,&lt;/span&gt;
    &lt;span class="c1"&gt;// but we're now inside a plain Dart class with no lifecycle awareness.&lt;/span&gt;
    &lt;span class="c1"&gt;// There is no 'mounted' check available here. No safety net at all.&lt;/span&gt;
    &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushNamedAndRemoveUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comments tell the story, but let's make the root cause explicit.&lt;/p&gt;

&lt;h4&gt;
  
  
  Navigating from a Bloc or a Service
&lt;/h4&gt;

&lt;p&gt;This is the most common one. You have a &lt;code&gt;Cubit&lt;/code&gt; handling login. When the login succeeds, you want to navigate to the home screen. The natural instinct is to pass the &lt;code&gt;BuildContext&lt;/code&gt; into the cubit and call &lt;code&gt;Navigator.of(context)&lt;/code&gt; from inside it.&lt;/p&gt;

&lt;p&gt;It feels reasonable. It compiles. &lt;strong&gt;And it will eventually crash or misbehave.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BuildContext&lt;/code&gt; is a widget tree concept. It belongs to an element that lives and dies with the widget lifecycle. When you pass it into a &lt;code&gt;Cubit&lt;/code&gt; or a &lt;code&gt;Service&lt;/code&gt;, you are smuggling a tree-bound object into a layer that has no knowledge of the tree, no lifecycle hooks, and no way to check whether that object is still valid.&lt;/p&gt;

&lt;p&gt;At this point, some of you may feel confident. You are like: &lt;em&gt;"What is this guy talking about? Didn't he read the latest news from &lt;a href="https://docs.flutter.dev/release/release-notes/release-notes-3.7.0" rel="noopener noreferrer"&gt;Flutter release 3.7.0&lt;/a&gt;? I can use &lt;code&gt;mounted&lt;/code&gt; property because from now on it's accesible from Buildcontext."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthCubit&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Cubit&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// The Async Gap&lt;/span&gt;

    &lt;span class="c1"&gt;// Yes, Flutter now lets you do this:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushReplacementNamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/home'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you think that &lt;code&gt;if (context.mounted)&lt;/code&gt; is the perfect solution, &lt;strong&gt;you are wrong.&lt;/strong&gt; And there are a few reasons why you shouldn'y use this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tight Coupling:&lt;/strong&gt; Your &lt;code&gt;AuthCubit&lt;/code&gt; is now impossible to unit test without mocking the entire Flutter framework. You’ve successfully welded your business logic to your widget tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Wrong House" Problem:&lt;/strong&gt; A Cubit is often shared. If you pass a context from Screen A, but the user has already navigated to Screen B, &lt;code&gt;context.mounted&lt;/code&gt; might still be true, but you’re now performing navigation logic from a "stale" location in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle Ignorance:&lt;/strong&gt; A Cubit or a Service class doesn't know about the Widget's lifecycle. It doesn't know why a widget was unmounted. By the time your await finishes, the entire navigation intent might be irrelevant, but your service is still trying to drive the car.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To sum up, the &lt;code&gt;BuildContext&lt;/code&gt; is a "handle" for the &lt;strong&gt;Element Tree&lt;/strong&gt;. It belongs to the &lt;strong&gt;View&lt;/strong&gt;. When you pass it into a Cubit or a Service, you aren't just passing an object, you're leaking a dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 5: The NavigationService Pattern
&lt;/h2&gt;

&lt;p&gt;Everything in the previous section points to the same conclusion: the problem isn't your code, it's the dependency. As long as navigation requires a &lt;code&gt;BuildContext&lt;/code&gt; → it requires a live element → which requires a mounted widget → which requires being inside the widget lifecycle. Break any link in that chain and navigation breaks with it.&lt;br&gt;
The &lt;code&gt;NavigationService&lt;/code&gt; pattern severs that dependency entirely. Instead of walking the element tree to find NavigatorState, &lt;strong&gt;you hold a permanent direct reference to it&lt;/strong&gt;, one that is valid for the entire lifetime of the app, not just the lifetime of a widget.&lt;/p&gt;
&lt;h4&gt;
  
  
  The GlobalKey: a reference that survives the tree
&lt;/h4&gt;

&lt;p&gt;A &lt;code&gt;GlobalKey&amp;lt;NavigatorState&amp;gt;&lt;/code&gt; is Flutter's mechanism for holding a stable reference to a specific State object across the entire app. Unlike a &lt;code&gt;BuildContext&lt;/code&gt;, which is tied to an element's position in the tree, a &lt;strong&gt;GlobalKey&lt;/strong&gt; is registered in a global registry the moment its associated widget is mounted, and it stays there until the widget is permanently removed. What this means in practice: &lt;code&gt;navigatorKey.currentState&lt;/code&gt; gives you direct access to the NavigatorState at any time, from anywhere in your codebase. &lt;strong&gt;No walk. No context. No lifecycle dependency.&lt;/strong&gt; As long as &lt;code&gt;MaterialApp&lt;/code&gt; is in the tree (which is always, for a running app), the key is valid.&lt;/p&gt;

&lt;p&gt;This is the mechanical reason the pattern works. It's not a workaround or a hack, it's using a first-class Flutter API exactly as it was designed to be used. Let's look at the code:&lt;/p&gt;
&lt;h4&gt;
  
  
  Building the NavigationService
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;NavigationService&lt;/code&gt; is straightforward. It's a singleton that wraps the key and exposes clean navigation methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NavigationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NavigationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;NavigationService&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NavigationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;navigatorKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

  &lt;span class="n"&gt;NavigatorState&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;_navigator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;navigatorKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentState&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// implement pushReplacement, pushNamed, pushNamedAndRemoveUntil and everything else&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  MaterialApp
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;navigatorKey:&lt;/span&gt; &lt;span class="n"&gt;NavigationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;navigatorKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;initialRoute:&lt;/span&gt; &lt;span class="s"&gt;'/home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;'/home'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;'/login'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LoginScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s"&gt;'/profile'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we set our &lt;code&gt;GlobalKey&lt;/code&gt; into our MaterialApp, and we are good to go. All we have to do right now, is use our &lt;code&gt;NavigationService&lt;/code&gt; for every navigation we want to execute in our app. Nothing will break, nothing will crash, and there is no need to search all the ancestors until you reach this destination.&lt;/p&gt;

&lt;h4&gt;
  
  
  Simple Cubit Example
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthCubit&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Cubit&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AuthCubit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AuthRepository&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_authRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthInitial&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AuthRepository&lt;/span&gt; &lt;span class="n"&gt;_authRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Direct call — always safe, no mounted check needed&lt;/span&gt;
    &lt;span class="n"&gt;NavigationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushNamedAndRemoveUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/home'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;NavigationService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushNamedAndRemoveUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;NavigationService&lt;/code&gt; in place, your architecture has a clean separation: widgets own the element tree, business logic owns state, services own data, and navigation is a shared utility that any layer can call without crossing into another layer's territory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you have it! What started as a simple &lt;code&gt;Navigator.of(context).push(...)&lt;/code&gt; turned into a full expedition through Flutter's internals. I know that we usually don't really care about all these things we use in our everyday-code. We just type 2-3 lines that we know, we see that it works, and we move on. But this topic felt really interesting to me and I believe you can benefit a lot by searching through its core.&lt;/p&gt;

&lt;p&gt;So... what did we learn today? We learned that &lt;code&gt;BuildContext&lt;/code&gt; is not just a reference to your widget, it's a live node in the element tree, with a position, a lifetime, and a very specific job. We traced how &lt;code&gt;Navigator.of(context)&lt;/code&gt; walks that tree upward, node by node, until it finds a &lt;code&gt;NavigatorState&lt;/code&gt; to grab onto. We opened up NavigatorState itself and saw the two things it owns &lt;strong&gt;(the Route Stack and the Overlay)&lt;/strong&gt; and we watched exactly how a single &lt;code&gt;push()&lt;/code&gt; call mutates both of them in five precise steps. &lt;/p&gt;

&lt;p&gt;And finally, we saw where the context-based approach hits its limits, and how a NavigationService backed by a GlobalKey lets you navigate from anywhere in your app. More importantly, we didn't just learn how to navigate. We learned why it works the way it does. And that's the kind of understanding that sticks, the kind that makes the next confusing bug a little less mysterious, and the next architecture decision a little more confident.&lt;/p&gt;

&lt;p&gt;One last thing, we didn't cover &lt;a href="https://pub.dev/packages/go_router" rel="noopener noreferrer"&gt;go_router&lt;/a&gt; in this article, and that was intentional. This was about the "what" and the "how" underneath the hood. But if you're starting a new Flutter app, go_router is absolutely worth your time. It's the recommended routing solution in the Flutter documentation for a reason, and once you use it, you can't go back.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, go ahead and experiment, dig into the framework source, and don't be afraid to follow the code wherever it leads. Happy coding, Flutter friends!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>navigation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Flutter App Taking Too Long to Start? Here's What You're Doing Wrong</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Fri, 27 Feb 2026 07:27:45 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/flutter-app-taking-too-long-to-start-heres-what-youre-doing-wrong-2a05</link>
      <guid>https://dev.to/thanasistraitsis/flutter-app-taking-too-long-to-start-heres-what-youre-doing-wrong-2a05</guid>
      <description>&lt;p&gt;If you've ever launched your app and stared at a white screen for what feels like an eternity before anything shows up, you're not alone. I've been there too. Recently, while working on a project that had grown quite a bit in complexity, I noticed that the startup time had become... let's say, &lt;em&gt;uncomfortably long&lt;/em&gt;. The splash screen wasn't even getting a chance to say hello before the app had already made a bad first impression. That's when I realized I had a problem, and more importantly, that I had been thinking about app initialization the wrong way.&lt;/p&gt;

&lt;p&gt;So in this article, we're going to fix that. We'll go through &lt;strong&gt;7 practical tips&lt;/strong&gt; that will help you dramatically reduce your Flutter app's startup time, eliminate that white screen, and give your users the smooth, snappy experience they deserve. Some of these are quick wins, others require a small shift in how you think about your app's startup flow. But all of them are worth it. &lt;/p&gt;

&lt;h2&gt;
  
  
  1. Move Heavy Work After runApp()
&lt;/h2&gt;

&lt;p&gt;This is the most important one. You can skip the rest of the article and move on with your life after reading this (please don't).  The root of most startup performance problems in Flutter comes down to a single habit: loading everything before calling &lt;code&gt;runApp()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Think about what happens when your app launches. Flutter needs to paint the first frame on the screen as fast as possible. But if your &lt;code&gt;main()&lt;/code&gt; function is busy initializing databases, fetching remote configs, setting up services, and registering half the universe before &lt;code&gt;runApp()&lt;/code&gt; even gets called, Flutter has no choice but to wait. And while it waits... your user stares at a white screen.&lt;/p&gt;

&lt;p&gt;The fix? &lt;strong&gt;Be ruthless about what truly needs to be ready before the first frame.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask yourself this question for every single thing you initialize: &lt;em&gt;"Will the app crash or look broken without this on the very first frame?"&lt;/em&gt; If the answer is no, it doesn't belong before &lt;code&gt;runApp()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In reality, the list of things that genuinely need to be ready before &lt;code&gt;runApp()&lt;/code&gt; is much shorter than most people think. Here's what actually makes the cut:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Theme&lt;/strong&gt; → your app needs to know which colors and fonts to use from the very first pixel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language/Locale&lt;/strong&gt; → same idea, the UI needs to know which language to render immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication state&lt;/strong&gt; → you need to know if the user is logged in or not before deciding which screen to show first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's pretty much it. Everything else, your analytics setup, your remote config, your ad services, your connectivity listeners, can wait. Initialize them after &lt;code&gt;runApp()&lt;/code&gt;, in the background, while your user is already looking at something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Compress Your Assets
&lt;/h2&gt;

&lt;p&gt;Are you still here? Great! This one is a quick win that most developers completely overlook. While you're focusing on the code side of initialization, your assets might be quietly working against you behind the scenes.&lt;/p&gt;

&lt;p&gt;Think about your splash screen image. It's the very first visual your app needs to load, so if that image is a 2MB PNG that Flutter has to decode before painting anything, you've already lost precious milliseconds before the user sees a single pixel. The same goes for any font files or images that are part of your initial UI.&lt;/p&gt;

&lt;p&gt;The fix here is simple: &lt;strong&gt;compress everything that gets loaded at startup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For images, tools like &lt;a href="https://tinypng.com/" rel="noopener noreferrer"&gt;TinyPNG&lt;/a&gt; or &lt;a href="https://squoosh.app/" rel="noopener noreferrer"&gt;Squoosh&lt;/a&gt; can reduce file sizes dramatically, often by &lt;strong&gt;60-80%&lt;/strong&gt;, with little to no visible quality difference. For your splash screen specifically, consider using a simple vector image (SVG) or even a plain color with your logo instead of a heavy raster image. Flutter's native splash screen supports this out of the box and it's blazing fast.&lt;/p&gt;

&lt;p&gt;For fonts, make sure you're only bundling the font weights you actually use. It's very easy to include an entire font family with 10 different weights when your app only uses 2 of them. Every unnecessary file is extra work Flutter has to do before your app feels alive.&lt;/p&gt;

&lt;p&gt;A good rule of thumb: &lt;strong&gt;anything that gets loaded before or during your splash screen should be as lightweight as possible.&lt;/strong&gt; Your goal is to make Flutter's job as easy as it can be in those critical first moments.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Show a Screen Immediately, Load the Real App Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;Here's the idea behind this one. Instead of making the user wait until every single thing is ready before showing anything, you show a simple screen instantly and then load the real app behind the scenes. By the time the user has registered what they're looking at, your app is already done initializing.&lt;/p&gt;

&lt;p&gt;Think of it like a restaurant. A good waiter doesn't make you stand outside until your table is fully set, the food is cooked, and the wine is poured. They seat you immediately, hand you the menu, and let the kitchen do its thing in the background. You're already inside, you're comfortable, and everything else arrives naturally.&lt;/p&gt;

&lt;p&gt;In Flutter, this pattern looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Load only the essential data&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;setupEssentialServices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AppLoader&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AppLoader&lt;/code&gt; is not your real &lt;code&gt;MaterialApp&lt;/code&gt;. It's a lightweight widget that shows something instantly, maybe your splash screen or a simple branded loading screen, while the rest of your services finish initializing in the background. Once everything is ready, you swap it out for your real &lt;code&gt;MaterialApp&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppLoader&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AppLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppLoader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_AppLoaderState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_AppLoaderState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppLoader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_loadRemainingServices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_loadRemainingServices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;setupRemainingServices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;SplashScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RealApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty of this approach is that your user never sees a white screen. They land on your splash screen almost instantly, and by the time they're done admiring your beautiful logo, the app is fully ready to go.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  4. Use compute() for Heavy Work
&lt;/h2&gt;

&lt;p&gt;Let's talk about something that trips up a lot of Flutter developers: the &lt;strong&gt;UI thread&lt;/strong&gt;. Flutter runs everything, your widgets, your animations, your build methods, on a single thread called the &lt;strong&gt;main isolate&lt;/strong&gt;. Think of it as a single chef in a kitchen. As long as the orders are small and simple, everything runs smoothly. But the moment you ask that chef to also butcher a whole cow, debone a fish, and bake a soufflé at the same time, the orders start piling up and the whole kitchen grinds to a halt.&lt;/p&gt;

&lt;p&gt;In Flutter terms, that "grinding to a halt" is what we call jank. Dropped frames, frozen animations, an unresponsive UI. And it often happens right at startup, when you're doing things like parsing a large JSON response, processing a big list of data, or running some heavy computation to set up your initial app state. &lt;strong&gt;So what's the solution?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dart actually supports multiple isolates, which are independent threads of execution that don't share memory with each other. Think of it as hiring a second chef who works in a completely separate kitchen. They can handle the heavy lifting without ever interfering with the main kitchen's flow.&lt;/p&gt;

&lt;p&gt;This is exactly where &lt;code&gt;compute()&lt;/code&gt; comes in. It's Flutter's friendly wrapper around isolates that makes the whole thing approachable. You simply hand it a function and some data, it spins up a separate isolate, runs your function there, and hands you back the result. The main thread never feels a thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A top-level or static function (required for compute)&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WordModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;parseWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WordModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Using compute() to run it off the main thread&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parseWords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawJsonString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the function you pass to &lt;code&gt;compute()&lt;/code&gt; must be a top-level or static function. This is because isolates don't share memory, so the function needs to be accessible without any context from the main isolate.&lt;br&gt;
When should you actually use this?&lt;/p&gt;

&lt;p&gt;Not everything needs &lt;code&gt;compute()&lt;/code&gt;. Spinning up an isolate has its own small overhead, so using it for trivial work would actually make things slower. A good rule of thumb is to reach for &lt;code&gt;compute()&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're parsing a large JSON file or dataset&lt;/li&gt;
&lt;li&gt;You're doing heavy string manipulation or encryption&lt;/li&gt;
&lt;li&gt;You're processing images or running complex algorithms&lt;/li&gt;
&lt;li&gt;Any operation that takes more than a few milliseconds and makes your UI feel laggy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything quick and lightweight, just run it normally on the main thread. The chef can handle a simple sandwich without any help. &lt;/p&gt;
&lt;h2&gt;
  
  
  5. Use Lazy Loading for Your Dependencies
&lt;/h2&gt;

&lt;p&gt;Let's talk about dependency injection and a mistake that's very easy to make without even realizing it. When you set up your app's services and dependencies at startup, there's a natural temptation to initialize everything upfront. It feels organized, it feels safe, it feels like you're in control. But here's the problem: most of those services won't be needed until much later in the app's lifecycle. Some of them might not even be needed at all during a particular session.&lt;/p&gt;

&lt;p&gt;Initializing a service means allocating memory, running setup code, and sometimes even making network calls or reading from disk. If you're doing all of that for 15 different services before your app even shows the first screen, you're essentially paying a cost you don't need to pay yet. This is where &lt;strong&gt;lazy loading&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of initializing a service immediately when you register it, you tell your app &lt;em&gt;"create this only when someone actually asks for it for the first time."&lt;/em&gt; Until that moment, it doesn't exist, it doesn't consume memory, and it doesn't slow down your startup. The difference looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Eager initialization — created immediately, whether you need it or not&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;analyticsService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnalyticsService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Lazy initialization — created only when first accessed&lt;/span&gt;
&lt;span class="n"&gt;AnalyticsService&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_analyticsService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;AnalyticsService&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;analyticsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_analyticsService&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="n"&gt;AnalyticsService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_analyticsService&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small change in how you think about your dependencies, but the impact on startup time can be significant, especially as your app grows and the number of services increases.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use Future.wait() for Parallel Async Work
&lt;/h2&gt;

&lt;p&gt;Alright, this one is all about efficiency. Let's say you have 3 services that need to be initialized at startup. The most natural way to write this is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initializeAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks clean and readable. But here's the problem: these three operations run &lt;strong&gt;sequentially&lt;/strong&gt;. One after the other. &lt;code&gt;initializeLanguage()&lt;/code&gt; doesn't even start until &lt;code&gt;initializeTheme()&lt;/code&gt; is completely done. And &lt;code&gt;initializeAuth()&lt;/code&gt; sits there waiting for both of them to finish before it even gets to wake up.&lt;/p&gt;

&lt;p&gt;Cooking references are never enough... Imagine you need to boil three separate pots of water. The sequential approach would be: boil the first pot, wait until it's done, then start the second pot, wait until it's done, then start the third. That's... not how anyone actually cooks. You put all three pots on the stove at the same time and let them all boil together. &lt;code&gt;Future.wait()&lt;/code&gt; is exactly that. It takes a list of futures and runs them &lt;strong&gt;all at the same time&lt;/strong&gt;, then waits until every single one of them is done before moving forward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await Future.wait([
  initializeTheme(),
  initializeLanguage(),
  initializeAuth(),
]);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The total time is now determined by the &lt;strong&gt;slowest&lt;/strong&gt; operation, not the sum of all of them. If each operation takes 300ms sequentially that's 900ms total. With &lt;code&gt;Future.wait()&lt;/code&gt;, it's just 300ms. That's a 3x improvement with a one-line change&lt;/p&gt;

&lt;h4&gt;
  
  
  But wait, when can you actually use this?
&lt;/h4&gt;

&lt;p&gt;This is the important part that a lot of articles skip over. &lt;code&gt;Future.wait()&lt;/code&gt; is not a magic wand you can wave over every initialization call. It only works safely when your &lt;strong&gt;services are completely independent of each other.&lt;/strong&gt; Think about it. If &lt;code&gt;initializeAuth()&lt;/code&gt; needs the result of &lt;code&gt;initializeDatabase()&lt;/code&gt; to do its job, you can't run them in parallel. Auth would try to use a database that isn't ready yet and everything would fall apart.&lt;/p&gt;

&lt;p&gt;It's a very powerful tool, but it needs to be used thoughtfully!&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Avoid Synchronous Work in main()
&lt;/h2&gt;

&lt;p&gt;We've talked a lot about async operations so far, but there's another category of startup killers that's even more sneaky: &lt;strong&gt;synchronous work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why it's sneaky. When you &lt;code&gt;await&lt;/code&gt; something async, Flutter at least has the opportunity to do other things while it waits. But synchronous code is different. It runs from start to finish without ever letting go of the thread. No pauses, no breaks, no mercy. The UI thread is completely blocked until every last line is done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'config.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsStringSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Synchronous file read&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Heavy JSON parsing&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;processSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Complex computation&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those lines is holding the UI thread hostage. Flutter cannot paint a single pixel until all of that is done. The user sees white. The rule here is simple: &lt;strong&gt;if it's heavy and it's synchronous, it has no business being in &lt;code&gt;main()&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The solution depends on what the work actually is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If it's I/O work:&lt;/strong&gt; (reading files, accessing disk), swap the synchronous version for its async counterpart:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Blocks the thread completely&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'config.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsStringSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Frees the thread while waiting&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'config.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If it's heavy computation&lt;/strong&gt; (parsing, processing, transforming data), this is exactly the case for &lt;code&gt;compute()&lt;/code&gt; that we covered in &lt;strong&gt;Tip #4.&lt;/strong&gt; Ship it off to a separate isolate and let the main thread breathe:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Heavy parsing blocking the main thread&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseHeavyConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Same work, different thread&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parseHeavyConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;main()&lt;/code&gt; function should be lean, fast, and humble. It should do the minimum amount of work needed to get something on the screen, and then get out of the way.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There you have it, my friends! Seven practical tips that can completely transform the way your app feels from the moment it launches. Let's do a quick recap of everything we covered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move heavy work after &lt;code&gt;runApp()&lt;/code&gt; — be ruthless about what truly needs to be ready before the first frame&lt;/li&gt;
&lt;li&gt;Compress your assets — don't let a heavy splash screen image undo all your hard work&lt;/li&gt;
&lt;li&gt;Show a screen immediately — perceived performance is just as important as real performance&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;compute()&lt;/code&gt; for heavy work — stop blocking the UI thread and let isolates do the heavy lifting&lt;/li&gt;
&lt;li&gt;Use lazy loading for your dependencies — don't pay for services you're not using yet&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Future.wait()&lt;/code&gt; for parallel async work — stop waiting in line when you can run everything at the same time&lt;/li&gt;
&lt;li&gt;Avoid synchronous work in &lt;code&gt;main()&lt;/code&gt; — synchronous code is a silent killer that blocks the thread completely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the bigger picture though. All seven of these tips share the same underlying philosophy. &lt;strong&gt;Your app should do the minimum amount of work needed to show something to the user as fast as possible, and defer everything else.&lt;/strong&gt; Once you start thinking that way, you'll naturally write faster, more efficient Flutter apps without even trying. The white screen is not inevitable. It's a symptom of not prioritizing what truly matters at startup. And now that you know better, you can fix it.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Flutter Sticky Bottom Button: Beyond the FloatingActionButton</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Wed, 18 Feb 2026 23:05:32 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/flutter-sticky-bottom-button-beyond-the-floatingactionbutton-2i7o</link>
      <guid>https://dev.to/thanasistraitsis/flutter-sticky-bottom-button-beyond-the-floatingactionbutton-2i7o</guid>
      <description>&lt;p&gt;Flutter gives us the &lt;code&gt;FloatingActionButton&lt;/code&gt;, and don’t get me wrong, it’s great.&lt;/p&gt;

&lt;p&gt;But sometimes… it’s just not enough.&lt;/p&gt;

&lt;p&gt;Sometimes we want more control over how and when a button appears. Maybe we want it to stick to the bottom of the screen only for a specific section. And when we finally reach the bottom of that section, we want it to stay there, in its natural position, without floating awkwardly above the content.&lt;/p&gt;

&lt;p&gt;There are countless scenarios where this behavior makes sense. I won’t limit your imagination with just one. Today, I’ll show you how to create a smart sticky button, one that stays pinned exactly as long as you want it to. And more importantly, we’re not just going to build it. &lt;strong&gt;We’re going to understand every single detail behind it.&lt;/strong&gt; By the end of this article, you’ll be able to take this pattern and adapt it to anything you need. Let’s begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are going to build
&lt;/h2&gt;

&lt;p&gt;Before we dive into the code, let’s first see the final result. Here's a preview:&lt;/p&gt;

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

&lt;p&gt;What exactly is going on here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the button is visible in its natural position → nothing special happens.&lt;/li&gt;
&lt;li&gt;As soon as you scroll and the button moves off screen → it smoothly pins itself to the bottom.&lt;/li&gt;
&lt;li&gt;When you scroll back down and reach the button’s original position → the pinned version disappears and the inline one takes over again. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No layout shifts. No jumpy behavior. No weird overlap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the best part? We’re not using any external packages. We will use all the available tools from Flutter. We need &lt;code&gt;Stack&lt;/code&gt;, &lt;code&gt;ScrollController&lt;/code&gt; and &lt;code&gt;GlobalKey&lt;/code&gt; together with some math. Enough with the preview, let's get to the coding part!&lt;/p&gt;

&lt;h2&gt;
  
  
  How to build this actually?
&lt;/h2&gt;

&lt;p&gt;Before we make anything sticky, we need a normal screen. Just a simple scrollable layout with a button at the bottom.&lt;/p&gt;

&lt;p&gt;Here’s our starting point:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;debugShowCheckedModeBanner:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomePage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Pinned Bottom Button'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;SafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;SingleChildScrollView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;spacing:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;205&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;206&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;207&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="nl"&gt;border:&lt;/span&gt; &lt;span class="n"&gt;Border&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="s"&gt;"Random List Item &lt;/span&gt;&lt;span class="si"&gt;$index&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
                  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pinned Button"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Random content at the bottom of the page."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Do We Have So Far?
&lt;/h3&gt;

&lt;p&gt;Let’s break this down. We have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;SingleChildScrollView&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Column&lt;/code&gt; with dummy content&lt;/li&gt;
&lt;li&gt;A button placed naturally at the bottom&lt;/li&gt;
&lt;li&gt;Some extra content after the button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Right now, the button behaves normally. If you scroll up, it disappears. If you scroll down, you see it again.&lt;/p&gt;

&lt;p&gt;There is nothing &lt;strong&gt;“sticky”&lt;/strong&gt; about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduce the StatefulWidget
&lt;/h3&gt;

&lt;p&gt;Right now, our screen is a &lt;code&gt;StatelessWidget&lt;/code&gt;. And that makes sense, until we realize something important. We want the UI to react to scroll changes. The moment we say &lt;strong&gt;“react”&lt;/strong&gt;, we introduce state.&lt;/p&gt;

&lt;p&gt;Our button visibility will depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the screen is scrollable&lt;/li&gt;
&lt;li&gt;Where the inline button currently is&lt;/li&gt;
&lt;li&gt;Whether it reached the fixed bottom position&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of those values change over time. And when something changes over time in Flutter… we need a &lt;code&gt;StatefulWidget&lt;/code&gt;. With that change, we get access to important methods like &lt;code&gt;initState()&lt;/code&gt;. Let's see how we use them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_HomePageState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isScrollable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_checkIfScrollable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_checkIfScrollable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasClients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isScrollable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxScrollExtent&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_isScrollable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isScrollable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isScrollable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_updateButtonVisibility&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isScrollable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_updateButtonVisibility&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why we need ScrollController?
&lt;/h4&gt;

&lt;p&gt;With the &lt;code&gt;SingleChildScrollView&lt;/code&gt; we apply the scrolling functionality to our screen. By default, we have no idea when it actually scrolls. So, here comes the &lt;code&gt;ScrollController&lt;/code&gt;!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;SingleChildScrollView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have access to the scroll position, we listen to scrolls and we also have the information about how much content exceeds the screen. As you can see, this controller acts as the communication bridge between the scrollable widget and our state logic. And this line is where things become reactive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time the scroll position changes, &lt;code&gt;_onScroll()&lt;/code&gt; gets called.&lt;/p&gt;

&lt;h4&gt;
  
  
  The importance of initState()
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;initState()&lt;/code&gt; runs exactly once, when the widget is inserted into the widget tree. This makes it the perfect place to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attach listeners&lt;/li&gt;
&lt;li&gt;Initialize controllers&lt;/li&gt;
&lt;li&gt;Perform one-time setup logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We never attach listeners inside &lt;code&gt;build()&lt;/code&gt;. &lt;strong&gt;Why?&lt;/strong&gt; Because &lt;code&gt;build()&lt;/code&gt; can run many times. Think of all the "noise" we would have if we attached a listener everytime the &lt;code&gt;build()&lt;/code&gt; was triggered.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Use addPostFrameCallback?
&lt;/h4&gt;

&lt;p&gt;If you see inside &lt;code&gt;initState()&lt;/code&gt;, we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_checkIfScrollable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why don't we just call &lt;code&gt;_checkIfScrollable()&lt;/code&gt; directly? Because when &lt;code&gt;initState()&lt;/code&gt; runs, &lt;strong&gt;the UI has not been laid out yet.&lt;/strong&gt; At that moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scroll view doesn’t know its dimensions&lt;/li&gt;
&lt;li&gt;The content hasn’t been measured&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxScrollExtent&lt;/code&gt; is not reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we check scroll metrics too early, we might get incorrect values. But, &lt;code&gt;addPostFrameCallback&lt;/code&gt; runs after the first frame is rendered. &lt;strong&gt;This guarantees that our logic runs at the correct time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also, it's important to explain one more critical part of the code. Inside &lt;code&gt;_checkIfScrollable()&lt;/code&gt;, we calculate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isScrollable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxScrollExtent&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;maxScrollExtent&lt;/code&gt; represents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The total scrollable distance beyond the visible viewport.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If it &lt;strong&gt;equals 0&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The content fits entirely on the screen.&lt;/li&gt;
&lt;li&gt;There is nothing to scroll.&lt;/li&gt;
&lt;li&gt;Sticky behavior is unnecessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it’s &lt;strong&gt;greater than 0&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The content exceeds the screen height.&lt;/li&gt;
&lt;li&gt;The screen is scrollable.&lt;/li&gt;
&lt;li&gt;Our sticky logic becomes relevant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;_isScrollable&lt;/code&gt; acts as a guard. If the screen isn’t scrollable, we don’t waste time calculating button positions. It's a small optimization, &lt;strong&gt;but important&lt;/strong&gt;. This is what makes the difference in the code afterall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's add the Sticky button
&lt;/h3&gt;

&lt;p&gt;What do we have so far? Right now, we have a button that lives inside our &lt;code&gt;SingleChildScrollView&lt;/code&gt;, but we need a button that will flow above all content. This functionality gets unlocked by wrapping our entire content into a &lt;code&gt;Stack&lt;/code&gt;. Stack is a very powerful Widget in Flutter that lets multiple layers of content get on top of each-other. And this is exactly what we need here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;SafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;SingleChildScrollView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="c1"&gt;// Scrollable content&lt;/span&gt;
            &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;_inlineButtonKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Opacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;opacity:&lt;/span&gt; &lt;span class="n"&gt;_inlineOpacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_buildButton&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;

      &lt;span class="n"&gt;Positioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;left:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;right:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Opacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;_floatingButtonKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;opacity:&lt;/span&gt; &lt;span class="n"&gt;_floatingOpacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;IgnorePointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;ignoring:&lt;/span&gt; &lt;span class="n"&gt;_floatingOpacity&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_buildButton&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_buildButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;styleFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFD97757&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;shape:&lt;/span&gt; &lt;span class="n"&gt;RoundedRectangleBorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Pinned Button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the same button for both the floating button and our inline button, so it's better if we create a separate Widget to stay aligned in both cases. But now there are a lot of new interesting things inside the code. Why do we need keys? How do we use opacity? Let's explain everything!&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Do We Need GlobalKey?
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final GlobalKey _inlineButtonKey = GlobalKey();
final GlobalKey _floatingButtonKey = GlobalKey();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We attach two &lt;code&gt;GlobalKey&lt;/code&gt;s to our two button widgets because this gives us access to their underlying &lt;code&gt;RenderObject&lt;/code&gt;. Normally in Flutter, we don’t go that deep. We declare widgets, Flutter lays them out, and that’s it.&lt;/p&gt;

&lt;p&gt;But in this case, we need something very specific:&lt;/p&gt;

&lt;p&gt;We need to know the exact position of these buttons on the screen. And that information is not available at the widget level. It lives inside the rendering layer. More specifically, inside the widget’s &lt;code&gt;RenderObject&lt;/code&gt;. That’s why we need a &lt;code&gt;GlobalKey&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  How Do We Access the Exact Location of the Buttons?
&lt;/h4&gt;

&lt;p&gt;Earlier, we referenced a method called &lt;code&gt;_updateButtonVisibility()&lt;/code&gt;. Now it’s time to take a closer look at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_updateButtonVisibility&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;inlineBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;_inlineButtonKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderBox&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;floatingBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;_floatingButtonKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderBox&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inlineBox&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;floatingBox&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;inlinePosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inlineBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localToGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;floatingPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floatingBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localToGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;reachedFixedPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inlinePosition&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floatingPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reachedFixedPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inlineOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_floatingOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inlineOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_floatingOpacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key part of this method, is here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;inlinePosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inlineBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localToGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;floatingPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floatingBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localToGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what's going on here. First of all, we use &lt;code&gt;Offset.zero&lt;/code&gt;. That represents the top-left corner of the widget in its local coordinate system, which will be our &lt;strong&gt;target-point&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Offset(0,0)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By default, a widget’s position is relative to its parent. That means if we just queried its layout position normally, we would get coordinates relative to the parent widget, &lt;strong&gt;not the screen&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;But we don’t care about the parent. We care about the screen. &lt;code&gt;localToGlobal()&lt;/code&gt; converts a local coordinate into a global screen coordinate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Offset(24.0, 612.0)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;24 pixels from the left side of the screen&lt;/li&gt;
&lt;li&gt;612 pixels from the top of the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From that &lt;code&gt;Offset&lt;/code&gt;, we only care about the vertical position, so we extract the &lt;code&gt;dy&lt;/code&gt;. And the final result looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;inlinePosition = 612.0&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The logic behind this calculation
&lt;/h4&gt;

&lt;p&gt;The entire purpose of measuring these two positions is very simple. We want to know when both buttons are aligned vertically.&lt;br&gt;
Let’s think about their behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The inline button lives inside the scroll view → &lt;strong&gt;its Y position constantly changes while scrolling.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The floating button is positioned using Positioned(bottom: 16) → &lt;strong&gt;its Y position remains stable.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we scroll upward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The inline button moves closer to the floating button’s position.&lt;/li&gt;
&lt;li&gt;Its &lt;code&gt;.dy&lt;/code&gt; value decreases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment this becomes true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inlinePosition &amp;lt;= floatingPosition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means:&lt;/p&gt;

&lt;p&gt;The top-left corner of the inline button has reached (or passed) the top-left corner of the floating button. At that exact moment, the two buttons are aligned. And that’s when we switch visibility.&lt;/p&gt;

&lt;p&gt;The inline button takes over. The floating button disappears.&lt;/p&gt;

&lt;h4&gt;
  
  
  Opacity toggle
&lt;/h4&gt;

&lt;p&gt;Inside &lt;code&gt;setState()&lt;/code&gt; we change the opacity between the two buttons. Only one of them can be visible. But why is it so important to use &lt;code&gt;Opacity&lt;/code&gt; and we didn't choose to remove the unsused Widget? The reason is to avoid UI leaks. If we add/remove Widgets all the time, we may &lt;br&gt;
change layout height, cause visual jumps, or more importantly affect scroll position. &lt;strong&gt;Opacity keeps layout stable.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you go! What started as a simple scrollable screen slowly evolved into a fully controlled sticky button pattern, built entirely with Flutter’s core tools. &lt;strong&gt;No packages. No hacks.&lt;/strong&gt; Just a solid understanding of how layout, scrolling, and rendering work together. &lt;/p&gt;

&lt;p&gt;Along the way, we didn’t just “make a button stick.” We explored &lt;code&gt;StatefulWidget&lt;/code&gt;, &lt;code&gt;ScrollController&lt;/code&gt;, &lt;code&gt;addPostFrameCallback&lt;/code&gt;, &lt;code&gt;GlobalKey&lt;/code&gt; and more. This pattern might not be something you implement every day. But when you need precise scroll-aware behavior, (like checkout CTAs, smart action bars, or contextual buttons) understanding this technique gives you full control.&lt;/p&gt;

&lt;p&gt;Hopefully, this guide didn’t just show you how to build a sticky button, but helped you better understand how Flutter thinks about layout and positioning.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_sticky_button" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now go ahead. Play with it. Improve it. And we will catch up in the next blog!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Creating Spotlight Tutorials in Flutter: The Complete Guide to Selective Overlays</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sun, 11 Jan 2026 11:10:11 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/creating-spotlight-tutorials-in-flutter-the-complete-guide-to-selective-overlays-4iil</link>
      <guid>https://dev.to/thanasistraitsis/creating-spotlight-tutorials-in-flutter-the-complete-guide-to-selective-overlays-4iil</guid>
      <description>&lt;p&gt;Hello there, my Flutter friends! Recently, I downloaded an app and, when I opened it for the first time, I noticed this cool feature that many apps use to guide new users through their core functionality. You know what I am talking about: a button gets highlighted, the rest of the screen fades out, and your attention is drawn exactly where the developer wants it to be. This kind of &lt;strong&gt;spotlight onboarding&lt;/strong&gt; is everywhere, and that’s when I had my “Eureka” moment!&lt;/p&gt;

&lt;p&gt;That’s what this blog is all about. We’ll build this exact effect in Flutter, step by step, and use it as an opportunity to deeply understand how Flutter handles rendering, painting, and widget positioning under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI Setup
&lt;/h2&gt;

&lt;p&gt;I want to keep the UI as simple as possible. We’ll start with a clean home screen that contains just a few buttons. The ones we’re going to highlight, and one extra button that will trigger the highlight effect. Yes, we’ll manually turn the effect on and off. You don’t expect me to build a full onboarding system from scratch… do you? You can find all the code you need from the github repository at the end of this article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Fhome_screen.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Fhome_screen.png%3Fraw%3Dtrue" alt="Home Screen" width="394" height="853"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Highlight Effect
&lt;/h2&gt;

&lt;p&gt;Now it’s time for the interesting part. How do we "remove" the color from the entire screen, while keeping specific widgets fully visible? The answer is simple: &lt;strong&gt;we don’t remove anything&lt;/strong&gt;. Instead, we are going to add something on top.&lt;/p&gt;

&lt;p&gt;To achieve this effect, we introduce an extra layer that sits above our existing UI. This layer acts as a semi-transparent overlay, covering the whole screen and dimming everything underneath. The key trick, however, is that this overlay is not solid. We intentionally “cut out” specific areas that line up with the widgets we want to highlight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Flayers.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Flayers.png%3Fraw%3Dtrue" alt="Layers" width="749" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How does this effect work?
&lt;/h3&gt;

&lt;p&gt;We add a full-screen overlay on top of our screen and paint it with a dark color and some opacity. This allows the underlying UI to remain visible, but visually de-emphasized. Then, for each widget we want to spotlight, we carve a transparent hole in that overlay, positioned exactly where the widget appears on the screen.&lt;/p&gt;

&lt;p&gt;As a result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The background content fades into the dark overlay&lt;/li&gt;
&lt;li&gt;The highlighted widgets remain completely clear and untouched&lt;/li&gt;
&lt;li&gt;The user’s attention is naturally drawn to the important elements&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Nothing from the original UI is modified or removed. The entire effect is purely visual and lives in its own layer, which makes it both powerful and flexible. Now that we have a clear picture of what we want to achieve, it’s time to start building it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Overlay Layer
&lt;/h3&gt;

&lt;p&gt;Before we dive into anything fancy, we need a solid foundation. The first step is to create a reusable screen widget that we can use across the app. Nothing special here, just a simple &lt;code&gt;Scaffold&lt;/code&gt; with some padding and a &lt;code&gt;SafeArea&lt;/code&gt; to keep things clean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: child,
        ),
      ),
    );
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have a perfectly normal screen. Now comes the important question:&lt;br&gt;
&lt;em&gt;How do we place something on top of it without touching the existing UI?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is where the &lt;code&gt;Stack&lt;/code&gt; widget comes in. Stack lets us layer widgets on top of each other (like layers in Photoshop). The first child is the bottom layer, subsequent children stack on top. Here’s how our updated &lt;code&gt;CustomScreen&lt;/code&gt; looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class CustomScreen extends StatelessWidget {
  final Widget child;
  final bool showOverlay;
  final List&amp;lt;GlobalKey&amp;gt;? highlightWidgetKeys;
  final VoidCallback? onOverlayTap;
  final HighlightStyle? highlightStyle;

  const CustomScreen({
    super.key,
    required this.child,
    this.showOverlay = false,
    this.highlightWidgetKeys,
    this.onOverlayTap,
    this.highlightStyle,
  });

  bool get _shouldShowOverlay =&amp;gt;
      showOverlay &amp;amp;&amp;amp;
      highlightWidgetKeys != null &amp;amp;&amp;amp;
      highlightWidgetKeys!.isNotEmpty;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Scaffold(
          body: SafeArea(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: child,
            ),
          ),
        ),
        if (_shouldShowOverlay)
          Positioned.fill(
            child: _SelectiveOverlay(
              widgetKeys: highlightWidgetKeys!,
              onDarkAreaTap: onOverlayTap,
              style: highlightStyle ?? const HighlightStyle(),
            ),
          ),
      ],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few important things are happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;Stack&lt;/code&gt; allows us to layer widgets on top of each other&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Scaffold&lt;/code&gt; remains untouched and behaves exactly as before&lt;/li&gt;
&lt;li&gt;The overlay is rendered only when needed, controlled by _shouldShowOverlay&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Positioned.fill&lt;/code&gt; ensures the overlay covers the entire screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, let’s look at some of the more interesting parts of this implementation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Highlighting specific widgets
&lt;/h4&gt;

&lt;p&gt;You’ll notice that we pass a variable called &lt;strong&gt;highlightWidgetKeys&lt;/strong&gt;. This is a list of &lt;code&gt;GlobalKey&lt;/code&gt;s that correspond to the widgets we want to highlight. Since we may want to spotlight more than one widget at a time, we use a List.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why GlobalKeys?&lt;/strong&gt;&lt;br&gt;
GlobalKeys are special identifiers in Flutter that let us access a widget's &lt;code&gt;RenderObject&lt;/code&gt; (the actual painted object on screen) and its position. Without them, we couldn't know where to "cut holes" in the overlay. You can check the detailed documentation &lt;a href="https://api.flutter.dev/flutter/widgets/GlobalKey-class.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Styling the highlights
&lt;/h4&gt;

&lt;p&gt;You’ll also notice that we pass a &lt;code&gt;HighlightStyle&lt;/code&gt; object into our overlay. This is a small but important design choice.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding colors, padding, and border radius inside our rendering logic, we extract all styling concerns into a dedicated class. This keeps our code clean, reusable, and much easier to maintain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class HighlightStyle extends Equatable {
  final Color overlayColor;
  final double borderRadius;
  final double padding;

  const HighlightStyle({
    this.overlayColor = Colors.black54, // Opacity for the overlay layer
    this.borderRadius = 12.0, // Rounded corners for highlighted areas
    this.padding = 8.0, // Extra spacing around highlighted widgets
  });

  @override
  List&amp;lt;Object?&amp;gt; get props =&amp;gt; [overlayColor, borderRadius, padding];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, we’re not drawing anything yet. We’re simply preparing the screen to host a visual layer that can sit above the UI and react to user interactions independently.&lt;/p&gt;

&lt;p&gt;In the next section, we’ll see how this overlay actually draws itself and how it knows where to create those transparent “holes” on the screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Flutter go from widgets to pixels?
&lt;/h3&gt;

&lt;p&gt;Before we dive into the nitty-gritty of custom painting and hit testing, we need to understand an important bridge in Flutter's architecture. You see, Flutter has a beautiful separation of concerns that says &lt;strong&gt;widgets describe what you want&lt;/strong&gt;, while &lt;strong&gt;RenderObjects do the actual work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think of it this way: widgets are like blueprints, and RenderObjects are the construction workers that build from those blueprints. Most of the time, Flutter handles this translation automatically. But when we need custom behavior &lt;em&gt;'like our selective overlay'&lt;/em&gt; we need to create this bridge ourselves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class _SelectiveOverlay extends SingleChildRenderObjectWidget {
  final List&amp;lt;GlobalKey&amp;gt; widgetKeys;
  final HighlightStyle style;
  final VoidCallback? onDarkAreaTap;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;SingleChildRenderObjectWidget&lt;/code&gt;&lt;/strong&gt; is our bridge. It's a special type of widget that says: &lt;em&gt;"I'm going to create my own custom RenderObject to handle the heavy lifting."&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;_SelectiveOverlay&lt;/code&gt; widget is quite simple. It holds three pieces of configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;widgetKeys:&lt;/strong&gt; The list of GlobalKeys pointing to widgets we want to highlight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;style:&lt;/strong&gt; Our &lt;code&gt;HighlightStyle&lt;/code&gt; object containing colors, padding, and border radius&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;onDarkAreaTap:&lt;/strong&gt; An optional callback for when users tap the dark overlay area&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The createRenderObject Method
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@override
RenderObject createRenderObject(BuildContext context) {
  return _RenderSelectiveOverlay(
    widgetKeys: widgetKeys,
    style: style,
    onDarkAreaTap: onDarkAreaTap,
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the magic starts. When Flutter builds our widget tree and encounters &lt;code&gt;_SelectiveOverlay&lt;/code&gt;, it calls &lt;strong&gt;createRenderObject()&lt;/strong&gt;. This method's job is simple: create the RenderObject that will do the actual painting and hit testing.&lt;/p&gt;

&lt;p&gt;Think of this like a factory pattern. The widget is the factory, and it produces a worker (the RenderObject) that knows how to do the job. This separation is crucial because &lt;strong&gt;widgets are rebuilt frequently (they're cheap and immutable)&lt;/strong&gt;, but RenderObjects stick around and do the &lt;strong&gt;expensive work&lt;/strong&gt; like layout, painting, and hit testing.&lt;/p&gt;

&lt;h4&gt;
  
  
  The updateRenderObject Method
&lt;/h4&gt;

&lt;p&gt;Here's where things get interesting. Remember how I said widgets are rebuilt frequently? Well, we don't want to throw away and recreate our RenderObject every time, that would be wasteful. Instead, Flutter is smart! When our widget rebuilds with new data, it calls updateRenderObject() to pass the new configuration to the existing RenderObject.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@override
void updateRenderObject(
  BuildContext context,
  _RenderSelectiveOverlay renderObject,
) {
  renderObject
    ..widgetKeys = widgetKeys
    ..style = style
    ..onDarkAreaTap = onDarkAreaTap;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is Flutter's optimization in action. We're essentially saying: &lt;em&gt;"Hey, RenderObject, here's your updated configuration. If anything changed, you'll need to repaint."&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Why This Architecture Matters
&lt;/h4&gt;

&lt;p&gt;Now the real question is: "Why do we need all that? Couldn't we just use &lt;code&gt;CustomPaint&lt;/code&gt; like usual?" Fair question! Well, we could use &lt;code&gt;CustomPaint&lt;/code&gt; for the drawing part, but we'd still struggle with the hit testing. By going down to the &lt;code&gt;RenderObject&lt;/code&gt; level, we get complete control over both painting and how taps are handled. This is the secret sauce that lets us create "holes" in our overlay that pass taps through to the buttons underneath.&lt;/p&gt;

&lt;p&gt;This widget layer is lightweight and declarative, it just describes what we want. The real heavy lifting happens in the &lt;code&gt;_RenderSelectiveOverlay&lt;/code&gt; class, which we're about to explore. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Heart of the System: Understanding RenderBox
&lt;/h3&gt;

&lt;p&gt;Now we're getting to the exciting part, the &lt;strong&gt;&lt;code&gt;_RenderSelectiveOverlay&lt;/code&gt;&lt;/strong&gt; RenderBox. This is where all the painting, the hit testing, and the performance optimizations happens, that make our overlay silky smooth. Don't worry if this looks intimidating at first. We'll break it down piece by piece, and by the end, you'll understand exactly how Flutter's rendering engine works under the hood.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting Up Our RenderBox
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class _RenderSelectiveOverlay extends RenderBox {
  List&amp;lt;GlobalKey&amp;gt; _widgetKeys;
  HighlightStyle _style;
  VoidCallback? _onDarkAreaTap;

  _RenderSelectiveOverlay({
    required List&amp;lt;GlobalKey&amp;gt; widgetKeys,
    required HighlightStyle style,
    VoidCallback? onDarkAreaTap,
  }) : _widgetKeys = widgetKeys,
       _style = style,
       _onDarkAreaTap = onDarkAreaTap;

  List&amp;lt;GlobalKey&amp;gt; get widgetKeys =&amp;gt; _widgetKeys;
  set widgetKeys(List&amp;lt;GlobalKey&amp;gt; value) {
    if (_widgetKeys == value) return;
    _widgetKeys = value;
    markNeedsPaint(); // Trigger the rebuild of the Overlay
  }

  HighlightStyle get style =&amp;gt; _style;
  set style(HighlightStyle value) {
    if (_style == value) return;
    _style = value;
    markNeedsPaint(); // Trigger the rebuild of the Overlay
  }

  VoidCallback? get onDarkAreaTap =&amp;gt; _onDarkAreaTap;
  set onDarkAreaTap(VoidCallback? value) {
    _onDarkAreaTap = value; // No need for visual rebuild
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our RenderBox extends Flutter's RenderBox class, which is the base for all 2D rectangular rendering. We store three private fields that hold our configuration. But here's the thing, we need to be smart about how they're updated.&lt;/p&gt;

&lt;p&gt;These getters and setters are doing something clever. When &lt;strong&gt;updateRenderObject()&lt;/strong&gt; passes new values from the widget layer, these setters spring into action. First, they check: "Is this actually a new value?" If the value hasn't changed, &lt;strong&gt;we bail out early&lt;/strong&gt;. There is no point in doing unnecessary work.&lt;/p&gt;

&lt;p&gt;But if the value has changed, we update the field and then call &lt;strong&gt;markNeedsPaint()&lt;/strong&gt;. This is our way of telling Flutter: "Hey, something visual changed! You need to repaint me on the next frame." Without this call, you could change the highlighted buttons or overlay color, and nothing would happen on screen. The data would update, but the pixels wouldn't!&lt;/p&gt;

&lt;h4&gt;
  
  
  Defining Our Size
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@override
bool get sizedByParent =&amp;gt; true;

@override
Size computeDryLayout(BoxConstraints constraints) {
  return constraints.biggest;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a cool Flutter optimization. By returning &lt;strong&gt;true&lt;/strong&gt; from &lt;code&gt;sizedByParent&lt;/code&gt;, we're telling Flutter: &lt;em&gt;"My size depends only on my constraints, not on my children or any other factors."&lt;/em&gt; This allows Flutter to skip some expensive layout calculations.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;computeDryLayout&lt;/code&gt; method then says: "Give me the biggest size you'll allow." Since we're using &lt;code&gt;Positioned.fill&lt;/code&gt; in our widget tree, we want to cover the entire screen, so we take up all available space. The term "dry layout" means we're calculating size without any side effects, pure calculation.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Paint Method: Creating Holes in the Darkness
&lt;/h4&gt;

&lt;p&gt;Now for the main event, &lt;strong&gt;the painting logic&lt;/strong&gt;. This is where we create that dark overlay with transparent holes for our highlighted widgets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  @override
  void paint(PaintingContext context, Offset offset) {
    final canvas = context.canvas;
    final size = this.size;

    final paint = Paint()
      ..color = _style.overlayColor
      ..style = PaintingStyle.fill;

    final path = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we grab the canvas (our drawing surface) and set up our paint brush. The &lt;code&gt;Paint&lt;/code&gt; object is like configuring a real paintbrush. We're saying "use this color" and "fill shapes completely" (not just stroke their outlines). Here's where it gets clever. We start by creating a &lt;code&gt;Path&lt;/code&gt; and adding a rectangle that covers the entire screen. At this point, if we drew this path, we'd have a solid overlay blocking everything. But we're not done yet!&lt;/p&gt;

&lt;h4&gt;
  
  
  Looping through highlighted Widgets
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (final key in _widgetKeys) {
  final renderBox = key.currentContext?.findRenderObject() as RenderBox?;

  if (renderBox == null || !renderBox.attached) continue;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we loop through each GlobalKey pointing to a widget we want to highlight. For each key, we try to get its &lt;strong&gt;RenderObject&lt;/strong&gt;. The null check (&lt;code&gt;renderBox == null&lt;/code&gt;) handles cases where the widget hasn't been built yet, and the &lt;code&gt;attached&lt;/code&gt; check prevents crashes if the widget is being removed from the tree. This is defensive programming at its best, we're handling edge cases that could occur during animations, page transitions, or rapid rebuilds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final position = renderBox.localToGlobal(Offset.zero);
  final widgetSize = renderBox.size;

  final widgetRect = Rect.fromLTWH(
    position.dx - (_style.padding / 2),
    position.dy - (_style.padding / 2),
    widgetSize.width + _style.padding,
    widgetSize.height + _style.padding,
  );

  path.addRRect(
    RRect.fromRectAndRadius(
      widgetRect,
      Radius.circular(_style.borderRadius),
    ),
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each widget we want to highlight, we need to know: "Where is it on the screen?" and "How big is it?" The &lt;code&gt;localToGlobal(Offset.zero)&lt;/code&gt; call converts the widget's local &lt;strong&gt;top-left corner (0, 0)&lt;/strong&gt; to global screen coordinates. Then we create a rectangle that's slightly larger than the widget itself. That's our padding creating a visual "breathing room" around the highlighted area. Finally, we add each highlighted area to our path as a rounded rectangle. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Flocal_to_global.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_spotlight%2Fblob%2Fmain%2Fassets%2Flocal_to_global.png%3Fraw%3Dtrue" alt="Corner Offset" width="453" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The final touch
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path.fillType = PathFillType.evenOdd;
canvas.drawPath(path, paint);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the moment everything comes together! The &lt;strong&gt;&lt;code&gt;PathFillType.evenOdd&lt;/code&gt;&lt;/strong&gt; is the secret that creates our holes. Here's how it works: imagine drawing a line from outside the screen to any point. Count how many times that line crosses a shape boundary. If the count is odd, fill that area. If it's even, leave it empty.&lt;/p&gt;

&lt;p&gt;So for our overlay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Points outside everything: 1 crossing (the screen rectangle) = odd = filled ✓&lt;/li&gt;
&lt;li&gt;Points inside a highlighted area: 2 crossings (screen + highlight) = even = empty ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's like magic, but it's just clever math! This single line is what creates those transparent "holes" in our dark overlay.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hit Testing: Making the Magic Interactive
&lt;/h4&gt;

&lt;p&gt;The painting makes things look right, but we still need to make them behave right. When a user taps the screen, how do we decide what happens?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
  for (final key in _widgetKeys) {
    final renderBox = key.currentContext?.findRenderObject() as RenderBox?;

    if (renderBox == null || !renderBox.attached) continue;

    final widgetPosition = renderBox.localToGlobal(Offset.zero);
    final widgetSize = renderBox.size;

    final rect = Rect.fromLTWH(
      widgetPosition.dx - (_style.padding / 2),
      widgetPosition.dy - (_style.padding / 2),
      widgetSize.width + _style.padding,
      widgetSize.height + _style.padding,
    );

    if (rect.contains(position)) {
      return false;
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flutter's hit testing works from top to bottom in the widget tree. When our overlay's &lt;code&gt;hitTest&lt;/code&gt; is called, we check: "Did the user tap inside any of our highlighted areas?" Notice we're using the same rectangle calculation as our paint method. &lt;strong&gt;This consistency is crucial.&lt;/strong&gt; The visual holes and the interaction holes must match perfectly.&lt;br&gt;
If the tap is inside a highlighted area, we return &lt;strong&gt;false&lt;/strong&gt;. This is our way of saying: "Nope, I'm not handling this tap. Let it pass through to the widget underneath!" This is what makes the buttons still work even though there's an overlay on top of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (size.contains(position)) {
    _onDarkAreaTap?.call();
    result.add(BoxHitTestEntry(this, position));
    return true;
  }

  return false;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if the tap is in the dark overlay area (not in any highlighted region), we handle it ourselves. We call the callback (if one was provided), add ourselves to the hit test result, and return &lt;strong&gt;true&lt;/strong&gt; to stop the hit test from continuing to widgets below.&lt;/p&gt;

&lt;p&gt;The final return &lt;strong&gt;false&lt;/strong&gt; is a safety net for taps that somehow fall outside our bounds—though in practice, this shouldn't happen since we fill the entire screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Conclusion
&lt;/h2&gt;

&lt;p&gt;And that’s pretty much it! We started with a simple UI and gradually built our way up from a basic &lt;code&gt;Stack&lt;/code&gt; to a fully custom overlay powered by &lt;code&gt;RenderBox&lt;/code&gt; and &lt;code&gt;Paint&lt;/code&gt;. Along the way, we explored how Flutter moves from widgets to pixels, how to locate widgets on the screen using &lt;strong&gt;GlobalKeys&lt;/strong&gt;, and how to draw directly on the canvas to create that spotlight effect.&lt;/p&gt;

&lt;p&gt;More importantly, we took a look under the hood. We didn’t just use Flutter’s rendering system, we learned how it works. From layering widgets, to custom painting, to controlling hit testing, every piece of this solution shows how flexible Flutter can be when you need to go beyond standard widgets. To be honest, this kind of approach isn’t something you’ll need every day, but when you do (like onboarding flows, tutorials, coach marks, or any kind of custom visual effect ), understanding the rendering pipeline makes all the difference. Hopefully, this article gave you both a practical feature you can reuse and a deeper appreciation of how Flutter actually draws things on the screen.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_spotlight" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, feel free to experiment, tweak the code, and make it your own. Happy coding, Flutter friends!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>coding</category>
    </item>
    <item>
      <title>The Ultimate Guide to Flutter Lists with Bloc : Part 3</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sun, 11 May 2025 19:54:12 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-3-599e</link>
      <guid>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-3-599e</guid>
      <description>&lt;h2&gt;
  
  
  Part 3 : Powerful Filtering for Your Flutter Listings
&lt;/h2&gt;

&lt;p&gt;Welcome back to our Flutter listing series! In Parts 1 and 2, we built a powerful Pokémon listing with pagination and infinite scrolling. Now it's time to introduce another essential feature of any good listing: &lt;strong&gt;filtering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Effective filtering transforms a simple list into a powerful tool that helps users find exactly what they're looking for, without extra effort and with less time needed. Whether you're filtering Pokémon by type, searching products by category, or sorting messages by date, the principles remain the same.&lt;/p&gt;

&lt;p&gt;In this third blog, we'll implement a complete filtering system for our Pokédex app. We'll create a simple &lt;strong&gt;FilterBloc&lt;/strong&gt; that communicates seamlessly with our &lt;strong&gt;ListingBloc&lt;/strong&gt;, apply filters efficiently without sacrificing performance, and ensure a smooth UX throughout the process.&lt;/p&gt;

&lt;p&gt;Let's explore how to build a filtering system that's both powerful and maintainable, all while keeping our Pokémon list running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's talk about the Filters
&lt;/h2&gt;

&lt;p&gt;The filtering system of a listing is a complex feature. It needs to combine multiple filter criteria while ensuring the UI responds appropriately throughout the application. This complexity requires the filter to maintain its own state. And that's where our beloved friend &lt;strong&gt;Bloc&lt;/strong&gt; comes to the rescue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Filters State
&lt;/h3&gt;

&lt;p&gt;First, we need to consider that we don't need multiple filter states. The &lt;code&gt;PokemonFilterState&lt;/code&gt; is singular, and only its values change as users select different filtering options. We can start with a simple implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonFilterState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{}});&lt;/span&gt;

  &lt;span class="n"&gt;PokemonFilterState&lt;/span&gt; &lt;span class="n"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;selectedFilters:&lt;/span&gt; &lt;span class="n"&gt;selectedFilters&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt; for the selected filters because it gives us flexibility to store different types of filter values. For example, we might need to store string values for Pokemon types, numeric ranges for stats, or boolean values for availability filters—all within the same state object.&lt;/p&gt;

&lt;p&gt;What makes this approach powerful is that I can track every filter change reliably by using consistent &lt;code&gt;key&lt;/code&gt; values as identifiers. By centralizing these &lt;code&gt;keys&lt;/code&gt;—either fetching them from an API or storing them as static constants—I ensure my UI and business logic remain perfectly aligned. This prevents filter-related bugs and makes the codebase more maintainable as the filtering system grows in complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply filter changes with Events
&lt;/h3&gt;

&lt;p&gt;Moving on, now that we initialized our state with an empty value, we need to figure out a way to add some values. That's where the events step up. But, what are the actual events that we need? Let's figure out first how to add a value. For this example, we will cover only two types of filters with the first one being the favorite pokemons, and the second one is based on the type of each pokemon. Let's make it!&lt;/p&gt;

&lt;h4&gt;
  
  
  Toggling the filters
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonFilterEvent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Equatable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterEvent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToggleFilter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;showFavorites&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ToggleFilter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showFavorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, we separate the logic of each filter by providing its own dedicated variable. This gives us clearer code and more flexibility when handling different filter types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For favorite Pokémon, we simply need a boolean flag &lt;code&gt;(showFavorites)&lt;/code&gt; - either we want to see favorites only &lt;strong&gt;(true)&lt;/strong&gt; or we don't &lt;strong&gt;(false)&lt;/strong&gt;. Nothing more complex is required here.&lt;/li&gt;
&lt;li&gt;For Pokémon types, we use a &lt;code&gt;Set&amp;lt;String&amp;gt;&lt;/code&gt; instead of a List. Why? Because each type should only appear once in our filter criteria (there's no scenario where we'd need to apply "Water" type twice). Sets inherently prevent duplicates and offer performance benefits when checking if a value exists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡Always use the right tools for the job. Sets aren't just about preventing duplicates - they also provide powerful operations like union (combining filters), intersection (finding common elements), and difference (removing specific filters) that make complex filter manipulations much cleaner. When your app grows and users demand more sophisticated filtering capabilities, you'll thank yourself for this decision.💡&lt;/p&gt;

&lt;p&gt;Now, our event is ready to receive filter changes. Notice how we made both parameters nullable? This allows us to toggle just one filter at a time without affecting the others. For instance, a user might want to change only the type filter without modifying their favorites preference.&lt;/p&gt;

&lt;h4&gt;
  
  
  Remove the filters
&lt;/h4&gt;

&lt;p&gt;Adding filters is only half the job. Users also need an easy way to remove them. Thanks to the well-structured foundation we’ve built, implementing filter removal is pretty straightforward.&lt;/p&gt;

&lt;p&gt;To remove a specific filter value, we simply need two pieces of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;key&lt;/code&gt; of the filter (e.g. "type")&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;value&lt;/code&gt; we want to remove (e.g. "Fire")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is important because some filters, like Pokémon types, can support multiple values at once. Here's how we model these events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RemoveFilter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;valueToRemove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RemoveFilter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueToRemove&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RemoveAllFilters&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this structure, removing a single filter or clearing all filters becomes a clean, manageable task, one that fits perfectly into our event-driven system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combine everything into Bloc
&lt;/h3&gt;

&lt;p&gt;Now comes the fun part! We've built a clean event structure and designed a state to hold our filter data. It's time to glue everything together into a working Bloc.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bloc Constructor
&lt;/h4&gt;

&lt;p&gt;To begin, let's initialize our Bloc with a proper constructor. We'll register each event type with its corresponding handler method, creating a clear mapping between user actions and our business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;PokemonFilterBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToggleFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onToggleFilter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RemoveFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onRemoveFilter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RemoveAllFilters&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onRemoveAllFilters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This constructor initializes our Bloc with an empty PokemonFilterState and registers three event handlers that we'll implement next.&lt;/p&gt;

&lt;h4&gt;
  
  
  Toggle filter
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onToggleFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToggleFilter&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;updatedFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showFavorites&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateFavoriteKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentValue&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateFavoriteKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateFavoriteKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateTypesKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;selectedFilters:&lt;/span&gt; &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation does several important things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It creates a mutable copy of our current filters map to work with&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;favorites&lt;/strong&gt;, it toggles the value - removing it if already true, or setting it to true if not present&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Pokémon types&lt;/strong&gt;, it simply updates the map with the new set of types&lt;/li&gt;
&lt;li&gt;Finally, it emits a new state with the updated filters&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice how we're using string constants from AppStrings as keys. This centralization ensures consistency between our Bloc implementation and UI code, making maintenance much easier.&lt;/p&gt;

&lt;h4&gt;
  
  
  Removing filters (1-by-1 or all of them)
&lt;/h4&gt;

&lt;p&gt;Of course, we need to give users the ability to remove filters they've applied. Our implementation handles two scenarios: removing individual filter values and clearing all filters at once.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't miss out on the comments inside the code. It will give you a clear view on how everything works.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onRemoveFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RemoveFilter&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;updatedFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// For boolean filters (like favorites), simply remove the entire key&lt;/span&gt;
    &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// For set-based filters (like types), remove just the specific value&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;originalSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;updatedSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueToRemove&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// If removing this value leaves the set empty, remove the entire filter key&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updatedSet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;selectedFilters:&lt;/span&gt; &lt;span class="n"&gt;updatedFilters&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onRemoveAllFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RemoveAllFilters&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonFilterState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;selectedFilters:&lt;/span&gt; &lt;span class="p"&gt;{}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation demonstrates the flexibility of our &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt; approach. The &lt;code&gt;_onRemoveFilter&lt;/code&gt; method handles different filter types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Boolean filters (such as favorites) are handled with a simple &lt;code&gt;key&lt;/code&gt; removal, just toggle it off and it's gone&lt;/li&gt;
&lt;li&gt;For collection-based filters like Pokémon types, we surgically remove just the selected value while preserving others&lt;/li&gt;
&lt;li&gt;We maintain clean state by automatically removing empty collections. &lt;strong&gt;There is no need to track filters that don't filter anything&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The companion &lt;code&gt;_onRemoveAllFilters&lt;/code&gt; method offers a "reset button" functionality by replacing the entire filters map with an empty one, effectively returning to a fresh start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication between Blocs
&lt;/h2&gt;

&lt;p&gt;Now that we have our Filters bloc ready to go, we need to make something out of it. In order to see the filters work, we need to apply them to our current Pokémon list. But wait, our list is in our Pokémon List Bloc, so how do we combine these two Blocs?&lt;/p&gt;

&lt;p&gt;A simple solution that comes to mind right away is utilizing BlocListener and handling everything in our UI. But let's be honest, do we really want to complicate our UI with this business logic? I don't think so. So, let me show you a more elegant approach!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;PokemonFilterBloc&lt;/span&gt; &lt;span class="n"&gt;filterBloc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;StreamSubscription&lt;/span&gt; &lt;span class="n"&gt;filterSubscription&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filteredMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFiltering&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterBloc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListInitial&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;filterSubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filterBloc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;filterState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="n"&gt;filterState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateFavoriteKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
              &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;selectedTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="n"&gt;filterState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedFilters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppStrings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterStateTypesKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;isFiltering&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filteredMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;filteredMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;isFiltering&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApplyFilters&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InitialFetch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onInitialPokemonFetch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onFetchNextPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApplyFilters&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onApplyFilters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here? Instead of having our UI act as the intermediary between blocs (bad practise), we're directly injecting our &lt;code&gt;filterBloc&lt;/code&gt; into our &lt;code&gt;PokemonListBloc&lt;/code&gt;. This creates a clean dependency that follows the natural flow of our application logic: filters affect the list, not the other way around.&lt;/p&gt;

&lt;p&gt;The real magic happens with &lt;code&gt;filterSubscription&lt;/code&gt;. We're creating a &lt;strong&gt;stream subscription&lt;/strong&gt; that listens to any changes in the filter state. Basically, the stream is a spy that works for our List Bloc. His secret mission is to hear everything that happens in the Filter Bloc headquarters and immediately report back. Whenever our spy detects a change in filter operations, it secretly dispatches an &lt;code&gt;ApplyFilters&lt;/code&gt; event to its commander (the List Bloc), all without the UI ever knowing about this covert operation. How we actually act on it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the current filter values (favorites and types)&lt;/li&gt;
&lt;li&gt;Search if any filtering is currently active&lt;/li&gt;
&lt;li&gt;Prepare our data structure for filtered results if needed&lt;/li&gt;
&lt;li&gt;Dispatch an internal ApplyFilters event to the list bloc itself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach keeps our UI clean and simple while maintaining proper separation of concerns. The List Bloc is responsible for maintaining and filtering the list, and it simply consumes the filter criteria from the filter bloc. &lt;/p&gt;

&lt;p&gt;⚠️ Don’t forget to close your ears when you’re done listening!&lt;br&gt;
Since we subscribed to another bloc’s stream, it’s critical to cancel that subscription in close(). Otherwise, it leads to memory leaks. Always clean up after your listeners to keep your app healthy and efficient!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;filterSubscription&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay now that we took care of everything, let's take a closer look on the List Bloc and how it actually handles the filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  New List Bloc with Two Lists
&lt;/h2&gt;

&lt;p&gt;Now that our filter data is flowing smoothly between blocs, we need to address one final architectural challenge: managing our filtered and unfiltered data efficiently.&lt;/p&gt;

&lt;p&gt;To maintain proper separation of concerns, our listing implementation uses two variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pokemonMap&lt;/code&gt;: Our source of truth that contains the complete, unfiltered collection of Pokémon&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filteredMap&lt;/code&gt;: A derived view that contains only the Pokémon matching our current filter criteria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This dual-map approach gives us significant performance advantages. We can apply complex filtering operations without repeatedly processing the entire dataset, and we maintain quick access to our original data when filters are cleared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply Filters Event
&lt;/h3&gt;

&lt;p&gt;There is one main event that handles filter changes — the &lt;code&gt;ApplyFilters&lt;/code&gt; event. Remember inside our &lt;code&gt;StreamSubscription&lt;/code&gt; when we secretly called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApplyFilters&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what's going on inside this event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onApplyFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;ApplyFilters&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_loadUnloadedFavoritePokemons&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_loadPokemonsBySelectedTypes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;_emitSuccessState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;currentMap:&lt;/span&gt; &lt;span class="n"&gt;isFiltering&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;filteredMap&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;PokemonListError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${AppStrings.failedToLoad}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;${e.toString()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// This is what's going on inside the emit success state:&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_emitSuccessState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;currentMap&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;pokemonMap:&lt;/span&gt; &lt;span class="n"&gt;currentMap&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;pagination:&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;favoritePokemons:&lt;/span&gt; &lt;span class="n"&gt;favoriteIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;showOnlyFavorites:&lt;/span&gt; &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;selectedTypes:&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes this special is that we don’t rebuild or mutate the original &lt;code&gt;pokemonMap&lt;/code&gt;. Instead, if filters are active, we work with a separate &lt;code&gt;filteredMap&lt;/code&gt;, leaving the full dataset intact.&lt;/p&gt;

&lt;p&gt;And here’s the big twist: even inside the event, we don’t slice and filter data manually. We simply check if the data we need already exists. If not? &lt;strong&gt;We fetch only what’s missing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No waste. No duplication. No rebuild.&lt;/p&gt;

&lt;p&gt;But where is the real filtering logic? Thought you'd never ask! It’s all deferred to the &lt;code&gt;PokemonListSuccess&lt;/code&gt; state. That’s where we dynamically compute what to show, based on the user's filters and our clean maps.&lt;/p&gt;

&lt;p&gt;Ready for the big reveal? I present you... &lt;strong&gt;THE NEW STATE&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonListSuccess&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonListState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;favoritePokemons&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pagination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;favoritePokemons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showOnlyFavorites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectedTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getPokemons&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PokemonDisplayHelper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDisplayedPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;pokemon:&lt;/span&gt; &lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;showOnlyFavorites:&lt;/span&gt; &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;selectedTypes:&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;pokemonMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;favoritePokemons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;selectedTypes&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of keeping filtered results in the state itself, we are making the state compute them on demand through the &lt;code&gt;getPokemons()&lt;/code&gt; method. Why? Because this approach has several powerful advantages. For example, our widgets (UI) only need to call &lt;code&gt;getPokemons(&lt;/code&gt;) without knowing about the filtering complexity behind the scenes. Plus, the state is pretty clean because we exclude the complex part into a helper.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter the Helper
&lt;/h3&gt;

&lt;p&gt;The real workhorse behind our filtering system is the &lt;code&gt;PokemonDisplayHelper&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonDisplayHelper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getDisplayedPokemons&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SortOption&lt;/span&gt; &lt;span class="n"&gt;sortBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SortOption&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;matchesFavorite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;showOnlyFavorites&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;matchesType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;selectedTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;matchesFavorite&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;matchesType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_sortPokemon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_sortPokemon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SortOption&lt;/span&gt; &lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;SortOption&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;SortOption&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;SortOption&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;pokemonA&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what's happening in our filtering method:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We start with values from our &lt;code&gt;Map&amp;lt;String, PokemonEntity&amp;gt;&lt;/code&gt; since we need to work with the actual objects&lt;/li&gt;
&lt;li&gt;We filter using Dart's &lt;code&gt;where()&lt;/code&gt; method, applying our filtering rules:

&lt;ul&gt;
&lt;li&gt;A Pokémon passes the favorite filter if either &lt;code&gt;showOnlyFavorites&lt;/code&gt; is false OR the Pokémon is marked as favorite&lt;/li&gt;
&lt;li&gt;A Pokémon passes the type filter if either no types are selected OR it has at least one matching type&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We combine these conditions with a logical AND &lt;code&gt;(&amp;amp;&amp;amp;)&lt;/code&gt;, so a Pokémon must satisfy both filters to be included&lt;/li&gt;
&lt;li&gt;Finally, we apply sorting to the filtered list based on the requested sort option&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The UI: Stupidly Simple
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonListing&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonListing&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BlocBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt; &lt;span class="nl"&gt;success:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PokemonListingSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPokemons&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PokemonListError&lt;/span&gt; &lt;span class="nl"&gt;error:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PokemonListingLoading&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No, seriously, that's the entire listing component! Just a simple &lt;code&gt;BlocBuilder&lt;/code&gt; that renders different widgets based on the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There is no need to dive deeper into the UI implementation. You now have all the business logic ready and you can use it however you want in your own projects. You're free to go and make the best out of these solid fundamentals we've created for filtering. If you want to see my complete approach including the UI components and styles, you can always check out the GitHub repository linked at the bottom of this blog.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_pokedex/tree/part-3-filters" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Ultimate Guide to Flutter Lists with Bloc : Part 2</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sun, 30 Mar 2025 20:04:57 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-2-1o67</link>
      <guid>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-2-1o67</guid>
      <description>&lt;h2&gt;
  
  
  Part 2 : Pagination &amp;amp; Infinite Scrolling
&lt;/h2&gt;

&lt;p&gt;Welcome back, Flutter devs! In &lt;a href="https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-1-2720"&gt;Part 1&lt;/a&gt;, we explored how to build clean and efficient listings using Flutter and Bloc, powered by the &lt;strong&gt;PokéAPI&lt;/strong&gt;. We successfully fetched data, displayed Pokémon cards, handled state with precision, and optimized our UI for performance.&lt;/p&gt;

&lt;p&gt;Now, in Part 2, we’re stepping it up. &lt;/p&gt;

&lt;p&gt;We’re going to implement infinite scrolling and smart pagination. Think about the experience you get on top-tier apps. You know what I am talking about. The list just keeps going, with zero loading interruptions, and new content appears right as you need it. Well, that’s what we’re building.&lt;/p&gt;

&lt;p&gt;We’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to detect when to load more items&lt;/li&gt;
&lt;li&gt;How to fetch additional data behind the scenes&lt;/li&gt;
&lt;li&gt;How to prevent janky rebuilds or duplicate fetches&lt;/li&gt;
&lt;li&gt;And how to structure this cleanly inside Bloc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, your Pokémon listing will feel instant, scalable, and app-store ready. Let’s dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s the Problem with Basic Pagination?
&lt;/h2&gt;

&lt;p&gt;The classic approach to pagination usually goes something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wait until the user scrolls to the bottom&lt;/li&gt;
&lt;li&gt;Show a loading spinner&lt;/li&gt;
&lt;li&gt;Fetch the next batch of items&lt;/li&gt;
&lt;li&gt;Append them to the list&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And technically… that works. But it’s not the kind of experience users expect anymore. Think about the apps you use every day. Instagram, TikTok, Ebay... You never see a loader. &lt;strong&gt;You never wait&lt;/strong&gt;. You just scroll, and things appear as if they were already there.&lt;/p&gt;

&lt;p&gt;That’s because these apps don't just paginate, they anticipate. And this is exactly what we are going to do as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We’re Building
&lt;/h3&gt;

&lt;p&gt;In this part, we’re going to build a prefetching system. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ll load more items before the user hits the bottom&lt;/li&gt;
&lt;li&gt;You’ll fetch multiple pages ahead in the background&lt;/li&gt;
&lt;li&gt;You’ll make the scroll feel continuous and uninterrupted&lt;/li&gt;
&lt;li&gt;And most importantly, you’ll do it all cleanly with Bloc and efficient memory usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach gives you the best of both worlds: great UX and great performance at the same time. Let's go!&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination the right way
&lt;/h2&gt;

&lt;p&gt;To implement pagination, we need a new Bloc event. Unlike the initial fetch, this event is triggered when we want to load more data, either while scrolling or during background prefetching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FetchNextPage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonListEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to track the progress of the list
&lt;/h3&gt;

&lt;p&gt;Here's the thing:&lt;/p&gt;

&lt;p&gt;When you add new items to your list and emit a state, Bloc (via Equatable) uses &lt;strong&gt;props&lt;/strong&gt; to determine if the state has changed. If your list grows but the object reference stays the same, Bloc assumes nothing changed, and doesn’t rebuild the UI. So, many developers solve this by doing something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works? Because in our Bloc, we have set our props to look on the &lt;code&gt;pokemons&lt;/code&gt; value. So, by implementing the &lt;code&gt;List.of&lt;/code&gt; we create a new instance of the list. The Bloc sees a new instance, and as a result we get a new state with the updated list.&lt;/p&gt;

&lt;p&gt;Does this work? Yes. Do we like it? &lt;strong&gt;Of course not&lt;/strong&gt;. Let me explain you the reason.&lt;/p&gt;

&lt;p&gt;With this approach, you're copying the entire list every time, even when just a few items are added. If you’ve got 1,000+ Pokémon already loaded, that’s an expensive operation for no real reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Better Way: Emit a Version Counter
&lt;/h3&gt;

&lt;p&gt;Instead of copying the list, we introduce a simple int pagination value that only increases when the list changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonListSuccess&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonListState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, whenever we fetch more data or toggle a favorite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;pagination:&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By increasing the pagination, we trigger the &lt;strong&gt;emit&lt;/strong&gt; every time we make a change in our list.&lt;br&gt;
What this gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need to copy lists (which saves memory and processing time)&lt;/li&gt;
&lt;li&gt;Bloc still detects a state change because pagination changes&lt;/li&gt;
&lt;li&gt;UI rebuilds as expected, even though the list reference stays the same&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Update Bloc and Events
&lt;/h3&gt;

&lt;p&gt;Because the logic of the initial fetch is exactly the same with every fetch we will execute in any page, we will separate the &lt;code&gt;fetch pokemon&lt;/code&gt; logic outside of the events. Good old &lt;em&gt;DRY&lt;/em&gt; logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;chosenLimit&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;PokemonRepositories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPokemonUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;limit:&lt;/span&gt; &lt;span class="n"&gt;chosenLimit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;offset:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonUrls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchedPokemons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pokemonUrls&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PokemonRepositories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPokemonDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

        &lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetchedPokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;

        &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chosenLimit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;pagination:&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;"There are no more pokemons"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;"Failed to fetch Pokémon: &lt;/span&gt;&lt;span class="si"&gt;${e.toString()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all you have to do, is call this function from the events we created, and keep track of the key variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pagination (for triggering new state changes)&lt;/li&gt;
&lt;li&gt;offset (set the starting point of the next fetch, from the end of the last one)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListInitial&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InitialFetch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onInitialPokemonFetch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onFetchNextPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_onToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onInitialPokemonFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;InitialFetch&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;chosenLimit:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstPokemon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onFetchNextPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;FetchNextPage&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pagination with an int
&lt;/h3&gt;

&lt;p&gt;Here’s the magic line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of copying and re-emitting the whole list (which could already have hundreds of items), we simply bump a counter.&lt;/p&gt;

&lt;p&gt;This triggers a new Bloc state, and thanks to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…the UI rebuilds as expected. Enough with the Bloc, I think it's time to take a look in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI implementation
&lt;/h2&gt;

&lt;p&gt;Now that our Bloc handles everything, it’s time to connect the logic to the UI and turn this into a scrollable, preloading Pokédex that feels instant and endless. Let's break down the logic in small parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: When should I fetch extra Pokémon?
&lt;/h3&gt;

&lt;p&gt;When using &lt;code&gt;ListView.builder&lt;/code&gt;, we gain access to something really useful: the &lt;strong&gt;index&lt;/strong&gt; of each item as it’s being built. As we mentioned in Part 1, &lt;code&gt;ListView.builder&lt;/code&gt; is highly memory-efficient, it doesn’t build the entire list at once, just what's visible on screen plus a few more. This gives us the perfect opportunity to detect when the user is getting close to the end of the list and dispatch an event to fetch more Pokémon. Here's a basic example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;cacheExtent:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Keep in mind, we initially fetch 50 Pokémon, that's why we start fetching the next page on Pokémon #30&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, when the user reaches at the pokemon #30, the &lt;code&gt;FetchNextPage()&lt;/code&gt; will execute, and we will add 30 more Pokémon to the list. Amazing right? Something is missing though...&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 2: Avoid fetching the same page twice
&lt;/h3&gt;

&lt;p&gt;The logic above works, but there’s one problem. If the &lt;code&gt;ListView.builder&lt;/code&gt; keeps building items beyond index 30 (and it will), the condition &lt;code&gt;if (index &amp;gt;= 30)&lt;/code&gt; will be true forever. That means we’ll keep dispatching the FetchNextPage event over and over, even if we already fetched it.&lt;/p&gt;

&lt;p&gt;To solve this, we need to add a simple guard. A local boolean flag that prevents multiple fetches at once. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFetchingMore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, once the new data comes in, we reset the flag to false using a BlocListener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;BlocListener&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;listener:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okat now let's take a step back and see what we have accomplished: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We fetch new Pokémon before the user reaches the bottom of the list&lt;/li&gt;
&lt;li&gt;We prevent duplicate fetches by tracking the loading state &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now, to complete the perfect infinite scroll experience, we’re going to add one final enhancement. &lt;strong&gt;Prefetching a few pages in advance during the initial load.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 3: Prefetch and Infinite Scrolling
&lt;/h3&gt;

&lt;p&gt;The idea is simple: instead of fetching just one batch of Pokémon when the app starts, we fetch multiple pages ahead in the background. This way, we’re always staying 2–3 pages ahead of the user, so they never catch up to the data or hit a visible loading state.&lt;/p&gt;

&lt;p&gt;It looks like this in the Bloc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onInitialPokemonFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;InitialFetch&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;chosenLimit:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstPokemon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// First page&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Page 2&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Page 3&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_fetchPokemons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Page 4&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These additional fetches happen immediately after the first one, giving the user a smooth experience from the very first scroll gesture.&lt;/p&gt;

&lt;p&gt;Now, when the user reaches index 200, the app already has Pokémon 201–300 ready to go. No delays. No spinners. Just scroll and enjoy.&lt;/p&gt;

&lt;p&gt;Finally, apply these small adjustments to the UI, because we want to stay ahead of the user like we already said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
              &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFetchingMore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final screen should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonListingSuccess&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonListingSuccess&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListingSuccess&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_PokemonListingSuccessState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_PokemonListingSuccessState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListingSuccess&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BlocListener&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;listener:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;cacheExtent:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
              &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFetchingMore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;isFetchingMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FetchNextPage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;margin:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AppSpacing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;PokemonCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;ValueKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="nl"&gt;pokemon:&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's it everybody! We have successfully delivered the best scrolling experience using Flutter + Bloc, completely from scratch. Not only did we manage to trigger pagination based on the list index, but we also &lt;strong&gt;prefetched multiple pages upfront&lt;/strong&gt; to provide a truly seamless user experience.&lt;/p&gt;

&lt;p&gt;This isn’t just a pagination strategy, it’s the foundation of how modern apps deliver seamless, high-performance data loading at scale.&lt;/p&gt;

&lt;p&gt;Whether you're listing products, social feeds, restaurants, or Pokémon, this is a blueprint you can reuse again and again.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_pokedex/tree/part-2-pagination" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Ultimate Guide to Flutter Lists with Bloc : Part 1</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sun, 23 Mar 2025 15:18:02 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-1-2720</link>
      <guid>https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-1-2720</guid>
      <description>&lt;h2&gt;
  
  
  Part 1 : Introduction to Listings
&lt;/h2&gt;

&lt;p&gt;Hello Flutter people! I’m back with a brand-new Flutter blog series, and this one is going to be special. We’re diving into a topic you’ll find in just about every real-world app: &lt;strong&gt;listings&lt;/strong&gt;. Whether you’re displaying products, contacts, messages, or in our case, Pokémon &lt;em&gt;(a.k.a. the greatest thing in the world)&lt;/em&gt;, building smooth, scalable lists is a core skill for any Flutter developer.&lt;/p&gt;

&lt;p&gt;In this first part of the series, we’ll focus on how to fetch and display data using Flutter + Bloc, and build a list that updates in real time, handles state efficiently, and delivers a great user experience. We’ll dive into ListView.builder, state management patterns, performance optimizations, and even sprinkle in some UI polish, all using the &lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; as our data source.&lt;/p&gt;

&lt;p&gt;Ready to catch ’em all? Or should I say... display them all? Eh, forget about it, let’s get started with our Pokédex. 😅&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What are we building?
&lt;/h2&gt;

&lt;p&gt;Before we dive into the code, let’s take a moment to understand what we’re building and how it all comes together.&lt;/p&gt;

&lt;p&gt;We’ll be using the open-source &lt;a href="https://pokeapi.co/" rel="noopener noreferrer"&gt;PokéAPI&lt;/a&gt; to fetch and display a list of Pokémon. Each card will include the Pokémon’s name, image, number, and types. As a little bonus, we’ll also make each card interactive by adding a favorite icon that users can toggle on or off, and all of this will be handled using Bloc for state management.&lt;/p&gt;

&lt;p&gt;Now, don’t worry too much about the UI or styling. This blog will focus on data flow, and state handling. I’ll provide a link to the full GitHub repo at the end of the post with all the code, so you can dig in when you're ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does the PokéAPI Work?
&lt;/h3&gt;

&lt;p&gt;The PokéAPI gives us everything we need, but not in a single request. Instead, we’ll use two API calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first call gives us a list of Pokémon URLs, just enough to know which ones are available.&lt;/li&gt;
&lt;li&gt;The second call fetches the details of each Pokémon individually using those URLs (name, ID, types, image, and more).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It might sound like extra work, but trust me, this gives us the flexibility to load detailed data only when we need it. It’s a great example of working with a two-step API flow, which you’ll run into often when working with public APIs or pagination.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do we handle those API calls?
&lt;/h2&gt;

&lt;p&gt;Like I mentioned earlier, we will use Bloc to handle our listing state. The states we will need are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PokemonListInitial&lt;/code&gt; → the default state before anything happens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PokemonListLoading&lt;/code&gt; → shown while data is being fetched&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PokemonListSuccess&lt;/code&gt; → returns the full list of Pokémon&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PokemonListError&lt;/code&gt; → triggered when something goes wrong (e.g. no data, API failed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As simple as it gets right? It gets even simpler when it comes to events. We only need a single event to kick off the initial fetch when the app launches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InitialFetch&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonListEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;firstPokemon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InitialFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstPokemon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;int&lt;/code&gt; represents how many Pokémon we want to fetch on the first load. This lets us tell the Bloc:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Hey, fetch the first n Pokémon starting from this index.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Perfect setup for pagination later on 👀 🫢 &lt;em&gt;(spoiler for Part 2?)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s look at the event handler that handles this logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onInitialPokemonFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;InitialFetch&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListLoading&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pokemonUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;PokemonRepositories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPokemonUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstPokemon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemonUrls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchedPokemons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pokemonUrls&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PokemonRepositories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPokemonDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

        &lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetchedPokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whereType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="n"&gt;pokemonList&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;"There are no more pokemons"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;"Failed to fetch Pokémon: &lt;/span&gt;&lt;span class="si"&gt;${e.toString()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what’s happening, step-by-step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We emit the loading state to let the UI know something is happening in the background.&lt;/li&gt;
&lt;li&gt;We fetch all the Pokémon URLs based on the number provided inside the PokemonRepositories.&lt;/li&gt;
&lt;li&gt;We fetch the full details for each of those URLs (in parallel) using Future.wait.&lt;/li&gt;
&lt;li&gt;We filter out any nulls (just in case) and add the valid Pokémon to our list.&lt;/li&gt;
&lt;li&gt;We emit the &lt;code&gt;PokemonListSuccess&lt;/code&gt; state with the final list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the way, this is how this Pokémon model looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PokemonEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;PokemonEntity&lt;/span&gt; &lt;span class="n"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PokemonEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;types:&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;image:&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;isFavorite:&lt;/span&gt; &lt;span class="n"&gt;isFavorite&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Do We Display the Pokémon in UI?
&lt;/h2&gt;

&lt;p&gt;Once we have the data ready, it’s time to put it on the screen. And for this, we’ll use one of Flutter’s most powerful and efficient widgets: &lt;strong&gt;ListView.builder&lt;/strong&gt;. But why this one?&lt;/p&gt;

&lt;p&gt;Because unlike a regular &lt;code&gt;Column&lt;/code&gt; or &lt;code&gt;ListView(children: [...])&lt;/code&gt;, &lt;strong&gt;ListView.builder&lt;/strong&gt; doesn’t build everything at once. It only builds the widgets that are currently visible on screen, and recycles them as you scroll.&lt;/p&gt;

&lt;p&gt;This makes it perfect for long or infinite lists. Here is a quick breakdown of how everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;margin:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AppSpacing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;PokemonCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="nl"&gt;pokemon:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;itemCount&lt;/code&gt; tells Flutter how many items it should expect&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;itemBuilder&lt;/code&gt; gets called only when a new widget is needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flutter recycles previously built widgets as you scroll, this is why &lt;strong&gt;ListView.builder&lt;/strong&gt; is so memory-efficient. This is pretty awesome if you ask me. But can we make it even more awesome?? Well, in Flutter everything is possible.&lt;/p&gt;

&lt;p&gt;What if I told you... we could control how many pixels ahead Flutter preloads your list items?&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter the cacheExtent ...
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;cacheExtent&lt;/code&gt; controls the number of pixels outside the visible area that Flutter will preload list items. You can think of it like a "scroll buffer." If the screen height is 800 pixels and your &lt;code&gt;cacheExtent&lt;/code&gt; is set to 300, Flutter will start building list items 300 pixels before they become visible. &lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;cacheExtent&lt;/code&gt; is set to 250 pixels, which works fine in many cases. But depending on your UI, this number might need some tuning. This is a powerful tool, but it comes with responsibility. It’s important to adjust it based on your specific scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your list items are small, lightweight, or don’t use heavy assets like images, the default value might be perfect.&lt;/li&gt;
&lt;li&gt;If your list items are larger, or you're using images, shimmer loaders, or animated widgets, you may want to increase the cacheExtent to something like &lt;strong&gt;300–600&lt;/strong&gt; pixels for a smoother UX.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;⚠️ Tip: Going too high on &lt;code&gt;cacheExtent&lt;/code&gt; may lead to increased memory usage, especially with complex lists. Try to keep it below &lt;strong&gt;600&lt;/strong&gt; unless you’ve benchmarked for your specific case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extra Pro Tip: Add Keys to Your List Items
&lt;/h3&gt;

&lt;p&gt;Why use a key? Because it gives Flutter a reliable way to track which widget is which, especially when your list updates dynamically. Without keys, Flutter identifies widgets by their position in the list (index). But once your list starts changing (like adding, deleting, or reordering items) relying on the index becomes dangerous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common issue:&lt;/strong&gt; You delete the 3rd item in your data, expecting it to be the Pokémon with ID &lt;code&gt;0003&lt;/code&gt;.&lt;br&gt;
But Flutter sees "item at index 2" and reuses the wrong widget, maybe deleting or updating the wrong card.&lt;/p&gt;

&lt;p&gt;It might sound like a small difference, but it's a bug with a big UX impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution?&lt;/strong&gt; Use a Unique Key Per Item&lt;/p&gt;

&lt;p&gt;Now Flutter knows exactly which widget maps to which Pokémon, even if the order changes, or the list is updated.&lt;/p&gt;

&lt;p&gt;The final List should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;cacheExtent:&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;margin:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AppSpacing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;md&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;PokemonCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;ValueKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="nl"&gt;pokemon:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that our Pokémon list is up and running, let’s make it feel more alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toggling Favorites Pokémon
&lt;/h2&gt;

&lt;p&gt;We’re going to add the ability to toggle a favorite icon on each Pokémon card, and we’ll do it the right way. Using Bloc, immutability, and the power of Equatable.&lt;/p&gt;

&lt;p&gt;This isn’t just about clicking a heart icon.&lt;br&gt;
It’s about learning how to update a single item in a list, emit the right Bloc state, and rebuild only what’s necessary.&lt;/p&gt;

&lt;p&gt;First of all, we start by defining a new event in our Bloc. This event simply carries the pokemonId of the card we want to toggle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToggleFavoriteStatus&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PokemonListEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;pokemonId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemonId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pokemonId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we add this event inside our &lt;code&gt;PokemonListBloc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;_onToggleFavoriteStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;ToggleFavoriteStatus&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PokemonListState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;updatedList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokemonId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;isFavorite:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFavorite&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PokemonListSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;pokemons:&lt;/span&gt; &lt;span class="n"&gt;updatedList&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, even though we’re emitting the same state class, you might be wondering:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Why does this actually work? Shouldn’t Bloc ignore it since we didn’t change the state type?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Great question, and this is where Equatable and immutability come into play.&lt;/p&gt;

&lt;p&gt;When we emit the new PokemonListSuccess state, we’re passing in a brand-new list instance with a modified Pokémon object inside it. Since our Bloc state class overrides props like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pokemons&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bloc uses Equatable to compare the new list against the previous one. And because it’s a different list object (new reference), Bloc considers it a new state, and triggers the rebuild.&lt;/p&gt;

&lt;p&gt;Flutter then efficiently rebuilds only the widgets that need updating, thanks to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ListView.builder reusing widgets&lt;/li&gt;
&lt;li&gt;Our use of ValueKey(pokemon.id) to help Flutter track which widgets changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the power of immutability and Equatable working together. You change only what’s necessary, and Flutter takes care of the rest.&lt;/p&gt;

&lt;p&gt;So yes, we’re using the same state type (&lt;code&gt;PokemonListSuccess&lt;/code&gt;) again, but with new data inside, and that’s exactly what Bloc is built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up Part 1
&lt;/h2&gt;

&lt;p&gt;And there you have it! We’ve built a clean, efficient, and scalable listing system using Bloc, Equatable, and Flutter’s powerful rendering engine.&lt;/p&gt;

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

&lt;p&gt;From setting up the API flow with the PokéAPI, to handling list state with Bloc, and finally making the list interactive with favorite toggles, we’ve covered the essential concepts that power most real-world listing features in mobile apps.&lt;/p&gt;

&lt;p&gt;This wasn’t just about displaying some data. We explored how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage state cleanly with immutable patterns&lt;/li&gt;
&lt;li&gt;Trigger UI updates the smart way with Equatable&lt;/li&gt;
&lt;li&gt;Optimize list rendering with ListView.builder, keys, and cacheExtent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve followed along this far, congratulations! You’ve just added another level of skill to your Flutter toolbox. But we have a lot more work to do if we want to create the most complete and efficient Pokédex. In the &lt;a href="https://dev.to/thanasistraitsis/the-ultimate-guide-to-flutter-lists-with-bloc-part-2-1o67"&gt;next part&lt;/a&gt;, we will cover the pagination feature. Stay tuned!&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_pokedex/tree/part-1-listing" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to reach out if you have any questions or need further guidance. Also, I would love to see if you have any ideas for the next parts. What features would you like to add on this Pokémon journey? &lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Flutter Flip Card Animation</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sun, 02 Feb 2025 15:41:53 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/flutter-flip-card-animation-3jb4</link>
      <guid>https://dev.to/thanasistraitsis/flutter-flip-card-animation-3jb4</guid>
      <description>&lt;p&gt;Welcome fellow Flutter developers! Today, we’re diving into something really exciting: building a flip card animation from scratch. You know those satisfying card-flip effects you see in card board-games and bank applications? We'll create that using nothing but Flutter's built-in animation tools. No external packages needed! And don't worry, this is not an ordinary "copy-paste the code" article. We'll break down the animation principles that make this effect work, giving you the knowledge to create not just this flip animation, but any custom animation your app needs. Ready to upgrade your Flutter apps? Let's start by answering a question that I think you are all wondering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why No Packages?
&lt;/h3&gt;

&lt;p&gt;There are plenty of packages in Flutter that can help you create a flip animation in no time. But when you build it yourself, you gain a much better understanding of how animations work under the hood in Flutter. Plus, building it from scratch means you have full control and flexibility over the animation. You can customize it in ways that might not be possible with pre-built solutions.&lt;/p&gt;

&lt;p&gt;Now that we got that out of the way, time to build our foundation. We will use a very simple UI because we want to focus on the animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI Setup
&lt;/h2&gt;

&lt;p&gt;Before diving into animations, let's create the basic building blocks of our card. We'll start with a simple card component that will represent the front face of the card.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;FrontCard&lt;/code&gt; widget, is just a basic card layout with a title and some placeholder text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FrontCard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;margin:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;border:&lt;/span&gt; &lt;span class="n"&gt;Border&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;235&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;crossAxisAlignment:&lt;/span&gt; &lt;span class="n"&gt;CrossAxisAlignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;mainAxisSize:&lt;/span&gt; &lt;span class="n"&gt;MainAxisSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"💳 Flip the card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="s"&gt;"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step would be creating our &lt;code&gt;CardContainer&lt;/code&gt; widget inside our Home Screen to properly position this card. As I said, we will keep the UI as simple as possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardContainer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Basic Logic
&lt;/h2&gt;

&lt;p&gt;Let's build out the core mechanics of our flip card feature. Think of it like setting up a basic two-sided playing card - we can't see both sides at once, but we need to be able to switch between them.&lt;/p&gt;

&lt;p&gt;First, we'll make a &lt;code&gt;BackCard&lt;/code&gt; widget that mirrors our &lt;code&gt;FrontCard&lt;/code&gt; structure but with different content and styling. This gives us a clear visual distinction between the front and back states of our card.&lt;/p&gt;

&lt;p&gt;With both card faces ready, we need a way to switch between them. We'll transform our &lt;code&gt;CardContainer&lt;/code&gt; into a stateful widget that tracks which side is currently visible using a boolean flag. When a user taps the card, we'll toggle between the front and back faces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardContainer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_CardContainerState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CardContainerState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fbasic_logic.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fbasic_logic.png%3Fraw%3Dtrue" alt="Basic Logic" width="1672" height="1582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Flip Animation
&lt;/h2&gt;

&lt;p&gt;Enough with the boring part, don't you think? Now that we’ve established the basic logic for toggling between the front and back sides of our card, let’s take things to the next level by adding a smooth flip animation. Rather than instantly switching between the two card faces, we’ll implement a visually engaging transition that mimics the natural motion of a card flipping through space. &lt;/p&gt;

&lt;p&gt;In this section, we’ll break down the animation mechanism step by step, explain how the code works, and highlight the key parts that bring the flip animation to life.&lt;/p&gt;

&lt;h3&gt;
  
  
  Animation Controller
&lt;/h3&gt;

&lt;p&gt;To bring our card flip animation to life, we need an &lt;code&gt;AnimationController&lt;/code&gt;. This is the backbone of the animation mechanism in Flutter, giving us complete control over how and when the animation plays. With a controller we can manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The duration of the animation.&lt;/li&gt;
&lt;li&gt;The flow of the animation. For example, we can &lt;strong&gt;start&lt;/strong&gt;, &lt;strong&gt;stop&lt;/strong&gt;, &lt;strong&gt;reverse&lt;/strong&gt;, or &lt;strong&gt;repeat&lt;/strong&gt; the animation as needed.&lt;/li&gt;
&lt;li&gt;Synchronization with the screen. By using the &lt;code&gt;vsync&lt;/code&gt; keyword, we ensure that the animation we build is tied with the screen's refresh rate system.&lt;/li&gt;
&lt;li&gt;Every single value of the animation from the beginning to the very end. The controller has a range of values from &lt;strong&gt;0&lt;/strong&gt; to &lt;strong&gt;1&lt;/strong&gt;, pointing the start and the finish of the animation, giving us full access of the whole proccess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, we will use our AnimationController to rotate the &lt;code&gt;FrontCard&lt;/code&gt; 180°, but first we need to initialize it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt; &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Animation lasts 2 seconds&lt;/span&gt;
      &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Syncs with the screen refresh rate&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you might see an error on your screen because of the &lt;code&gt;this&lt;/code&gt; keyword in the &lt;code&gt;vsync&lt;/code&gt; parameter. If your editor seems confused, don’t worry, this is a common issue. Essentially, your program is saying, &lt;em&gt;"Okay, what the f...k is this?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Relax, dear editor. We can fix this pretty easily by adding a mixin to our class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CardContainerState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;SingleTickerProviderStateMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you’re feeling ambitious and want multiple animations in the same widget, just remove the word "Single" from the mixin, and you’re good to go. (Yes, it’s that easy!)&lt;/p&gt;

&lt;h3&gt;
  
  
  Animation Setup
&lt;/h3&gt;

&lt;p&gt;With the &lt;strong&gt;AnimationController&lt;/strong&gt; set up, we are ready to build the actual animation. This is where &lt;code&gt;Tween&lt;/code&gt; comes into play. The &lt;code&gt;Tween&lt;/code&gt; help us provide the actual values of our animation. Remember in the previous section, where we said that we can define the values from the beginning to the end? This is the place where we can set these values. In our case, we need to start from &lt;strong&gt;0°&lt;/strong&gt;, and produce half a circle, which means &lt;strong&gt;180°&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt; &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Animation lasts 2 seconds&lt;/span&gt;
      &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Syncs with the screen refresh rate&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;begin:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ---&amp;gt; 0°&lt;/span&gt;
      &lt;span class="nl"&gt;end:&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ---&amp;gt; 180°&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Place the animation controller here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But wait, what is this &lt;code&gt;pi&lt;/code&gt;? Thought you never ask... TIME FOR SOME MATHS!&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Do We Rotate the Card to π?
&lt;/h4&gt;

&lt;p&gt;The value of π radians (equivalent to 180°) represents half of a full rotation. By transitioning to this value, we create the illusion of flipping the card over to reveal its back side. This provides the visual "turning" motion that feels natural.&lt;/p&gt;

&lt;p&gt;We have access to this value by importing the &lt;code&gt;dart:math&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:math'&lt;/span&gt; &lt;span class="kd"&gt;show&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fcircle_board.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fcircle_board.png%3Fraw%3Dtrue" alt="Board Image" width="1693" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Perfect, now all that's left, is updating our UI, so we can apply this amazing animation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating UI
&lt;/h3&gt;

&lt;p&gt;Connecting the animation logic with the UI, will not work with the simple widgets. We need something specifically for this job. Enter the &lt;code&gt;Transform&lt;/code&gt; widget, which allows us to manipulate the visual properties of our card, like its position, size, and most importantly—its rotation.&lt;/p&gt;

&lt;p&gt;But in order to visualize our animation, we will need another Widget, the &lt;code&gt;AnimationBuilder&lt;/code&gt; which will recreate the UI based on the animation. This widget listens to changes in the animation’s value and rebuilds the UI at every frame, ensuring the flip appears smooth and fluid.&lt;/p&gt;

&lt;p&gt;Here’s how it all comes together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;animation:&lt;/span&gt; &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Perspective for the realistic flip of the card&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know that the &lt;code&gt;Transform&lt;/code&gt; widget, might look a bit confusing to you, so let's break it down line by line.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alignment.center:&lt;/strong&gt; This ensures the card rotates around its center point. Without this, the rotation might pivot from an awkward position, like the top-left corner. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;setEntry:&lt;/strong&gt; Adds a perspective effect, simulating 3D depth. Without this line, the card flip would look flat, as if it were rotating in 2D space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rotateY:&lt;/strong&gt; Rotates the card along the Y-axis based on the animation progress. This line drives the actual flipping motion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The image below, can help you visualize everything we discussed above:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fflip_card.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_card_flip%2Fblob%2Fmain%2Fassets%2Fflip_card.png%3Fraw%3Dtrue" alt="Rotation Image" width="1693" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Completing the animation - Switching to BackCard
&lt;/h3&gt;

&lt;p&gt;Our flip animation is looking amazing! However, there's one last thing that needs to be fixed. We only ever see the &lt;code&gt;FrontCard&lt;/code&gt;. Even though the animation flips smoothly, the back side never appears because we never swap the UI to display the &lt;code&gt;BackCard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To achieve this swap seamlessly, we need to find the most efficient point in time during the animation, and change our two widgets. If you think about it, there is a very specific moment, where we cannot see the face of the card, because it is turned sideways. That's our chance to act!&lt;/p&gt;

&lt;h4&gt;
  
  
  How Do We Implement This?
&lt;/h4&gt;

&lt;p&gt;We use an animation listener on our AnimationController to detect when the animation reaches its halfway point. When it does, we toggle the isFront boolean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardContainer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_CardContainerState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CardContainerState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;SingleTickerProviderStateMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt; &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;Animation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;begin:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;end:&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add a listener to the animation controller&lt;/span&gt;
    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Check if animation is at halfway point (π / 2)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Switch to back card&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Switch to front card&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;animation:&lt;/span&gt; &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Rotate 180° for the starting position of the back card&lt;/span&gt;
                &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we move on, let’s address one final detail. If you look at the code, you'll notice that when displaying the &lt;code&gt;BackCard&lt;/code&gt;, we apply an additional 180° rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Rotate 180° for the starting position of the back card&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why is this necessary?
&lt;/h4&gt;

&lt;p&gt;Since our animation only rotates the card from 0 to π (180°), we need to ensure that the BackCard is correctly oriented when it appears.&lt;/p&gt;

&lt;p&gt;Without this extra rotation, the &lt;code&gt;BackCard&lt;/code&gt; would appear upside down when it replaces the &lt;code&gt;FrontCard&lt;/code&gt;. By pre-rotating the &lt;code&gt;BackCard&lt;/code&gt; by π (180°), we align it properly so that when it becomes visible, it appears in the correct orientation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping it up
&lt;/h2&gt;

&lt;p&gt;And there you have it! You’ve successfully developed and fully understand how to create a smooth and realistic &lt;strong&gt;flip animation&lt;/strong&gt; in Flutter. More importantly, you now grasp the fundamentals of &lt;strong&gt;3D&lt;/strong&gt; transformations, perspective effects, and animation control.&lt;/p&gt;

&lt;p&gt;With this knowledge, you can level up your application by incorporating this awesome effect into any UI element like flashcards, game mechanics, or stylish flipping menus.&lt;/p&gt;

&lt;p&gt;Buuuuut before you go...allow me to add one last thing to this tutorial. I would like to include everything around this flip animation, so whenever you step into something challenging while implementing something similar, this would be the perfect place to jump back into and find the solution you need. I promise it's the last topic we cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Card Views with Different Heights – The Asymmetric Flip
&lt;/h3&gt;

&lt;p&gt;This is a pretty special case. You will almost never need an implementation like this in a typical app. However, if you ever find yourself needing an asymmetric card flip animation, where the front and back of the card have different heights, this section has you covered.&lt;/p&gt;

&lt;p&gt;I know what you’re thinking: &lt;em&gt;"Why would anyone need this?"&lt;/em&gt; Well, trust me, there are people out there desperately looking for this solution (I was one of them).&lt;/p&gt;

&lt;h4&gt;
  
  
  The Problem
&lt;/h4&gt;

&lt;p&gt;Normally, a card flip animation assumes that both sides of the card are the same size. However, in some UI designs, the back card might be larger because it contains more content. This creates an issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If we use a fixed height, one side might be cut off.&lt;/li&gt;
&lt;li&gt;If we let the card resize mid-flip, the content above and below the card will move everytime the user flips the card, providing a very bad user experience to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution? We need to measure both cards' heights before displaying them and then set a consistent maximum height for the animation.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Solution
&lt;/h4&gt;

&lt;p&gt;Here is how we solve our asymmetric problem in a few simple steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;GlobalKeys&lt;/code&gt; for both Front and Back widgets, so you can measure their heights&lt;/li&gt;
&lt;li&gt;Store the maximum height inside the &lt;code&gt;maxHeight&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;OffStage&lt;/code&gt; widget in order to build the cards (so we can measure their heights), but without displaying them to the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how the final code will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:card_rotation/card/back_card.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:card_rotation/card/front_card.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:math'&lt;/span&gt; &lt;span class="kd"&gt;show&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardContainer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_CardContainerState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CardContainerState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CardContainer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;SingleTickerProviderStateMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt; &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;Animation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;begin:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;end:&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;isFront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Measure the heights after the first layout phase&lt;/span&gt;
    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;measureHeights&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt; &lt;span class="n"&gt;_frontCardKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// GlobalKey for FrontCard&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt; &lt;span class="n"&gt;_backCardKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// GlobalKey for BackCard&lt;/span&gt;

  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;frontCardHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;backCardHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;maxHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Function to measure heights of FrontCard and BackCard&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;measureHeights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;frontRenderBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;_frontCardKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderBox&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;backRenderBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;_backCardKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderBox&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frontRenderBox&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;backRenderBox&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;frontCardHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frontRenderBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;backCardHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backRenderBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;maxHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;frontCardHeight&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;backCardHeight&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;frontCardHeight&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;backCardHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;Offstage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;_frontCardKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Attach the GlobalKey here&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Offstage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;_backCardKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Attach the GlobalKey here&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="n"&gt;maxHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isFront&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_cardFlipController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;animation:&lt;/span&gt; &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="n"&gt;_cardFlipAnimation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;isFront&lt;/span&gt;
                  &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FrontCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                  &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="nl"&gt;transform:&lt;/span&gt; &lt;span class="n"&gt;Matrix4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rotateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;BackCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we have to mention that this is not a space-efficient solution. We create two extra widgets (&lt;code&gt;FrontCard&lt;/code&gt; and &lt;code&gt;BackCard&lt;/code&gt;) that never actually appear on the screen. Overloading the app with unused UI elements is bad practice and can lead to performance issues, especially in more complex applications. As a developer, your goal should always be to write efficient, scalable code. However, sometimes, special problems require special solutions, and that’s okay as long as you understand the trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you have it everyone. Another Flutter tutorial has come to an end. I hope this guide helped you find exactly what you were looking for when searching for a &lt;strong&gt;Card Flip Animation&lt;/strong&gt;. We covered everything from the fundamentals of an &lt;code&gt;AnimationController&lt;/code&gt; to the explanation of 3D transformations in Flutter. Along the way, we explored perspective effects, UI swapping, and even handling asymmetric card flips, leaving no stone unturned!&lt;/p&gt;

&lt;p&gt;Now, you're fully equipped with a solid understanding of Flutter animations. Whether you want to implement a card flip, or even more complex 3D animations, you have all the tools you need to bring your ideas to life. Until we meet again on another coding adventure...Happy Coding!!&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_card_flip" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to reach out if you have any questions or need further guidance. Cheers to your future Flutter projects!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>animation</category>
    </item>
    <item>
      <title>Flutter Bloc Transformer Events</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Mon, 30 Dec 2024 10:12:53 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/flutter-bloc-transformer-events-2hph</link>
      <guid>https://dev.to/thanasistraitsis/flutter-bloc-transformer-events-2hph</guid>
      <description>&lt;p&gt;Hey there, Flutter devs! I see you. You've been crafting that new feature, but something feels... off. You know you got the logic right, it's pretty clear in that well-structured code you wrote. Yet somehow, your Bloc events seem to have a mind of their own. Why aren't they behaving the way you know they should?&lt;/p&gt;

&lt;p&gt;Don't worry—you're not alone. Today, we're diving into Bloc transformer events. What if I told you that in every Bloc event you build, there is a transformer running behind the scene. And what If I take it a step further and tell you that there are not one but &lt;strong&gt;FOUR&lt;/strong&gt; transformers you can choose from. These little operators can drastically shape how your events are processed, and trust me, they can make or break your flow.&lt;/p&gt;

&lt;p&gt;So, it's time for another debugging adventure. Jump back to your code and let's solve the mystery of transformer events. By the end, you'll wonder how you ever lived without them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Transformer Events
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Concurrent
&lt;/h3&gt;

&lt;p&gt;When you create a Bloc and start creating events, concurrent is the transformer working behind the scenes. This is the default behaviour of Bloc. It's a pretty standard way of executing the code and a really convenient one. Every event operate independently, without affecting the behaviour of others.&lt;/p&gt;

&lt;p&gt;Think of concurrent events like customers at a café. One customer orders a coffee and enjoys it at their own pace. While enjoying their coffee, another customer places an order without waiting for the first to finish. We can all enjoy our coffee with no problem.&lt;/p&gt;

&lt;p&gt;In Bloc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple events can be processed simultaneously without waiting for the previous one to complete.&lt;/li&gt;
&lt;li&gt;Events aren’t bottlenecked—each one is handled as soon as it arrives.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;But wait, what if the café has only one chair? Suddenly, things aren’t so smooth. Now, each customer has to wait for the previous one to finish their coffee before placing an order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sequential
&lt;/h3&gt;

&lt;p&gt;Events are carried out exactly as they come in thanks to the sequential transformer, but only one at a time. A new event cannot start until the processing of the prior one is complete.&lt;/p&gt;

&lt;p&gt;Imagine a tiny espresso bar with just one chair and a single barista. One customer orders, sits, drinks their coffee, and only then can the next customer step up. &lt;/p&gt;

&lt;p&gt;In Bloc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events queue up and are processed sequentially.&lt;/li&gt;
&lt;li&gt;No two events overlap, ensuring a predictable and orderly flow.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Okay now that we get the first two transformers out of the way, it’s time to introduce something that’s a total game-changer—especially for search and autocomplete features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restartable
&lt;/h3&gt;

&lt;p&gt;This event is perfect for situations where only the latest event matters. When a new event arrives, it cancels any ongoing ones and immediately starts processing the fresh request.&lt;/p&gt;

&lt;p&gt;Now, I want you to imagine that you decide to drink your coffee in the worst café of all time. You sit down, peacefully sipping your hot coffee, enjoying life. Then a new customer appears out of nowhere! Without hesitation, they snatch your cup, kick you out, and start drinking their own coffee.&lt;/p&gt;

&lt;p&gt;In Bloc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If multiple search queries are fired rapidly, only the latest one gets processed.&lt;/li&gt;
&lt;li&gt;Any previous events still in progress are cancelled as soon as a new one arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For real-time search, form validation, or any situation where the most recent input is prioritised, this transformer is your best option. Restartable is the best option if the most recent modification is all that matters.&lt;/p&gt;

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

&lt;p&gt;Last but not least, the final transformer event is a really interesting one. Let's talk about the droppable event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Droppable
&lt;/h3&gt;

&lt;p&gt;This event is really useful when you want to avoid repetitive calls. If an event is already being processed, any new events of the same type are ignored until the first one finishes. Once the running event completes, the next one can be accepted.&lt;/p&gt;

&lt;p&gt;Let’s visit one last café. You walk in, eager for a cup of coffee, and you see another person already enjoying their drink. You sit down and start waving to place your order, but no one listens. The staff won’t take your order until the current customer finishes and leaves. Once that last drop is gone, the café is all yours, and the waiters rush to take your order.&lt;/p&gt;

&lt;p&gt;In Bloc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If an event (like fetching data) is already running, new incoming events are ignored.&lt;/li&gt;
&lt;li&gt;Once the active event completes, the next event will be accepted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a really useful tools for situations like API calls that shouldn't be interrupted, or maybe for preventing duplicate submissions.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Package &amp;amp; Code
&lt;/h2&gt;

&lt;p&gt;All it takes to unlock this feature is installing a package and writing just &lt;strong&gt;ONE&lt;/strong&gt; line of code. Really, that's all! The rest? Just your regular Bloc setup, nothing special.&lt;/p&gt;

&lt;p&gt;First, download this package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pub.dev/packages/bloc_concurrency" rel="noopener noreferrer"&gt;bloc_concurrency&lt;/a&gt; : With this package we are able to use all the transformer events with built-in functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, all you have to do, is jump in the &lt;code&gt;example_bloc.dart&lt;/code&gt; file, and change the way you initialize a bloc event. For example, I have a simple counter application, in which I manage the state with a Bloc event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;TransformerBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TransformerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IncrementPressed&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onIncrement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// By default, it has the concurrent transformer&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onIncrement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;IncrementPressed&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Emitter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TransformerState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I want to change the way I handle this event, by applying sequential transformer for example, all I have to do is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;TransformerBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TransformerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IncrementPressed&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onIncrement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Apply this line&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bloc transformer events might look like a hidden detail at first, but as you can see, they can take your state management in a whole new level. And the best part? You only need to add one line in your existing code, and change the entire functionality. How amazing is that?&lt;/p&gt;

&lt;p&gt;Now go on my Flutter friends, you are free to explore new code adventures. Next time your Bloc events aren’t behaving quite the way you expected, remember—you’ve got options. Pick the right transformer, like you pick the right café in your hometown that serves the best coffee. 😉 ☕&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/bloc_transformer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to reach out if you have any questions or need further guidance. Cheers to your future Flutter projects!&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>tutorial</category>
      <category>dart</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Mastering Custom Widget Themes in Flutter</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Sat, 05 Oct 2024 06:36:07 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/mastering-custom-widget-themes-in-flutter-45b4</link>
      <guid>https://dev.to/thanasistraitsis/mastering-custom-widget-themes-in-flutter-45b4</guid>
      <description>&lt;p&gt;Well hello there Flutter people. It's been a while since my last blog post, but I'm back with something very interesting that will level up your Flutter applications. Today's subject is &lt;strong&gt;Custom Widget Themes&lt;/strong&gt;, a powerful way to manage consistent, reusable styling throughout your app.&lt;/p&gt;

&lt;p&gt;Ever found yourself copying and pasting container styles throughout your app? Sure, creating a Stateless Widget might seem like a quick and easy fix, but there's a more clean, maintainable solution hiding in Flutter's theming system.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll explore how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create custom container themes within ThemeData.&lt;/li&gt;
&lt;li&gt;Define reusable style variables (colors, text sizes, etc.).&lt;/li&gt;
&lt;li&gt;Implement responsive design principles using themes.&lt;/li&gt;
&lt;li&gt;Keep your code DRY (Don't Repeat Yourself) and maintainable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time you finish reading this post, you'll have a new tool in your Flutter toolbelt for creating more consistent, scalable UI designs. Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Theme Values 🎨
&lt;/h2&gt;

&lt;p&gt;Before we dive into custom container themes, we need to establish a foundation of consistent values that we'll use throughout our application. This approach ensures visual consistency and makes future updates much easier to manage.&lt;/p&gt;

&lt;p&gt;Let's create a file and call it &lt;code&gt;app_values.dart&lt;/code&gt;. This file will store all our standard values and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppValues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;smallText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;normalText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;largeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;24.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;pokecardBorderRadius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;pokecardPadding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a &lt;code&gt;colors.dart&lt;/code&gt; file to define our color scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppColors&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;grassBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;grassTextColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;grassOutlineColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassTextColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassOutlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;mainColors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;grassBackgroundColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xffcef79f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;grassTextColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xff2a513f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;grassOutlineColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xff378e8e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, with these two files perfectly setup, it's time to create our ThemeData. Create a file, name it &lt;code&gt;app_theme.dart&lt;/code&gt; and include this class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;ThemeData&lt;/span&gt; &lt;span class="n"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;useMaterial3:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// You can add your own appbar style, background color, primary color etc...&lt;/span&gt;
      &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;TextTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;titleLarge:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;largeText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassTextColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent color usage enhances your app's visual coherence.&lt;/li&gt;
&lt;li&gt;Using semantic color names (like 'grassBackgroundColor') makes your code more maintainable.&lt;/li&gt;
&lt;li&gt;The factory constructor pattern allows for easy theme switching if needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the way, if you are paying attention on the variable names, I think you know what's the main topic of this tutorial...&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Theme Extension
&lt;/h2&gt;

&lt;p&gt;Let's dive into the main reason you clicked on this tutorial. The scenario of this application is that I want to showcase every Grass-type Pokémon inside its own card. This card will be the same for every Pokémon, so there is no need to build the same widget continuously. Instead, let’s create a theme to define every Grass-type card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GrassTypeTheme&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ThemeExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt; &lt;span class="n"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt; &lt;span class="n"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the foundation of our Theme. What makes this situation particularly noteworthy is the inheritance of the class, because it extends the &lt;strong&gt;ThemeExtension&lt;/strong&gt;. That's the key of the whole tutorial. To properly implement this extension, we need to focus on two critical steps: &lt;br&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Implement the &lt;code&gt;copyWith&lt;/code&gt; Method&lt;/strong&gt; &lt;br&gt;&lt;br&gt;
The &lt;code&gt;copyWith&lt;/code&gt; method is crucial for our custom theme. It allows us to create new instances of our theme by overriding specific properties. This is useful when you want to create variants or update only a few values dynamically.&lt;/p&gt;

&lt;p&gt;Let’s add it to our class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;ThemeExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;textColor:&lt;/span&gt; &lt;span class="n"&gt;textColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;outlineColor:&lt;/span&gt; &lt;span class="n"&gt;outlineColor&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;cardPadding:&lt;/span&gt; &lt;span class="n"&gt;cardPadding&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;cardBorderRadius:&lt;/span&gt; &lt;span class="n"&gt;cardBorderRadius&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:  Implement the &lt;code&gt;lerp&lt;/code&gt; Method&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The lerp method (linear interpolation) is essential when animating theme changes or transitioning between different theme states. It helps Flutter animate changes smoothly when switching themes.&lt;/p&gt;

&lt;p&gt;Here’s how you can implement it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt; &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;ThemeExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;covariant&lt;/span&gt; &lt;span class="n"&gt;ThemeExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;textColor:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;outlineColor:&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;cardPadding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;cardBorderRadius:&lt;/span&gt;
          &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these two methods (copyWith and lerp) in place, your custom theme is now complete and ready to be used!&lt;/p&gt;

&lt;h2&gt;
  
  
  Completing The App Theme 🔧
&lt;/h2&gt;

&lt;p&gt;You are one step away of completing the most responsive and maintainable application setup you ever created. We just need to add the extension we just built into our ThemeData. Here's the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;ThemeData&lt;/span&gt; &lt;span class="n"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;useMaterial3:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;TextTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;titleLarge:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;largeText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassTextColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="c1"&gt;// There is the ThemeExtension we created&lt;/span&gt;
      &lt;span class="nl"&gt;extensions:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;[&lt;/span&gt;
        &lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;textColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassTextColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;outlineColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grassOutlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;cardBorderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokecardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;cardPadding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pokecardPadding&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The power of our theming system lies in its flexibility. Since the extension property is a List, you can define multiple themes to suit your needs. With our foundation in place, all that's left is to create a Widget that leverages our carefully crafted theme.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomContainer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CustomContainer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;containerTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GrassTypeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;containerTheme&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;cardPadding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;containerTheme&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;cardBorderRadius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;containerTheme&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;border:&lt;/span&gt; &lt;span class="n"&gt;Border&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;containerTheme&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;outlineColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bottomLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;Positioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;left:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;right:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;titleLarge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you implement this widget in your app, you'll see the theme come to life, automatically adapting to any theme changes you make. For now, let's enjoy our beautiful Bulbasaur card that we created with such quality code. &lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_custom_theme_extension%2Fblob%2Fmain%2Fassets%2Fhome_screen.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_custom_theme_extension%2Fblob%2Fmain%2Fassets%2Fhome_screen.png%3Fraw%3Dtrue" alt="Home Screen" width="1179" height="2556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you have it! We've unlocked a new level of customisation that goes much beyond basic color schemes by utilising Flutter's ThemeExtension. This approach enables us to create rich, deeply customizable themes that can hold any design property we need - from padding and border radius to complex color palettes and more.&lt;/p&gt;

&lt;p&gt;The next time you find yourself reaching for hard-coded styles or repetitive design code, consider leveraging ThemeExtension to create a more elegant, maintainable solution. Happy theming!&lt;/p&gt;

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_custom_theme_extension" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to reach out if you have any questions or need further guidance. Cheers to your future Flutter projects!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to switch themes in Flutter using BLoC</title>
      <dc:creator>Thanasis Traitsis</dc:creator>
      <pubDate>Fri, 03 May 2024 20:25:47 +0000</pubDate>
      <link>https://dev.to/thanasistraitsis/how-to-switch-themes-in-flutter-using-bloc-5fim</link>
      <guid>https://dev.to/thanasistraitsis/how-to-switch-themes-in-flutter-using-bloc-5fim</guid>
      <description>&lt;p&gt;Welcome to the ultimate guide on how to switch themes in your Flutter application using BLoC. In this article, we're embarking on a journey to master the art of seamlessly toggling between light and dark themes. By leveraging the power of the BLoC pattern, we'll completely separate the theme-switching logic from the UI components, ensuring maintainability and flexibility as your app evolves.&lt;/p&gt;

&lt;p&gt;But wait, this is not your ordinary "theme-switching" article. Before we dive into the intricacies of toggling between themes, let's lay a solid foundation by exploring the entire theming process from the ground up. We'll start by learning how to create our own custom themes, diving into clean architecture essentials on how to meticulously separate the fonts, colors, and everything in between.&lt;/p&gt;

&lt;p&gt;So, let's dive right in and begin crafting our custom light and dark themes, shall we? We'll gently guide our themes onto the canvas of our Flutter app. Remember, there are no mistakes, only happy little themes waiting to emerge.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Setting up the custom App Theme
&lt;/h2&gt;

&lt;p&gt;To establish a clean and organized codebase, we'll begin by setting up our custom app theme structure. Here's a suggested folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
📦lib
 ┗ 📂config
    ┗ 📂themes
 ┗ 📂core
    ┣ 📂constants
    ┗ 📂usecases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the constants, lets create two files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1. constants.dart:&lt;/strong&gt; Here you can add any constant variable, for example, the name of your Google Font.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ========================================================&lt;/span&gt;
&lt;span class="c1"&gt;// Font Family&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'Manrope'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ========================================================&lt;/span&gt;
&lt;span class="c1"&gt;// Spacing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;paragraphSpacing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;buttonGap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2. sizes.dart:&lt;/strong&gt; In this file, we gather all values related to sizing aspects of our application, including font sizes and any other dimensions that play a crucial role in defining the visual appearance and layout.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppValues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ========================================================&lt;/span&gt;
  &lt;span class="c1"&gt;// Font Size&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;smallText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;14.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;normalText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;largeText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;appBarText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;22.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's focus on defining the color palette of our application. Inside the themes folder, we'll create a dedicated file for managing colors. Here's our &lt;code&gt;colors.dart&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppColors&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;whiteColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;blackColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whiteColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blackColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// ========================================================&lt;/span&gt;
  &lt;span class="c1"&gt;// Main color theme&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;mainColors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;whiteColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xffFFFFFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;blackColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xff000000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;textColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;primaryColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ========================================================&lt;/span&gt;
  &lt;span class="c1"&gt;// Dark color theme&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;darkColors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;whiteColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xff000000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;blackColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xffFFFFFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;textColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;primaryColor:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromARGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;169&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;178&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom App Theme
&lt;/h3&gt;

&lt;p&gt;With all the core values being set, it's time to bring our app theme to life. Within the themes folder, let's create a new file named &lt;code&gt;app_theme.dart&lt;/code&gt;. This file will have a class that returns a ThemeData based on the colors we choose. Our goal with this class is to keep the core structure of the app theme intact while allowing for dynamic color changes. This approach ensures that any theme switch seamlessly updates the colors of our application without impacting text or container sizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppColors&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chosenColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;ThemeData&lt;/span&gt; &lt;span class="n"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;fontFamily:&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;colorScheme:&lt;/span&gt; &lt;span class="n"&gt;ColorScheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;background:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;seedColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;primary:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;appBarTheme:&lt;/span&gt; &lt;span class="n"&gt;AppBarTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;foregroundColor:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;titleTextStyle:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;fontFamily:&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appBarText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whiteColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontWeight:&lt;/span&gt; &lt;span class="n"&gt;FontWeight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;w500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;TextTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;bodyMedium:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;normalText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontFamily:&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;headlineLarge:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;largeText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;fontFamily:&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;filledButtonTheme:&lt;/span&gt; &lt;span class="n"&gt;FilledButtonThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;ButtonStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt;
              &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;foregroundColor:&lt;/span&gt;
              &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;whiteColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;textStyle:&lt;/span&gt; &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;smallText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;fontFamily:&lt;/span&gt; &lt;span class="n"&gt;fontFamilyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;switchTheme:&lt;/span&gt; &lt;span class="n"&gt;SwitchThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;thumbColor:&lt;/span&gt; &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;overlayColor:&lt;/span&gt;
            &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;trackColor:&lt;/span&gt;
            &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;trackOutlineColor:&lt;/span&gt;
            &lt;span class="n"&gt;MaterialStateProperty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;chosenColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's pretty much it. That's how you create a custom theme. Now, all you have to do, is set this theme inside the main.dart file, where the MaterialApp Widget is located.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;debugShowCheckedModeBanner:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mainColors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_theme_switch_bloc%2Fblob%2Fmain%2Fassets%2Farticle_images%2Flight_home.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_theme_switch_bloc%2Fblob%2Fmain%2Fassets%2Farticle_images%2Flight_home.png%3Fraw%3Dtrue" alt="Light Theme" width="633" height="1373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching themes
&lt;/h2&gt;

&lt;p&gt;Before diving into building our state management system to control the theme, we'll need to download some essential packages first. Here's the list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pub.dev/packages/bloc" rel="noopener noreferrer"&gt;bloc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pub.dev/packages/flutter_bloc" rel="noopener noreferrer"&gt;flutter_bloc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pub.dev/packages/shared_preferences" rel="noopener noreferrer"&gt;shared_preferences&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pub.dev/packages/go_router" rel="noopener noreferrer"&gt;go_router&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These packages will equip us with the essential tools to effectively manage theme switching functionality in our Flutter application. Additionally, they enable us to store and retrieve the user's preferred theme, ensuring a seamless and personalized experience for each user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the BLoC state manager
&lt;/h3&gt;

&lt;p&gt;In this project, we're simplifying our state management strategy by utilizing &lt;code&gt;theme_bloc&lt;/code&gt; and &lt;code&gt;theme_event&lt;/code&gt;. Instead of dealing with multiple theme states, we'll streamline our implementation by representing the theme status with a single boolean value. This is how the &lt;code&gt;theme_bloc.dart&lt;/code&gt; will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// if bool = true -----&amp;gt; the Theme is dark &lt;/span&gt;
&lt;span class="c1"&gt;// if bool = false -----&amp;gt; the Theme is light &lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThemeBloc&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Bloc&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;ThemeBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// execute this event when the app starts&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SetInitialTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasThemeDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasThemeDark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// toggle the app theme&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChangeTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasThemeDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasThemeDark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasThemeDark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt; &lt;span class="n"&gt;sharedPreferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'isDark'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt; &lt;span class="n"&gt;sharedPreferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;sharedPreferences&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'isDark'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isDark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code of &lt;code&gt;theme_event.dart&lt;/code&gt; is as simple as possible. Here are the three lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThemeEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetInitialTheme&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ThemeEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChangeTheme&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ThemeEvent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Changes in the main.dart, to initialize the BLoC
&lt;/h3&gt;

&lt;p&gt;In our &lt;code&gt;main.dart&lt;/code&gt; file, we're taking the first steps to integrate our BLoC state management system seamlessly into our application. At the heart of it all, is the runApp function, where we set up a BlocProvider to supply our &lt;strong&gt;ThemeBloc&lt;/strong&gt; to the entire app. Within this provider, we ensure that the &lt;strong&gt;SetInitialTheme()&lt;/strong&gt; event is executed right from the start, setting the tone for our app's initial theme selection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BlocProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ThemeBloc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;SetInitialTheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BlocBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;debugShowCheckedModeBanner:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
              &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;darkColors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
              &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mainColors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By looking closely in the &lt;strong&gt;MaterialApp&lt;/strong&gt;, we can see that we're dynamically choosing between the dark and light themes based on the current state of our ThemeBloc. If the state indicates that the dark theme is active, we use the dark theme defined in AppTheme, otherwise, we default to the light theme. This ensures that our app's appearance adjusts in real-time according to the user's selected theme preference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implement the switch functionality
&lt;/h3&gt;

&lt;p&gt;To finalize our theme-switching functionality, we'll implement the &lt;strong&gt;ChangeTheme()&lt;/strong&gt; event. In our application, we'll use a Switch widget to provide a visual representation of theme toggling. Here's how we can integrate it into our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;  &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;actions:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;BlocBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChangeTheme&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_theme_switch_bloc%2Fblob%2Fmain%2Fassets%2Farticle_images%2Fdark_home.png%3Fraw%3Dtrue" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FThanasis-Traitsis%2Fflutter_theme_switch_bloc%2Fblob%2Fmain%2Fassets%2Farticle_images%2Fdark_home.png%3Fraw%3Dtrue" alt="Dark Theme" width="633" height="1373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There you go everybody! Indeed, theme switching in Flutter becomes effortless with the BLoC pattern. By decoupling the theme logic from the UI components, BLoC enables smooth and efficient management of theme changes. With this pattern, we can seamlessly toggle between themes, ensuring a consistent and visually appealing user experience across our Flutter applications. Also, let's not forget that we've demonstrated the flexibility of creating custom themes, allowing us to break away from the traditional light and dark themes provided by Flutter.&lt;/p&gt;

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

&lt;p&gt;If you enjoyed this article and want to stay connected, feel free to connect with me on &lt;a href="https://www.linkedin.com/in/thanasis-traitsis/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd like to dive deeper into the code and contribute to the project, visit the repository on &lt;a href="https://github.com/Thanasis-Traitsis/flutter_theme_switch_bloc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Was this guide helpful? Consider buying me a coffee!☕️ Your contribution goes a long way in fuelling future content and projects. &lt;a href="https://www.buymeacoffee.com/thanasis_traitsis" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to reach out if you have any questions or need further guidance. Cheers to your future Flutter projects!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
