Ramadan Series · Flutter Team
Episodes
28 published
Ramadan 1447 · Value Digital Services Flutter Team✦ Series Complete

FlutterMinute
Ramadan Series

During the month of Ramadan, each of us contributes to share tips, tricks, and insights about Flutter. Feel free to explore any topic you want — from Mobile to AI. Have fun!

📅 Challenge: Before the end of Ramadan, at least each of us should have one contribution!
✍️ Ready to share? Check out the Contribute section — it has everything you need to get started.
🎧
Listen to the Introduction
Learn about FlutterMinute series
Value Digital Services Flutter Team

The VDS Flutter Team — Building amazing apps and learning Flutter's secrets together 🚀

9
Team Members
28
Published
Ramadan
Daily Series
Topics to Explore

Topic Areas

Our team explores various aspects of Flutter's internals. Topics are chosen by contributors based on what they're learning or curious about.

♻️
State Management
BLoC, context, state patterns
4 episodes
🔐
Security & Reverse Engineering
Secrets, Blutter, Frida, real-world
4 episodes
🏗
Architecture & Patterns
DI, animations, error handling
3 episodes
Weekend Specials
Stories, bugs, and deep dives
3 episodes

Meet the Team

Nine Flutter developers on a mission to understand what's really happening under the hood. Each of us contributes insights, asks questions, and helps the whole team level up.

Amine Brahmi
Amine Brahmi
Flutter Developer
9 episodes
Yassine Sabri
Yassine Sabri
Flutter Developer
5 episodes
Habib Soula
Habib Soula
Flutter Developer
1 episode
Jihed Mrouki
Jihed Mrouki
Flutter Developer
6 episodes
Rania Gahbiche
Rania Gahbiche
Flutter Developer
1 episode
Skander Mallek
Skander Mallek
Flutter Developer
0 episodes
Ala Makhlouf
Ala Makhlouf
Flutter Developer
1 episode
Talel Briki
Talel Briki
Flutter Developer
3 episodes
Mahdi Menaa
Mahdi Menaa
Flutter Developer
1 episode

How This Works

Every day during Ramadan, one team member shares a deep dive on any topic they've been exploring — from Flutter widgets to mobile architecture, from AI integration to performance optimization. Each sharing follows a consistent 4-section format:

🧠 Question — A counterintuitive question that reveals something surprising
📖 Insight — How it actually works internally, with a real-world analogy
🎯 Takeaway — One sentence that captures the core insight
💬 Team Discussion — A thought-provoking question for the team

Anyone can contribute! Pick any topic you're curious about — Mobile, AI, architecture, performance, whatever interests you. Fill in the template, and share your knowledge. Have fun exploring! The discussion questions spark conversations that help everyone learn deeper.

Day 01 · Rendering & Widget System

Widget Rebuilds & Efficiency

Flutter rebuilds widgets constantly. Most developers assume that means work — it doesn't. Here's why.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 01 · Rendering & Widget System
Under the Hood
Flutter rebuilds widgets constantly.
So why doesn't your app feel like it's on fire? 🔥
🍕 Analogy: A widget is a menu order, not the pizza. Rewriting your order is free. Remaking the pizza is expensive.
1
Widgets are just cheap Dart objects — blueprints.
Calling build() creates a tiny config object. It costs almost nothing. Think of it as updating a description, not painting pixels.
2
Elements are the long-lived "real" instances.
The Element tree persists across rebuilds. Flutter compares the new widget blueprint to the old one using runtimeType + key. If they match — same element, just updated.
3
RenderObjects do the expensive work — and they're reused.
Layout, paint, compositing — that's all in the RenderObject. Flutter only calls updateRenderObject() with the new config. No recreation. No drama.
📄
Widget Tree
Blueprint. Rebuilt constantly on setState().
Cheap ✓
🪝
Element Tree
Stable middleman. Holds state & identity.
Long-lived
🖼
Render Tree
Does real layout & paint. Reused when possible.
Expensive
When Flutter "rebuilds a widget," it's not repainting pixels — it's just updating a cheap description object, and the heavy machinery underneath stays alive and reused.
If widgets are so cheap to rebuild — why do we still bother with const constructors? What exactly are they optimizing?

Why This Question

Most Flutter developers know that setState() triggers a rebuild. What they don't know is that "rebuild" and "re-render" are completely different things. This question forces that distinction into the open.

Key insight: Widget objects are so cheap that creating thousands per frame costs less than a single layout pass. The expensive objects — RenderObjects — are almost never recreated.

Discussion Thread

The closing question asks: "If widgets are so cheap to rebuild — why do we still bother with const constructors?"

This bridges directly into Day 26's topic on how const canonicalisation works at the memory level — a deliberately planted thread for later in the month.

Day 02 · Rendering & Widget System

The Three Trees

Every running Flutter app maintains three separate object trees simultaneously. Most developers have heard of one. Here's what all three actually do.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 02 · Rendering & Widget System
Under the Hood
Flutter runs three separate trees simultaneously.
What does each one do — and why can't one tree just do it all?
🏗️ Analogy: Think of building a house. The blueprint (Widget) is cheap to redraw. The project manager (Element) tracks what's built and what changed. The construction crew (RenderObject) does the actual heavy work.
1
Widget Tree — the "what you want" layer.
Widgets are immutable config objects. Every build() call produces a fresh widget tree. They describe the UI, they don't own it.
2
Element Tree — the "what actually exists" layer.
Elements are long-lived. Created once per widget position and persist across rebuilds. When a new widget arrives, the Element compares it using runtimeType + key. Elements hold state — this is why StatefulWidget's state survives rebuilds.
3
RenderObject Tree — the "actually do the work" layer.
RenderObjects handle layout, painting, and hit testing. Flutter reuses them aggressively — only calling performLayout() or paint() when something genuinely changed.
Tree Responsibility Lifetime Cost
Widget Describes what you want Ephemeral — recreated every build() Cheap
Element Bridges widget ↔ render. Holds state. Long-lived — survives rebuilds Stable
RenderObject Layout, paint, hit-testing Long-lived — reused when possible Expensive
Three trees exist because each job needs a different trade-off: Widgets are cheap and disposable, Elements are stable and stateful, RenderObjects are powerful and expensive — you want as few of them touched per frame as possible.
If Elements hold state — what actually happens to a StatefulWidget's State object when you wrap it in a new parent widget? Does it survive, or does Flutter recreate it? 👇

Why This Question

Knowing that three trees exist is trivia. Understanding why three trees exist is architecture. Each tree represents a different trade-off: disposability vs. stability vs. power.

Key insight: State doesn't live in the Widget — it lives in the Element. This is the most misunderstood fact in Flutter. It explains why wrapping a widget in a new parent doesn't destroy its state.

Discussion Thread

The closing question asks: "What happens to a StatefulWidget's State object when you wrap it in a new parent widget?"

This bridges into Day 04, which covers the exact rules Flutter uses to decide what to keep vs. recreate during reconciliation.

Day 03 · Caching & Performance

super_cache Deep Dive

Your app fetches the same data constantly. What if one mixin could fix that in 3 lines — with LRU eviction, TTL expiry, stampede protection, and AES 256 GCM encryption baked in?

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 03 · Caching & Performance
Package Deep Dive
Your app fetches the same data constantly.
What if one mixin could fix that in 3 lines? 🚀
🍕 Analogy: Calling your API on every widget build is like calling the pizza shop every time you want to think about pizza. Cache it. Think fast. Order once.
❌ WITHOUT CACHE
Widget A → API call → response
Widget B → API call → response
Widget C → API call → response
3 network calls. Race conditions. 😭
✅ WITH super_cache
Widget A → API call → response
Widget B ────────► (same future)
Widget C ────────► (same future)
1 network call. Stampede protection. 🎉
This is probably the most important thing to clarify before picking it up. super_cache is a caching engine, not a local database.

Tools like SQLite, Hive, Isar and Drift are persistence layers built around structured storage, querying, relationships and migrations. You use them when your data is the source of truth and needs to live on the device indefinitely.

super_cache sits in front of your data source (usually an API) and holds onto recent responses so you don't have to refetch them. Think of it as short-term memory for your repository layer. The data has a TTL, gets evicted when memory is low, and is optimised for speed, not exhaustive storage.
🗄 SQLite / Hive / Isar
· Permanent storage on disk
· Query language or key-value store
· Schema migrations, indexes
· Source of truth for offline data
· Reads are comparatively slow (disk I/O)
· No concept of TTL or LRU eviction
⚡ super_cache
· In memory by default (nanosecond reads)
· TTL: data expires automatically
· LRU: oldest entries evicted under pressure
· Stampede protection built in
· Optional encrypted memory (L2)
· Optional disk persistence (L3)
The right mental model: use Hive to store a user's saved articles permanently. Use super_cache to avoid refetching the article feed every time they scroll back to the home screen. They're not competing — they serve different jobs. You can even use both together: super_cache in front of a Hive repository gives you memory-speed reads on top of your persisted data.
Package What it does Deps
super_cache Core LRU + TTL engine, orchestrator, repository mixin Zero 🎉
super_cache_secure AES 256 GCM (256 bit authenticated encryption) in memory cache (L2) flutter_secure_storage
super_cache_disk Persistent file per entry cache with integrity checks (L3) Pure Dart
super_cache_testing FakeCache + ManualClock for deterministic unit tests Pure Dart
💨
L1 Memory
super_cache
O(1) LRU + TTL. Synchronous reads. Lives in RAM. Rebuilt on app restart.
0.05 µs / hit · 60fps frame ≈ 3 µs total
🔒
L2 Secure
super_cache_secure
AES 256 GCM (256 bit authenticated encryption) in memory. Decrypted values never touch disk.
~0.86 µs / get (orch. L2 path)
💾
L3 Disk
super_cache_disk
Survives restarts. File per entry with integrity checks. Codec-based serialization.
~114 µs / get (SSD)
The CacheOrchestrator wires all three layers together. On a miss in L1, it checks L2, then L3. When it finds the value in a lower layer, it promotes it upward automatically so the next read is instant.
The repository mixin collapses all of this into a single declaration — LRU eviction, TTL expiry, stampede protection and metrics activate the moment you declare a cache:
class ProductRepository with CacheRepositoryMixin<String, Product> { @override final cache = MemoryCache<String, Product>( maxEntries: 200, defaultTTL: const Duration(minutes: 5), ); Future<Product?> getProduct(String id) => fetchWithCache(id, onMiss: () => api.fetchProduct(id)); // ✅ LRU eviction, TTL expiry, stampede protection, metrics: all automatic }
🛍
E-Commerce App
ProductRepository
Cache product listings with a 5 min TTL. Three widgets showing the same product card? One API call. Flash sales expire automatically. Add DiskCache for offline browsing.
L1 Memory + L3 Disk
🏦
Banking / FinTech
AccountRepository
Balance and transaction history cached in SecureCache (AES 256 GCM). Decrypted data never hits disk. TTL evicts stale balances without any manual cleanup.
L1 + L2 Secure
📰
News / Content App
ArticleRepository
Articles cached to disk with a 24h TTL. App opens instantly offline. New articles evict old ones via LRU. Cache metrics feed into your analytics pipeline.
L1 Memory + L3 Disk
🗺
Maps / Location App
GeoRepository
Cache geocoding results and nearby POIs by coordinate key. sizeEstimator limits memory by byte budget. Background TTL sweep evicts stale tiles silently.
L1 Memory · byte eviction
FakeCache and ManualClock let you test TTL expiry without sleeping. Advance time programmatically and assert cache behaviour with zero flakiness.
final clock = ManualClock(); final cache = FakeCache<String, int>(clock: clock); cache.put('score', 42, ttl: Duration(minutes: 5)); expect(cache.get('score'), 42); // ✅ still valid clock.advance(Duration(minutes: 6)); expect(cache.get('score'), null); // ✅ expired — no sleep() needed
final m = cache.metrics; print('Hit rate: ${(m.hitRate * 100).toStringAsFixed(1)}%'); // Stream live metrics into your analytics pipeline cache.metricsStream.listen((m) { analytics.record('cache_hit_rate', m.hitRate); });
Layer Get (hit) Put Notes
MemoryCache 0.05 µs 0.11 µs synchronous; no event loop yield
Orchestrator L1+L2 0.19 µs 0.67 µs async overhead ~140 ns
DiskCache (SSD) 114 µs 1.73 µs async write back
💡 What do those numbers mean in a real app?
L1 Memory · 0.05 µs
60fps → ~3 µs / frame
60 cache reads per second at 60fps costs ~3 µs total per frame. The frame budget is 16,000 µs.
In your app: a product detail screen rebuilding at 60fps reads the cache 60×/sec. All 60 reads combined take ~3 µs — less than 0.02% of your 16 ms frame budget.
L2 path · ~0.86 µs
10 reads → ~9 µs
Orchestrator L2 hit (plain memory). SecureCache adds AES 256 GCM decrypt overhead on top.
In your app: a banking dashboard reading 10 values through the orchestrator takes ~9 µs total — well inside one animation frame. No API call, no spinner.
L3 Disk · 114 µs
20 reads → ~2.3 ms
File I/O. Only hit on cold start or first miss per key, then promoted to L1.
In your app: a news feed opening offline reads 20 cached articles in ~2.3 ms. Every subsequent rebuild reads from L1 at 0.05 µs — the disk is never touched again.
The key thing to remember: DiskCache is only hit once per session per key. Once L3 returns the value it's promoted straight to L1. From that point the same key costs 0.05 µs instead of 114 µs. That ~2,280× speedup happens automatically with no extra code on your side.
super_cache gives you production-grade LRU, TTL and stampede protection in 3 lines of code. Start with MemoryCache, add DiskCache for offline support, layer in SecureCache for sensitive data — and test all of it without a single sleep().
Which layer fits your current project's API responses? Does any data you're caching today actually deserve AES 256 GCM encryption? 🔒 And what's the worst stampede bug you've shipped to production?

Why This Package

Most Flutter apps fetch data on every widget build without thinking twice. super_cache was built to fix this with the minimum possible API surface — one mixin, one cache declaration, zero boilerplate. The stampede protection alone makes it worth the switch: when 3 widgets request the same data simultaneously, only one network call is made.

Key insight: super_cache is not a database replacement. It's a short-term memory layer that sits in front of your API — with automatic eviction, TTL expiry, and optional AES 256 GCM encryption for sensitive data.

Discussion Thread

The closing question asks: "Which cache layer fits your project, and does any of your cached data deserve encryption?"

This surfaces a practical architecture decision most teams skip. Jihed built super_cache to solve a real stampede bug in production — the question bridges directly to defensive API patterns and data security classifications.

Day 04 · Architecture & Internals

BuildContext Deep Dive

You've used context a thousand times — but do you actually know what it is? This episode unpacks BuildContext as your Element's live position in the tree, and why that matters for navigation, theming, and async safety.

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 04 · Architecture & Internals
Architecture
You've used context a thousand times.
Do you actually know what it is? 🌳
📍 Analogy: BuildContext is like a GPS coordinate in the widget tree. It tells Flutter where you are — which lets it look upward and find what's above you: your Theme, your Navigator, your inherited data. Without it, there's no "above you".
Most developers think of BuildContext as a magic object you pass around to call Navigator.push() or Theme.of(). But it's actually the Element itself — the live node in the Element tree. It holds a reference to your exact position. That's why using a stale context after a widget is disposed crashes your app.
1
context.findAncestorWidgetOfExactType() walks UP the tree.
Every Theme.of(context), Navigator.of(context), MediaQuery.of(context) call walks up the Element tree from your position to find the nearest matching InheritedWidget. Calling them from a context that's too high gets the wrong ancestor.
2
The "wrong context" bug is about position, not timing.
A common trap: calling Navigator.push() from inside a showDialog() using the outer context. The dialog lives in an overlay above your widget — the outer context's Navigator position doesn't include it.
3
Async gaps make context go stale.
After an await, the widget that owned the context might have been disposed. Checking if (mounted) before using context after an await is not optional — it prevents a very common crash in production.
⚠ FlutterError: Looking up a deactivated widget's ancestor is unsafe. At this point the state of the widget's element tree is no longer stable. → You used context after an await without checking mounted.
// ❌ Crash in production: context might be stale after await Future<void> _save() async { await repository.save(data); Navigator.of(context).pop(); // ← context might be dead } // ✅ Always guard context usage after an await Future<void> _save() async { await repository.save(data); if (!mounted) return; // ← guard Navigator.of(context).pop(); }
Why does Theme.of(context) subscribe to changes? Because InheritedWidget.of() doesn't just fetch the value — it registers your Element as a dependent. When the InheritedWidget updates, Flutter knows exactly which Elements to rebuild. context is both a position and a subscription handle.
BuildContext is your Element's position in the tree. It's what makes inheritance, navigation and theming work. Never store it across async gaps — always check mounted before using context after an await.
Have you ever seen the "deactivated widget" crash in production? What was the scenario? And do we consistently check mounted after awaits in our codebase right now?

Why BuildContext Matters

BuildContext is one of Flutter's most misunderstood primitives. Developers use it constantly — but rarely stop to think about what it actually is. Understanding that it is the Element bridges the gap between "Flutter magic" and predictable, debuggable code.

Key insight: BuildContext is not a handle or a reference to a widget — it is the Element itself. Every of(context) call walks the Element tree upward from your exact position. Position determines what you find.

Discussion Thread

The closing question asks: "Have you ever seen the 'deactivated widget' crash in production? And do we consistently check mounted after awaits in our codebase right now?"

This is a practical audit question — the mounted check is one of the most frequently missing safety guards in real Flutter codebases.

Day 05 · Rendering & Widget System

Widgets Are Immutable

Widgets are immutable — they can never change after creation. So how does your UI ever actually update? This episode unpacks the snapshot model that makes Flutter both fast and predictable.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 05 · Rendering & Widget System
Under the Hood
Widgets are immutable — they can never change after creation.
So how does your UI ever actually update? 🤔
📸 Analogy: A widget is a snapshot, not a living object. When you want a new photo, you don't edit the old one — you take a new shot. Flutter does the same: discard the old description, create a fresh one.
1
Immutability makes comparison trivial.
Because a widget never changes, Flutter knows that if two widgets are the same object in memory, nothing could have changed — skip the rebuild entirely. This is exactly what const exploits: same instance, zero diff work.
2
Updating the UI means replacing the widget, not mutating it.
When setState() fires, Flutter runs build() and produces a brand new widget object. The old one is discarded. The Element compares old vs. new using runtimeType + key and decides what to update downstream.
3
State lives elsewhere — that's the whole point.
Because widgets can't hold mutable data, Flutter is forced to put state somewhere stable: the Element. Cheap descriptions come and go freely, while state and layout objects persist and are reused across rebuilds.
📸
Immutable Widget
Fresh every build(). Discarded freely. Safe to compare by identity.
Stateless ✓
🗄️
Mutable State
Lives on the Element. Survives widget recreation. Never thrown away on rebuild.
Long-lived
Widgets are immutable not to make your life harder — but because throwaway descriptions are what makes Flutter fast: easy to create, safe to compare, and cheap to discard.
If const widgets are the same object in memory across rebuilds — does Flutter still call build() on their children? What exactly gets skipped, and what doesn't? 👇

Why Immutability is a Feature, Not a Constraint

Most developers encounter widget immutability as a friction point — you can't just set a field on a widget and expect the UI to update. But the constraint is the design. Immutable descriptions are trivially cheap to create, safe to throw away, and easy to diff — which is exactly what allows Flutter to rebuild aggressively without paying a performance penalty.

Key insight: const widgets are the same object in memory across rebuilds. Flutter sees a reference-equal widget and skips diffing entirely — no build() call, no Element update. Immutability is what makes this optimisation sound.

Discussion Thread

The closing question probes a subtle point: "If const widgets are the same object in memory across rebuilds — does Flutter still call build() on their children? What exactly gets skipped, and what doesn't?"

The answer touches on how Element identity, the rebuild scheduler, and subtree bailout interact — a great thread for the team.

✦ Weekend Special · Flutter Fun Facts

Flutter Was Almost Called "Sky"

Before it was Flutter, it was the Sky Engine — born from a call to "Open The Sky" inside Google in 2014. This is the story of a spinning square, a 60fps dream, and how a framework got its name.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Weekend Special · Flutter Fun Facts
Did You Know?
Flutter was almost called "Sky"
and its first ever demo was a spinning square. 🌀
🌤️ Origin: In October 2014, a team of Google engineers heard an internal call to "Open The Sky" — and that's literally how the project got its name.
14
October 2014
The Sky Engine is born.
A small Google team starts an experiment: what if Dart could render a mobile UI directly, bypassing the platform entirely? No Java. No Objective-C. Just Dart → pixels.
15
April 2015 · Dart Developer Summit
Sky goes public — with a spinning square.
Engineer Eric Seidel demoed Sky on stage. The first thing he showed was a square spinning. He wanted 120fps but the device maxed at 60Hz — so he settled for 60fps and called it "the first goal." Hot Reload was already there. People were stunned.
17
2017 · Google I/O
Sky gets a new name: Flutter.
Two years after the Summit demo, Google officially rebranded Sky as Flutter and released the first alpha. Same engine. New name. The world started paying attention.
18
December 4, 2018 · London
Flutter 1.0 — stable and ready for production.
From a spinning square to a production-ready framework used by Google, Alibaba, and BMW. In just 4 years.
🎬
Watch the original 2015 demo — Eric Seidel on stage, Sky spinning its first square, and Hot Reload working before most people knew what it was.
Sky: An Experiment Writing Dart for Mobile
Dart Developer Summit 2015 · youtube.com
The framework you use every day started as a spinning square on a stage in 2015 — and the engineer couldn't even hit 120fps because the phone wasn't ready yet.
If you had to rename Flutter today — what would you call it, and why? 👇😄

From Sky to Flutter

The Sky name wasn't just a codename — it reflected the ambition of the project: to bypass platform constraints entirely and own every pixel. The rename to Flutter in 2017 brought a more approachable identity, but the engine underneath was the same one Eric Seidel spun a square with in 2015.

Fun fact: Hot Reload was already working in the 2015 Sky demo — years before it became Flutter's headline feature. The fundamentals were right from day one.

Weekend Discussion

The closing question is delightfully open: "If you had to rename Flutter today — what would you call it, and why?"

A light question for the weekend — but it's a good way to think about what Flutter actually stands for in 2025: cross-platform, fast rendering, a great DX. What name would capture all of that?

Day 08 · State Management

bloc_concurrency

When a user taps a button five times fast, your Bloc fires all five events at once. bloc_concurrency gives you four declarative transformers to control exactly how your Bloc handles rapid-fire or overlapping events.

Yassine Sabri
Written by Yassine Sabri
Flutter Minute ⚡
Day 08 · State Management · bloc_concurrency ^0.3.0
State Management
What happens when the user taps a button 5 times fast — and your Bloc fires all 5 events at once? 💥
🎯 Analogy: It's like a coffee shop counter — do you serve every customer simultaneously (chaos), one at a time (slow), ignore new ones while busy (focused), or drop everything for the latest order (responsive)? bloc_concurrency lets you choose.
concurrent()
All events run at the same time, no waiting for each other.
⚡ default-like
sequential()
Events queue up and process one by one, in arrival order.
📋 ordered
droppable()
New events are ignored while a handler is still running.
🚫 no spam
restartable()
Cancels the current handler and restarts with the newest event.
🔄 latest wins
1
Pass a transformer to on<Event>()
Each event handler in your Bloc accepts an optional transformer parameter. Just import bloc_concurrency and pass one of its four transformers — no extra boilerplate needed. Different handlers in the same Bloc can even use different strategies.
2
It wraps Dart streams under the hood via stream_transform
Each transformer returns an EventTransformer<E> — a function that converts the raw event stream into a transformed one. droppable() uses exhaustMap, restartable() uses switchMap, and sequential() uses asyncExpand — battle-tested Rx patterns, now dead simple to apply.
3
Pick the right tool for the job
Use droppable() for submit buttons (prevent double-submit). Use restartable() for live search (cancel stale API calls). Use sequential() for cart operations (order matters). Use concurrent() for independent parallel fetches.
import 'package:bloc_concurrency/bloc_concurrency.dart';

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc() : super(SearchInitial()) {
    on<SearchQueryChanged>(
      _onQueryChanged,
      transformer: restartable(), // cancel old, use latest
    );
    on<FormSubmitted>(
      _onSubmit,
      transformer: droppable(), // no double-submit
    );
  }
}
bloc_concurrency gives you four declarative event transformers — concurrent, sequential, droppable, restartable — so you control exactly how your Bloc handles rapid-fire or overlapping events, with zero manual stream wiring.
In our app, which Blocs are most at risk from unhandled concurrent events right now? Go through one flow together — could a fast user trigger a race condition without droppable() or restartable() protecting it? And how does this connect to cancellation in our async handlers tomorrow?

Why bloc_concurrency?

Concurrency bugs are silent killers in Bloc-based apps. A double-submit on a checkout button, stale search results flashing on screen, or cart operations applied out of order — these all stem from not controlling how events are processed. bloc_concurrency solves this with a single line per handler, mapping each use case to a well-known Rx operator under the hood.

Key insight: The transformer you pick should match the semantic intent of the event — not just what feels convenient. droppable() says "this action must not overlap itself"; restartable() says "only the latest matters, discard the rest."

Discussion Thread

Audit your own Blocs: which event handlers are currently unprotected from concurrent calls? Pick one real flow in the app and decide together which transformer it needs — and why.

Day 09 · State Management

hydrated_bloc

Your user spent 20 minutes filling a form — then the OS killed the app. Was all that state just gone? hydrated_bloc turns any Bloc or Cubit into a self-persisting state machine with just two method overrides.

Yassine Sabri
Written by Yassine Sabri
Flutter Minute ⚡
Day 09 · State Management · hydrated_bloc ^10.1.1
State Management
Your user spent 20 mins filling a form — then the OS killed your app. Was all that state just gone? 💀
🎯 Analogy: A regular Bloc is like RAM — fast but wiped on shutdown. hydrated_bloc adds a SSD layer: every state change is auto-saved to disk and silently restored the next time the Bloc wakes up.
emit(state)
New state emitted
toJson()
You serialize it
HiveStorage
Auto-written to disk
fromJson()
Restored on next launch
🧱 HydratedBloc
🧊 HydratedCubit
🔀 HydratedMixin
🔑 storagePrefix
🔒 Custom Storage
1
One-time setup in main()
Initialize HydratedStorage once before runApp(). It wraps hive_ce under the hood for fast, platform-agnostic key-value persistence — works on Android, iOS, web, desktop. Use HydratedStorageDirectory.web on web and getTemporaryDirectory() on native.
2
Extend HydratedBloc / HydratedCubit, implement 2 methods
Override toJson(state) to serialize your state, and fromJson(json) to deserialize it. That's all. Every emit() call auto-triggers a write; every constructor auto-triggers a read. Or use HydratedMixin on an existing Bloc and call hydrate() manually.
3
Override storagePrefix in production — or lose your data
By default the key is runtimeType.toString(), which is not obfuscation-safe. If you ship with --obfuscate, the type name changes and your persisted data becomes unreachable. Override storagePrefix with a hardcoded string. Also supports per-instance storage override for encrypted or scoped storage needs.
// 1. main.dart — one-time setup
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  HydratedBloc.storage = await HydratedStorage.build(
    storageDirectory: kIsWeb
      ? HydratedStorageDirectory.web
      : HydratedStorageDirectory((await getTemporaryDirectory()).path),
  );
  runApp(App());
}

// 2. Your Bloc — just add toJson / fromJson
class SettingsBloc extends HydratedBloc<SettingsEvent, SettingsState> {
  SettingsBloc() : super(SettingsState.initial());

  @override
  String get storagePrefix => 'SettingsBloc'; // ← obfuscation-safe!

  @override
  SettingsState? fromJson(Map<String, dynamic> json) =>
    SettingsState.fromJson(json);

  @override
  Map<String, dynamic>? toJson(SettingsState state) =>
    state.toJson();
}
⚠️ Don't forget to mock storage in unit tests! Use MockStorage from package:mocktail and assign it via HydratedBloc.storage = MockStorage() in setUp() — otherwise tests leak real disk writes across runs.
hydrated_bloc turns any Bloc or Cubit into a self-persisting state machine — just extend HydratedBloc, implement toJson / fromJson, and override storagePrefix before shipping to production.
Which of our Blocs currently hold state that users would be frustrated to lose on an app restart — settings, cart, onboarding progress, filters? Should all of them be hydrated, or are some better off starting fresh? And what's our strategy for handling fromJson returning null after a breaking state schema change in a future release?

Why hydrated_bloc?

State loss on app kill is a silent UX killer. Users filling long forms, configuring settings, or progressing through onboarding expect their work to survive. hydrated_bloc solves this transparently — no manual SharedPreferences wiring, no custom persistence layer, just two method overrides and your Bloc remembers everything across sessions.

Key insight: Always override storagePrefix with a hardcoded string before shipping with --obfuscate. The default runtimeType.toString() key will change after obfuscation, silently orphaning all persisted data.

Discussion Thread

Review your existing Blocs: which ones hold user-generated state that would be frustrating to lose? Decide as a team which get hydrated, which start fresh — and plan your migration strategy for future state schema changes.

Day 10 · State Management

State Has Gravity

You've heard "lift your state up" a hundred times. But what happens when you lift it too high? State has a natural gravity — keep it as low as possible.

Rania Gahbiche
Written by Rania Gahbiche
Flutter Minute ⚡
Day 10 · State Management & Architecture
State Management
You've heard "lift your state up" a hundred times.
But what happens when you lift it too high? 🏗️
🧲 Analogy: State is like a TV remote. If everyone in the building shares one mounted in the lobby, every resident gets notified when anyone changes the channel — even people watching a different TV.
1
setState() is not the problem — scope is.
setState() is fast and fine for local UI state (a button's loading indicator, a text field's value). The mistake is putting all state at the root. Now every widget in the tree rebuilds on every change — even widgets that don't care.
2
State has a natural "gravity" — keep it as low as possible.
Ask one question: "Who needs this state?" If only one widget → keep it local. If a subtree → lift to their nearest common ancestor. If the whole app → then go global. Lifting higher than necessary is the real performance killer.
3
InheritedWidget is Flutter's native answer to sharing state downward.
Provider, Riverpod, Bloc — all build on top of InheritedWidget. Only widgets that subscribed via context.watch() rebuild. Widgets that read once with context.read() stay untouched.
dart
// ❌ Too high — entire tree rebuilds on cart update
class MyApp extends StatefulWidget { // cart lives here

// ✅ Nearest common ancestor — only subtree rebuilds
class ShopPage extends StatefulWidget { // cart lives here
State Type Where it lives Example
Local UI Inside the widget Button loading, toggle
Subtree shared Nearest common ancestor Form values, tab state
Global app InheritedWidget / Provider Auth, theme, cart
Don't ask "setState or state manager?" — ask "what is the smallest subtree that needs this state?" The answer tells you exactly where it belongs.
In our current app, is there state living at the root that only one or two widgets actually consume? What would it take to push it back down — and would it be worth it?

Why State Placement Matters

Putting all state at the root is the most common Flutter performance mistake — and the hardest to spot because everything still works. Every setState() call triggers a rebuild that ripples through the entire widget tree, causing unnecessary repaints for widgets that don't even use the changed state. Keeping state as low as possible in the tree is the simplest, most effective performance win available.

Key insight: setState() is not your enemy — lifting state too high is. Ask "who needs this state?" and place it at their nearest common ancestor, not at the app root by default.

Discussion Thread

Look at your own app's widget tree. Is there state at the root — auth aside — that only a handful of widgets consume? Could it be pushed down closer to where it's used? What would it take to refactor it, and would the rebuild reduction be worth the effort?

Day 11 · Widgets & Identity

Keys & Widget Identity

Flutter has a bug in your list. You didn't write it — you just forgot a Key. Learn how Flutter tracks widget identity across rebuilds and when you must help it.

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 11 · Widgets & Identity
Back to basics
Flutter has a bug in your list.
You didn't write it — you just forgot a Key. 🔑
🏷 Analogy: Imagine a row of lockers with no numbers. You rearrange them and suddenly everyone's stuff is in the wrong one. A Key is the locker number — it tells Flutter which widget is which, even after a shuffle.

When Flutter reconciles the widget tree after a rebuild, it matches old elements to new widgets using position + runtimeType by default. Most of the time that's fine. But the moment you reorder, insert or remove widgets in a list, Flutter matches by position and gets it completely wrong.

❌ Without Keys

Reorder two stateful list items. Flutter matches them by position. The state (scroll offset, text input, toggle) stays in the old slot. Your items swap visually but their state doesn't follow them.

✅ With Keys

Flutter matches by Key first. The state travels with the widget it belongs to. Reorder, insert, delete — the right state always stays with the right item.

🔑
ValueKey
Keyed by a value — a string, int, enum. Most common choice for list items with a unique ID.
ValueKey(item.id)
🏠
ObjectKey
Keyed by object identity. Use when the object itself is the unique identifier.
ObjectKey(item)
🆔
UniqueKey
Always different. Forces a fresh Element on every rebuild. Use to force recreation — but sparingly.
UniqueKey()
1
ValueKey is your default for lists.
If your list items have a unique ID from your model, use ValueKey(item.id). Flutter will correctly track each item through reorders and mutations.
2
GlobalKey is a different beast entirely.
It lets you access a widget's state or render box from anywhere in the tree. Useful for things like Form validation. But it's expensive — it registers globally and should be used sparingly.
3
UniqueKey forces recreation — which is sometimes exactly what you want.
Assigning a new UniqueKey() to a widget on rebuild tells Flutter "treat this as brand new." Great for resetting a widget's internal state on demand.
// ❌ Bug: state doesn't travel with the widget during reorder ListView( children: items.map((item) => TodoItem(item: item)).toList(), ) // ✅ Fixed: Flutter matches by ID, state follows the widget ListView( children: items.map((item) => TodoItem( key: ValueKey(item.id), // 👈 that's it item: item, )).toList(), )
Where Keys live matters too. A Key must be placed on the widget at the point where siblings can be confused — usually directly inside a Row, Column or ListView. Placing it deep inside a subtree does nothing because the confusion happens at the sibling level.
Keys are Flutter's way of giving widgets a stable identity across rebuilds. You only notice they're missing when something with state gets reordered. As a rule: every stateful widget inside a dynamic list needs a ValueKey.
Have you ever shipped a bug where a checkbox stayed checked on the wrong item after a list update? That was a missing Key. Where else in our app might we have this silently happening right now?

Why Keys Matter

Flutter's element reconciliation algorithm is elegant but positional by default. This is a deliberate performance trade-off — matching by position is O(1) and works perfectly for static lists. The problem only surfaces with dynamic lists: reordering, inserting, or removing stateful widgets. Without Keys, Flutter will preserve state in the wrong element, leading to bugs that are notoriously hard to reproduce and diagnose.

Key insight: Keys don't make Flutter faster — they make it correct. Every stateful widget inside a reorderable or dynamic list needs a ValueKey. This is non-negotiable.

Discussion Thread

Think about the lists in your current Flutter app — todo items, messages, notifications. Are they stateful? Are they reorderable or dynamically updated? If your answer is yes to both and you don't see ValueKey in the code, you may have a silent bug waiting to surface in production.

Day 12 · State / Architecture

Exception Lifecycle

Your Bloc should never use try/catch — here's why, and where exceptions actually belong in your architecture.

Habib Soula
Written by Habib Soula
Flutter Minute
Day 12 · State / Architecture
Performance
Why should your Bloc never use try/catch?
Analogy: A hospital triage desk. Raw injuries (exceptions) arrive at the ER door (data source), the triage nurse (repository) diagnoses and tags them with a chart (Result<T>), and the doctor (Bloc) only ever reads the chart — never touches the wound directly.
1
Data sources throw — that's their job
Dio throws DioException, Firebase throws FirebaseAuthException. Third-party code you don't control. If you let these leak upward, every layer couples to libraries it shouldn't know about.
2
The repository is the boundary — exceptions die here
Wrap calls in try/catch, map raw exceptions to your own domain types, and return a sealed Result<T>. Nothing above this layer knows Dio exists. The exception is dead — it's a value now.
3
Bloc switches on Result, UI reacts to State
The Bloc receives Result<T>, pattern-matches with switch, and emits a state. The UI just renders states — it has no idea what an exception even is. Dart 3 sealed classes make the switch exhaustive: miss a case and the compiler stops you.
result.dart
sealed class Result<T> {
  const Result();
  const factory Result.ok(T value) = Ok._;
  const factory Result.error(AppException error) = Err._;
}

final class Ok<T> extends Result<T> {
  const Ok._(this.value);
  final T value;
}

final class Err<T> extends Result<T> {
  const Err._(this.error);
  final AppException error;
}
repository
Future<Result<User>> login(String phone, String otp) async {
  try {
    final dto = await _source.login(phone, otp);
    return Result.ok(dto.toDomain());
  } on DioException catch (e) {
    return Result.error(_mapDio(e));
  } on Exception catch (_) {
    return Result.error(ServerException('Unexpected error'));
  }
}
bloc
final result = await _repo.login(phone, otp);

switch (result) {
  case Ok(:final value):
    emit(LoginSuccess(value));
  case Err(:final error):
    emit(LoginFailure(error.message));
} // exhaustive — miss a case, compiler stops you
Exceptions become values. Values become states. States become pixels. Keep try/catch in one place — the repository — and the rest of your app stays pure, testable, and type-safe.
If Result<T> replaces try/catch for business errors, what role does addError / onError still play in your Bloc — and when would you actually use it?

Why This Architecture

The "exceptions become values" pattern is one of the most impactful architectural decisions in a Flutter app. When exceptions leak through layers, every layer must know about libraries it shouldn't care about — your Bloc ends up importing Dio, Firebase, or whatever networking library you're using. The Result type pattern enforces a clean boundary: the repository is the last place any exception is allowed to exist as an exception.

Key insight: try/catch belongs in exactly one layer — the repository. Above that boundary, there are no exceptions, only typed values. Dart 3 sealed classes make this pattern exhaustive and compiler-enforced.

Discussion Thread

addError and onError in Bloc are designed for truly unexpected errors — programmer mistakes, null pointer exceptions, things that shouldn't happen in a correct program. Result<T> handles expected business failures. Understanding which bucket a failure falls into is the key architectural judgment call.

Weekend Special · Flutter Fun Facts

The Most Infamous Bug

Scrolling a Flutter list with two fingers made it scroll twice as fast. Three fingers? Three times faster. This is the story of a 6-year-old bug.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Weekend Special · Flutter Fun Facts
Did You Know?
Scrolling a Flutter list with two fingers made it scroll twice as fast.
Three fingers? Three times faster. 🖐️
🐛 The Bug: Each finger touching a scrollable list was treated as a separate scroll event — and they all stacked. More fingers = more scroll velocity. Simple math. Terrible UX.
☝️
1 Finger
1× speed
✌️
2 Fingers
2× speed
🤟
3 Fingers
3× speed
🖐️
5 Fingers
5× speed
17
August 2017 · GitHub Issue #11884
The bug is born — before Flutter 1.0.
A developer files the issue while Flutter is still in alpha. Nobody panics. The team has bigger fish to fry. The ticket sits quietly, collecting dust and GitHub reactions.
18
December 2018 · Flutter 1.0
Stable release. Bug still ships.
Flutter reaches production-ready status. The two-finger scroll bug comes along for the ride, officially shipping to the world. Community members start noticing, posting memes, testing with all five fingers.
19-22
2019 – 2022 · The Long Wait
Over 100 comments. Zero fix.
Developers pile onto the GitHub issue for years. It becomes a running joke in the Flutter community. New contributors ask "is this still a thing?" — yes, yes it is. The ticket accumulates over 100 reactions and earns legendary status.
23
Late 2023 · Flutter 3.18
After 6+ years — it's finally fixed. 🎉
The Flutter team refactored the scroll physics to correctly handle multiple simultaneous pointer events. One scroll at a time, regardless of how many fingers you use. The community celebrated as if a 6-year siege had finally ended.
🔖
The original GitHub issue #11884 — filed August 2017, 100+ comments, 6+ years of community suffering, now closed. A monument to patience.
🐛
Scrolling with multiple fingers scrolls too fast · Issue #11884
flutter/flutter · github.com · Issue #11884 · Closed 2023
Fix: Correct scroll velocity when using multiple pointers · PR #136708
flutter/flutter · github.com · Merged · Flutter 3.18
A bug filed before Flutter even had a stable release survived 3 major versions, hundreds of community comments, and 6+ years of production use — and the fix, when it finally came, was just a few lines of scroll physics logic.
Have you ever shipped an app and secretly wondered if your users were two-finger scrolling into the void? What's the oldest bug you've ever let live in production? 👇😄

Why This Bug Was So Hard to Kill

The multi-finger scroll bug lived in Flutter's gesture arena — specifically in how multiple PointerDownEvents were each spawning their own scroll drag. The fix required understanding the pointer event lifecycle and ensuring that only one drag is ever active on a scrollable at a time. Simple to describe, surprisingly tricky to get right without breaking legitimate multi-touch use cases like pinch-to-zoom.

Key insight: When a bug is this old and this well-known, there's usually a good reason it wasn't fixed sooner — either the fix is harder than it looks, or it touches code that's risky to change. In this case, it was both.

Weekend Discussion

What's the worst multi-touch bug you've seen in a production app? Have you ever tested your Flutter scrollables with multiple fingers? Go try it on your oldest project right now — you might be surprised.

Day 15 · UI/UX & Animations

Implicit vs Explicit Animations

Flutter has two animation systems. Picking the wrong one costs you days of pain. Learn which to reach for — and when to upgrade.

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 15 · UI/UX & Animations
UI/UX
Flutter has two animation systems.
Picking the wrong one costs you days of pain. 🎭
🎛 Analogy: Implicit animations are like cruise control — you set the destination and the car handles the transition. Explicit animations are manual gears — you control every shift, every rev, the exact moment things happen.
Most Flutter developers reach for AnimationController first because it sounds more powerful. Then they spend 3 hours fighting with vsync, Tween, AnimatedBuilder, and disposal logic — for an animation that AnimatedContainer would have solved in 3 lines.
💚 Implicit (AnimatedFoo)
· Flutter manages the controller
· Just change a value in setState()
· AnimatedContainer, AnimatedOpacity…
· Use for: value transitions, state-driven UI
· No controller, no disposal needed
🟡 Explicit (AnimationController)
· You manage the controller lifecycle
· Full control: repeat, reverse, sequence
· AnimatedBuilder, AnimatedWidget…
· Use for: looping, physics, complex choreography
· Must dispose() the controller
🗺 Decision Guide
A button that fades in when it becomes active
AnimatedOpacity
A card that grows when tapped, shrinks when released
AnimatedScale
A loading spinner that loops forever
AnimationController
A staggered list entrance (items appear one by one)
AnimationController
// Implicit: animates automatically when _visible changes in setState AnimatedOpacity( opacity: _visible ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: const MyWidget(), ) // Explicit: you control the controller — but remember to dispose! late final AnimationController _ctrl = AnimationController( vsync: this, duration: const Duration(seconds: 1), )..repeat(reverse: true); @override void dispose() { _ctrl.dispose(); super.dispose(); }
The golden rule: if your animation is triggered by a state change (show/hide, enabled/disabled, progress value), use implicit. If your animation has its own lifecycle (looping, sequencing, responding to gestures mid-flight), use explicit. When in doubt, start implicit — you can always upgrade.
Implicit animations are criminally underused. They handle the vast majority of UI transitions — state changes, fades, slides, sizes — with zero controller boilerplate. Reach for explicit only when you need choreography, looping, or gesture-driven control.
Look at the last animation you wrote. Was it implicit or explicit? Could it have been simpler? And — has anyone actually used TweenAnimationBuilder yet? Where does that one fit?

Why This Choice Matters

Flutter's animation system is one of its most powerful features — but the split between implicit and explicit trips up developers at every level. The implicit APIs (AnimatedContainer, AnimatedOpacity, AnimatedScale, etc.) were designed specifically to handle the most common case: animating between two states when something changes. Using AnimationController for these cases adds unnecessary complexity, lifecycle management burden, and memory leak risk if dispose() is forgotten.

Key insight: The implicit/explicit split isn't about power — it's about who owns the animation lifecycle. When your UI state owns the animation, go implicit. When the animation has its own lifecycle, go explicit.

Discussion Thread

Look back at your recent Flutter code. How many AnimationControllers could have been replaced with an AnimatedFoo widget? And what about TweenAnimationBuilder — the hybrid that lets you animate arbitrary values implicitly? Share your findings with the team.

Day 16 · Architecture & Patterns

Dependency Injection

When every screen manually builds its own services, your app becomes untestable and unmaintainable. DI separates object creation from usage — making your Flutter architecture scalable.

Ala Makhlouf
Written by Ala Makhlouf
Flutter Minute ⚡
Day 16 · Dependency Injection
Dependency Injection
What happens when your app grows — and every screen manually builds its own services, repositories, and data sources? 🧩
🎯 Analogy: Imagine a restaurant where the chef has to go to the farm to grow vegetables, build the stove, and make the plates before cooking — that's chaos. In a well-designed restaurant, suppliers bring the ingredients, factories provide the equipment, and the chef focuses only on cooking.
Coupling
Manual instantiation creates tight coupling.
Testing
Manual instantiation makes services harder to test.
1
Constructor Injection (Manual DI)
class LoginViewModel {
  final AuthService authService;

  LoginViewModel(
    this.authService,
  );
}

// Usage
final viewModel = LoginViewModel(
  AuthService(ApiService()),
);

2
Provider (DI through Widget Tree)
import 'package:provider/provider.dart';

MultiProvider(
  providers: [
    Provider(
      create: (_) => ApiService(),
    ),

    Provider(
      create: (context) => AuthService(
        context.read<ApiService>(),
      ),
    ),
  ],

  child: MyApp(),
);

3
Service Locator (get_it)
import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

void setup() {
  getIt.registerLazySingleton<ApiService>(
    () => ApiService(),
  );

  getIt.registerLazySingleton<AuthService>(
    () => AuthService(
      getIt<ApiService>(),
    ),
  );
}

// Usage
final auth = getIt<AuthService>();
Dependency Injection separates object creation from object usage, making your Flutter architecture scalable and testable.
Do you think projects remain maintainable without Dependency Injection, or does architecture eventually require it?

Why Dependency Injection

As Flutter apps grow, manually instantiating services deep inside screens creates a tangled web of dependencies. Every screen becomes responsible for constructing its own dependency graph — making testing painful (you can't swap real services for fakes) and changes ripple unpredictably. DI flips this: dependencies are declared, not constructed. Whether you use constructor injection, Provider through the widget tree, or a service locator like get_it, the result is the same: objects are created in one place and consumed in another.

Key insight: DI isn't a specific package — it's a principle. Even plain constructor injection counts. The goal is to separate where dependencies are created from where they're used.

Discussion Thread

Do you think projects remain maintainable without Dependency Injection, or does architecture eventually require it? Have you run into a real bug or testing problem that DI would have solved? Share your experience with the team.

Day 17 · Performance & Memory

dispose() or Die

You leave a screen. Your AnimationController? It's still running. Flutter never cleans up after you — dispose() is the only real off switch for everything that ticks, listens, or breathes in your State.

Mahdi Menaa
Written by Mahdi Menaa
Flutter Minute ⚡
Day 17 · Performance & Memory
Under the Hood
Tu quittes un écran. Ton AnimationController, lui ?
Il est toujours en train de tourner. 💀
🏠 Analogy: C'est comme partir en vacances en laissant toutes les lumières allumées. La maison ne sait pas que tu es parti.
1
Le Ticker ne dort jamais.
Quand tu crées un AnimationController, Flutter crée en interne un Ticker — un métronome qui bipe 60 fois par seconde, accroché au SchedulerBinding global. Naviguer vers un autre écran détruit le widget visuellement, mais le Ticker continue de tourner.
2
dispose() est le seul vrai off switch.
Appeler _controller.dispose() déclenche une chaîne : AnimationControllerTickerSchedulerBinding. Le Ticker se désenregistre, les listeners sont libérés, la mémoire est rendue. Sans ça, Flutter te crie dessus en debug : "A Ticker was active when the State was disposed." — ce n'est pas un warning anodin.
3
Le vrai danger : le memory leak silencieux en prod.
En production, pas de crash immédiat. Chaque navigation crée un nouveau Ticker fantôme. Sur une session longue : mémoire qui grimpe 📈, frames droppées 🐌, batterie qui fond 🔋. Et ce n'est pas que les animations — TextEditingController, ScrollController, FocusNode, StreamSubscription... tous ont besoin de dispose().
With dispose()
Ticker stoppé. Mémoire libérée. Aucun résidu.
Clean ✓
⚠️
Debug Mode
Flutter warn : "Ticker still active". Au moins tu le sais.
Détectable
💀
Production
Leak silencieux. Tickers fantômes. Perf qui se dégrade.
Dangereux
Flutter ne nettoie jamais derrière toi — quitter un écran ne suffit pas, dispose() est le seul vrai off switch pour tout ce qui tick, écoute, ou respire dans ton State.
On parle souvent de performance au niveau des widgets et des rebuilds — mais combien de nos écrans actuels ont un dispose() complet ? Et est-ce que le pattern AutoDispose de Riverpod résout vraiment ce problème — ou est-ce qu'il le cache juste ? 👀

Why dispose() Matters

Flutter's widget lifecycle gives you initState() to set things up — but the contract demands a matching dispose() to tear them down. AnimationController, TextEditingController, ScrollController, FocusNode, and StreamSubscription all hold resources that the framework will never reclaim on its own. In debug mode you'll see warnings; in production the leaks are silent — phantom Tickers fire every frame, memory climbs, and frame rates drop. The fix is always the same: override dispose() and call .dispose() on every controller you created.

Key insight: Every resource you create in initState() needs a matching .dispose() call. If you're using Riverpod's AutoDispose, it handles this automatically — but only for providers, not for local controllers.

Discussion Thread

How many screens in your current project have a complete dispose() implementation? And does Riverpod's AutoDispose truly solve this problem, or does it just hide it? Share your thoughts with the team.

Day 18 · Rendering / Element Tree

Element Reuse

Flutter doesn't recreate the Element tree from scratch on every rebuild — it reconciles. Understanding the two-step check (runtimeType + Key) that controls whether an Element is reused or replaced gives you direct control over whether State lives or dies.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 18 · Rendering / Element Tree
Under the Hood
How does Flutter decide whether to reuse an Element or create a new one? 🌳
🎯 Analogy: Think of Elements like hotel rooms. If a returning guest (Widget) matches the room type and number (runtimeType + Key), they keep their room and belongings (State). A new guest type means a fresh room from scratch.
1
Flutter first checks runtimeType
During reconciliation, Element.updateChild() compares the existing Element's widget type with the incoming widget. If the runtimeType differs, the old Element is unmounted immediately and a brand-new one is inflated — no reuse, no negotiation.
2
Then it checks the Key (if provided)
If runtimeType matches, Flutter looks at the widget's Key. A mismatched or missing key where one previously existed still forces a new Element. Keys let Flutter distinguish siblings of the same type — critical when reordering lists or stateful widgets inside a Row or Column.
3
On a match: update, not replace
When both checks pass, Flutter calls element.update(newWidget). The Element — and its associated State for StatefulWidget — stays alive. Only the widget reference is swapped, keeping expensive state and render objects intact across rebuilds.
Flutter reuses an Element only when the new widget's runtimeType and Key both match the existing one — control those two, and you control whether State lives or dies.
We've all hit bugs where State resets unexpectedly when reordering widgets. Now that you know the runtimeType + Key rule — can you think of a place in our codebase where missing keys might be silently discarding State? Tomorrow we'll explore how GlobalKey takes this even further by moving Elements across the tree entirely. 🔑

Why Element Reuse Matters

Every time Flutter rebuilds a widget, it runs a reconciliation pass through the Element tree. This isn't a full teardown and rebuild — Flutter is looking for opportunities to update existing Elements rather than replace them. The rule is simple but powerful: if the incoming widget has the same runtimeType and Key as the current Element's widget, that Element (and any associated State) is preserved. A type mismatch forces a complete remount. This is why swapping a Container for a SizedBox destroys child state, and why unkeyed siblings of the same type can silently steal each other's state when reordered.

Key insight: runtimeType and Key are the two levers Flutter uses to decide Element identity. Get them right, and you get predictable, efficient state preservation. Get them wrong, and you get mysterious state resets in production.

Discussion Thread

Have you ever hit a bug where State was unexpectedly reset — or unexpectedly preserved — when reordering widgets? Now that you understand the runtimeType + Key rule, can you identify any spots in the codebase where missing keys might be silently causing issues?

Day 19 · Engine / Memory

Tree Shaking

How Flutter automatically shrinks your app binary — without you deleting a single line of code. The Dart AOT compiler builds a call graph from main() and discards everything unreachable.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 19 · Engine / Memory
Under the Hood
How does Flutter automatically shrink your app binary — without you deleting a single line of code? 🌲✂️
🎯 Analogy: Tree-shaking is like a smart grocery order — instead of buying the whole supermarket, the compiler reads your recipe (code) and orders only the exact ingredients used. Unused aisles never make it to your cart.
1
The compiler builds a call graph from main()
During flutter build --release, the Dart AOT compiler starts at main() and traces every function, class, and constant that is actually called or instantiated. This forms a graph of all reachable code — everything outside that graph is dead code.
2
Dead code is erased before the binary is written
Any class, method, or library that has no path from main() is simply not included in the compiled output. This applies to your own code and the Flutter framework itself — unused widgets like Stepper or DatePicker get fully removed if you never reference them.
3
You can inspect exactly what survived with --analyze-size
Running flutter build apk --analyze-size produces a detailed breakdown of every library and symbol kept in your binary — letting you spot bloated packages and verify that unused code was correctly shaken out.
🔬 See It In Action
📦 helpers.dart
// Both classes exist
// in your source.
class PdfExporter {
  void export() { /*...*/ }
  void compress() { /*...*/ }
  void encrypt() { /*...*/ }
}

class CsvExporter {
  void export() { /*...*/ }
}
🎯 main.dart
void main() {
  // Only CsvExporter
  // is reachable from
  // the call graph.
  CsvExporter().export();

  // PdfExporter has
  // zero references →
  // absent from binary.
}
flutter build apk --analyze-size
$ flutter build apk --analyze-size

▒ 5.2 MB   package:flutter
   ▒ 1.8 MB   src/material
   ▒ 0.9 MB   src/widgets
▒  320 KB  package:your_app
   ▒  18 KB   helpers.dart  ← CsvExporter only
▒   80 KB  dart:core
────────────────────────────
  PdfExporter absent — not in output = shaken ✓
  (opens full breakdown in Dart DevTools)
                  
build A · both exporters referenced
helpers.dart ~52 KB
build B · only CsvExporter referenced
~18 KB
* sizes illustrative — real values depend on imports & methods per class
Tree-shaking is not a setting you turn on — it's always running at flutter build --release. Write clean, explicit Dart and the compiler will quietly discard everything your users never need.
Run flutter build apk --analyze-size on our app today — what's the biggest unexpected entry in the breakdown? Is there a package we import "just in case" that might be adding hundreds of KB we never actually use? Tomorrow, we'll explore how Dart's AOT compiler turns this lean, shaken binary into blazing-fast native machine code. ⚙️

Why Tree Shaking Matters

Mobile app size directly affects install rates, update friction, and startup performance. Flutter apps include the framework itself — but that doesn't mean you pay for all of it. The Dart AOT compiler's tree-shaking pass ensures that only the widgets, classes, and constants your app actually uses are compiled into the binary. Unused framework code like Stepper, DatePicker, or entire packages you import but never call gets silently discarded. The result is a lean binary that reflects exactly what your app does — nothing more.

Key insight: Tree-shaking is free and automatic on every flutter build --release. The best way to help it is to avoid dynamic dispatch patterns (like dart:mirrors) that prevent the compiler from tracing the full call graph.

Discussion Thread

Have you ever run flutter build apk --analyze-size and been surprised by what was in the output? Are there packages in our project imported "just in case" that might be contributing dead weight to the binary — or is tree-shaking already handling them cleanly?

Weekend Special · Flutter Community Legends

The State Management War

One innocent recipe app Reddit post accidentally summoned every state management zealot at once — and the thread has never been the same since.

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Weekend Special · Flutter Community Legends
Did You Know?
One innocent recipe app Reddit post accidentally summoned every state management zealot at once. 🍝🔥
📮 The Post: A Flutter beginner asked r/FlutterDev — "What state management should I use for a simple recipe app?" — and then walked away. They came back to 200+ comments, 14 nested debates, and a thread that had somehow pivoted to philosophy.
🧊
The Bloc Monks
"Events. States. Streams. Structure is love. Structure is life. You'll thank us in year 3."
🌿
The Riverpod Saints
"Provider is deprecated, Bloc is overkill. Riverpod is the way. It was always the way."
🐻
The GetX Cowboys
"One line. No boilerplate. No context. No problem. GetX does everything. Calm down."
🌀
The setState Purists
"It's a recipe app. Use setState. Please. For the love of all things holy. Use setState."
T+0
The Post · "Simple recipe app, what state management?"
The spark is lit.
A beginner, armed with curiosity and zero idea of what they'd unleashed, posts what they assume is a quick question. They expect three replies. They get a civilisation.
T+1h
First Wave · The Recommendations Arrive
Friendly advice turns into faction warfare.
First 20 comments are genuinely helpful. Then someone says "just use Bloc", someone replies "Bloc for a recipe app??", and the thread folds in on itself like a neutron star.
T+6h
Peak Chaos · The Philosophy Detour
200+ comments. Someone invokes Uncle Bob.
The debate has evolved past Flutter entirely. We're now discussing Clean Architecture, the nature of reactive programming, and whether beginners deserve suffering as a learning tool. The original poster has not been seen since.
T+24h
The Epilogue · OP Returns
"I used setState. The app works. Thank you all." 😭
The beginner resurfaces, shell shocked but fed. They chose the simplest answer. The thread erupts one final time — half celebrating, half devastated. The post is now referenced in at least three YouTube tutorials as a cautionary tale.
200+
Reddit comments
on a recipe app
4
Warring state
management factions
1
setState call
that ended it all
🍝
The real lesson: The Flutter community doesn't have a state management answer. It has five passionate answers, each convinced the other four are leading you to ruin — and they all show up the moment you ask.
There is no "right" state management for Flutter — only the one that fits your team, your app's complexity, and your current phase of enlightenment. The recipe app doesn't care. Ship the recipe app.
Which faction are you in — and be honest: have you ever left a state management comment on a beginner's post that was 10× more complex than what they actually needed? 👇😄 No judgment. We've all been there.

Why This Thread Became Legend

The "state management for a recipe app" thread became a piece of Flutter community folklore because it perfectly captured a real tension: the ecosystem's richness is also its curse. With at least five well-maintained, opinionated state management solutions — each backed by passionate developers — a beginner asking an innocent question can end up drowning in options. The thread wasn't just funny; it was a mirror the community needed. It's why "setState for simple cases" is now effectively gospel, passed down from senior devs every time a new thread starts.

Key insight: The best state management solution is the simplest one that doesn't get in your way. For a recipe app, setState is not a cop-out — it's the correct engineering choice. Reach for complexity only when simplicity breaks.

Discussion Thread

In our own codebase — do you think we reached for a state management solution that was the right fit for our app's complexity at the time? Or did we accidentally over-engineer? What would you choose if you were starting fresh today?

Day 22 · Rendering

Skia vs Impeller

Flutter owned its renderer from day one — so why did Skia eventually become the bottleneck, and what makes Impeller the future?

Amine Brahmi
Written by Amine Brahmi
Flutter Minute ⚡
Day 22 · Rendering
Rendering
Flutter owned its renderer from day one — so why did Skia eventually become the bottleneck, and what makes Impeller the future? 🎨
🎯 Analogy: Skia is like a brilliant chef who improvises every dish at serving time — creative, but slow on busy nights. Impeller is the chef who preps everything in the morning, so service is always instant.
1
Why Flutter chose Skia — full rendering ownership
Flutter skips the platform's native UI entirely. Instead, Skia paints every pixel directly onto a Canvas, giving Flutter pixel-perfect consistency across Android, iOS, and desktop. No OEM widget quirks — what you design is exactly what users see.
2
Skia's fatal flaw — runtime shader compilation
Skia compiles GLSL shaders on the fly the first time each effect is encountered. This causes "jank" — visible frame drops — especially on iOS. The ShaderMask, gradients, or clip animations stutter on first render because the GPU driver is busy compiling. This is the infamous shader compilation jank problem.
3
Impeller — pre-compiled shaders, zero surprises
Impeller ships with a fixed, known set of shaders that are compiled at build time, not at runtime. It uses Metal on iOS and Vulkan on Android, eliminating cold-compilation stalls entirely. The result: smooth 60/120fps from frame one, with a predictable DisplayList rendering model that also enables better multi-threading.
⚠️ Scenario: A gradient + blur animation that janks on first render with Skia, but runs silky-smooth with Impeller.
With Skia — first-frame jank
This widget triggers shader compilation on the GPU the first time it renders. You'll see a visible stutter on iOS especially.
Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.teal, Colors.purple],
    ),
    borderRadius: BorderRadius.circular(20),
  ),
  child: BackdropFilter(
    filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
    child: Text("Hello Impeller"),
  ),
)
// 🐌 Skia compiles the blur + gradient shader
// at runtime → frame spike on first render
With Impeller — zero jank, same code
No code change needed. Enable Impeller in Info.plist (iOS) or AndroidManifest.xml (Android) and the exact same widget renders instantly — shaders were already compiled at build time.
// iOS — Info.plist
<key>FLTEnableImpeller</key>
<true/>

// Android — AndroidManifest.xml
<meta-data
  android:name="io.flutter.embedding.android
                  .ImpellerEnabled"
  android:value="true" />

// ✨ Same widget. Pre-compiled shaders.
// Smooth 60/120fps from frame one.
🔍
How to confirm jank in DevTools
Run in profile mode and open the Flutter DevTools Performance tab. With Skia you'll see a tall GPU thread spike on the first frame of the animation. With Impeller, the GPU thread stays flat and consistent — no surprise compilation spikes.
Skia gave Flutter independence; Impeller gives it smoothness — the switch from runtime to build-time shader compilation is what finally kills jank for good.
Impeller's strength is its fixed shader set — but does that limit rendering flexibility compared to Skia's dynamic approach? Have you ever hit shader jank in production, and did flutter --profile traces reveal it? Tomorrow we'll look at how Flutter's Layer tree decides what actually gets repainted…

Why Rendering Architecture Matters

Most frameworks delegate rendering to the platform — Flutter doesn't. By owning its renderer entirely, Flutter achieves pixel-perfect consistency across every OS, but it also inherits the renderer's limitations. Skia's runtime shader compilation model was a pragmatic choice that worked well for years, but as Flutter apps became more visually ambitious — with complex blur effects, gradients, and clip animations — the GPU's just-in-time compilation overhead became impossible to hide. Impeller is the answer: a renderer designed specifically for Flutter's rendering patterns, with a fixed, pre-compiled shader library that eliminates cold-compilation completely.

Key insight: Impeller is now the default renderer on iOS (Flutter 3.16+) and is stable on Android. If you're still seeing jank on first-frame animations, make sure Impeller is enabled — and check flutter doctor to confirm your Flutter version supports it.

Discussion Thread

Have you ever profiled a Flutter app in release mode and noticed GPU thread spikes on first animation frames? Did switching to Impeller or using flutter run --profile help isolate the issue — or did the jank turn out to be something else entirely?

Day 23 · Security

The envied Myth

envied is one of the most popular packages for hiding API keys in Flutter apps — but does it actually protect your secrets from a determined attacker?

Yassine Sabri
Written by Yassine Sabri
Flutter Minute ⚡
Day 23 · Security
Security Alert
Is envied actually protecting your API keys from reverse engineering? 🔓
🎯 Analogy: envied is like writing your password on a sticky note, then putting another sticky note on top. Anyone who lifts the top note sees the password. XOR obfuscation is just two layers of paper.
1
envied XORs your secret with a random key
It generates two integer arrays: a random key and your secret XOR'd with that key. Both are stored as List<int> constants in the compiled Dart code. At runtime, it XOR's them back to get the original string.
2
blutter dumps the entire object pool
The tool blutter parses the Dart AOT snapshot and outputs every constant, including those integer arrays. They appear as consecutive List<int> pairs with matching lengths in pp.txt.
3
XOR decoding takes one line of Python
chr((key[i] ^ data[i]) & 0xFF) for each index. That's it. We decoded Stripe live keys and Keycloak credentials from a production APK in under 5 minutes.
envied provides obfuscation, not encryption. Any attacker with blutter can decode all your secrets in minutes. Never put real secrets in client apps — use server-side secret management.
If envied can't protect secrets, what's the right way to handle API keys in a Flutter app? Hint: think about what should never leave the server.

Why envied Doesn't Save You

The envied package is widely used with the best of intentions — keep secrets out of source code by generating obfuscated constants at build time. But obfuscation is not encryption. The XOR scheme stores both the key and the ciphertext in the same binary, making it trivially reversible. Tools like blutter were built precisely to extract Dart AOT object pools, and envied-protected secrets appear as obvious matching List<int> pairs. The package solves the source-code leakage problem, not the APK reverse-engineering problem.

Key insight: Any secret that must be decoded by the app at runtime is a secret the attacker can also decode. The correct architecture: secrets never enter the client. Use a backend proxy or a token exchange flow so the mobile app only ever holds short-lived, scoped credentials — never raw API keys.

Discussion Thread

Does your current app use envied — or any other obfuscation package — for secrets that could cause real damage if extracted? What would a server-side refactor look like for your most sensitive key?

Day 24 · Security

What Blutter Sees

Flutter's --obfuscate flag hides symbol names — but how much of your app is actually still visible to an attacker with the right tool?

Yassine Sabri
Written by Yassine Sabri
Flutter Minute ⚡
Day 24 · Security
Reverse Engineering
What can an attacker see inside your release APK — even with obfuscation enabled? 👁️
🎯 Analogy: --obfuscate is like renaming all the rooms in your office building. The rooms still exist, the documents are still inside — you just changed the signs on the doors. Anyone who opens a door still finds everything.
1
Strings are NEVER obfuscated
Flutter's --obfuscate flag only renames class and function names. All string literals — API URLs, error messages, config values — remain in plaintext inside libapp.so. Running strings on the binary reveals every URL, every endpoint path, every error message.
2
blutter reconstructs the full object pool
In our Smoov audit, blutter extracted 72,129 entries from the object pool: every class name, every constant, every string, every integer array. We found 33 API endpoints, 3 environment configurations, and the complete backend microservice architecture.
3
Even "hidden" secrets are visible
envied-encrypted values appear as List<int> pairs. Bundled certificates, Firebase configs, Stripe merchant IDs — everything the app needs at runtime is in the binary. If the app can read it, so can an attacker.
Assume everything in your APK is public. --obfuscate hides symbol names, not data. Design your architecture so that the APK contains nothing an attacker could use alone.
We found 3 full environment configs (prod, test, dev) in one APK — including internal domain names and separate credentials for each. How does your app handle environment switching? Could we find your staging URLs?

Why blutter Changes Everything

Before tools like blutter, reverse-engineering a Flutter app required deep expertise in Dart AOT internals. blutter automated it: point it at libapp.so, get back a structured dump of every object in the pool. The 72,129-entry output from a single real-world APK shows how much information is simply sitting there — class hierarchies, API surface, configuration environments, and obfuscated-but-decodable secrets. Flutter's closed rendering model makes the UI opaque to traditional Android reverse-engineering, but the data layer is fully exposed.

Key insight: The threat model for a mobile app must assume the binary is public. Design accordingly: no secrets in the binary, no trust placed on client-side checks, and a backend that validates every request independently of what the client claims.

Discussion Thread

If you ran blutter against your own app's release APK right now, what would it find? Internal API hostnames? Staging credentials? Feature-flag config? What would be the most damaging thing on that list?

Day 25 · Security

Real World Impact

We extracted a live Stripe secret key from a production APK — here's exactly what an attacker could do with it, and why the fix is architectural.

Yassine Sabri
Written by Yassine Sabri
Flutter Minute ⚡
Day 25 · Security
Case Study
We extracted a live Stripe secret key from our own APK — what could an attacker do with it? 💳
🎯 Analogy: A Stripe secret key in your APK is like leaving your company credit card in the lobby with the PIN written on it. Anyone who picks it up can charge whatever they want.
1
We confirmed full Stripe API access
The sk_live_ key we extracted was tested against the Stripe API. Balance check: 200 OK. List charges: 200 OK. Account info: 200 OK. 5 out of 5 endpoints returned full data. This key could create charges, issue refunds, and access customer payment data.
2
We decoded 27 secrets from a single APK
Across 3 environments: 3 Stripe secret keys (2 live!), 2 Keycloak client secrets, client IDs, OAuth configs, internal domain names, and the full backend API surface with 33 endpoints. All from envied-encrypted constants that took minutes to crack.
3
The fix is architectural, not tactical
Switching from envied to another obfuscation package doesn't solve the problem. The only solution: secrets must live on the server. The mobile app should receive temporary, scoped tokens — never raw API keys. Stripe integration must be server-side only.
If your app contains a secret key that could cause damage in an attacker's hands, it WILL be extracted. Move all sensitive operations server-side. The mobile app should be a dumb client that holds nothing valuable.
Review your current app: what secrets are embedded in the binary right now? Could you move them all server-side by next sprint? What would need to change in the architecture?

Why This Case Study Matters

This wasn't a theoretical attack — it was a real audit of a production Flutter app. The combination of envied-obfuscated secrets and blutter's object pool extraction took under 10 minutes from APK to plaintext Stripe secret key. The 27 secrets found weren't all equally dangerous, but two live Stripe secret keys with full API access represent a worst-case outcome: financial fraud, customer data exposure, and compliance violations — all from a single downloadable APK. The lesson isn't to use better obfuscation; it's that obfuscation is the wrong tool for the job entirely.

Key insight: A sk_live_ Stripe key in your binary is a critical vulnerability. Stripe integration belongs on the server: your backend creates Payment Intents and returns client secrets. The mobile app never sees the secret key. This pattern also applies to any third-party API that can act on behalf of your business.

Discussion Thread

What's the most sensitive secret currently in your app's binary? Have you ever done a security audit of your own APK — and if you ran blutter against it today, what would you find?

Day 26 · Performance & Rendering

The Rendering Pipeline

You call setState(). Exactly what happens between that line and the pixel? The 5-stage pipeline from build to GPU compositing — and which stage is costing you frames.

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 26 · Performance & Rendering
Performance
You call setState().
Exactly what happens between that line and the pixel? 🖥
🎬 Analogy: Your app is a movie. The rendering pipeline is the production chain — script (widget), casting (element), set construction (render), filming (layer), and final edit (compositor). Each stage is distinct and skipping one causes jank.
You have 16 milliseconds per frame at 60fps — and less than 4ms at 240fps. Understanding what Flutter does in those 16ms helps you know exactly which stage you're accidentally making expensive.
1
Build Phase
setState() marks the Element dirty. Flutter calls build() on dirty Elements, creating new Widget objects (cheap Dart objects). This is the widget tree rebuild.
Cost: cheap — widgets are just config objects
2
Element Reconciliation
Flutter walks the Element tree and matches new widgets to existing elements by runtimeType + key. Matched elements get updated; unmatched ones are inflated or disposed.
Cost: proportional to changed subtree size
3
Layout Phase
RenderObjects receive constraints from parent (max/min width + height). They compute their own size and position their children. A parent passes constraints down; children bubble sizes back up.
Cost: expensive — minimize layout-triggering rebuilds
4
Paint Phase
RenderObjects call paint() to draw onto a Canvas. Flutter records these painting instructions into Layer objects — it doesn't rasterize immediately.
Cost: medium — avoid unnecessary repaints with RepaintBoundary
5
Compositing & Rasterization
The Layer tree is sent to the GPU thread. Skia (or Impeller) rasterizes the layers and composites them. This is when pixels finally appear on screen.
Cost: on the GPU thread — saveLayer() is your enemy here
✅ What to optimize

Reduce dirty subtree size with const. Use RepaintBoundary to isolate frequently painting widgets. Avoid Opacity on animated widgets (use FadeTransition instead). Never use saveLayer() in hot paths.

❌ Common jank causes

Rebuilding a whole screen when only one widget changed. Doing layout inside a scrollable list without caching. Using ClipRRect on animated content. Shader warmup (first-frame jank with Impeller).

Build is cheap. Layout is expensive. Paint can be isolated. Compositing is on the GPU. Knowing which stage is slow tells you exactly where to look in DevTools — and exactly what to fix.
Have you opened the Flutter DevTools Frame Chart and actually looked at which phase eats your frame budget? Build, layout, or paint — which one do you think is hurting us most in our heaviest screen?

Why Understanding the Pipeline Matters

Most Flutter developers know setState() triggers a rebuild — but few know what that actually costs. The rendering pipeline has five distinct stages and each has a different performance profile. Build is nearly free; layout can cascade expensively through a deep tree; compositing is entirely on the GPU thread. Without knowing which stage is the bottleneck, every optimization is a guess. The Flutter DevTools Frame Chart maps these stages directly — build time, layout time, paint time, and GPU time are all visible per frame. This episode gives you the vocabulary to read that chart.

Key insight: A frozen UI means a frame took longer than 16ms (at 60fps). Opening DevTools' Performance tab and recording a slow scroll or animation will show you exactly which stage caused the drop — and that tells you whether to reach for const, RepaintBoundary, or restructure your widget tree.

Discussion Thread

Have you profiled your heaviest screen with Flutter DevTools? Which phase dominates — build, layout, or paint? And have you ever hit GPU-thread jank from saveLayer() or complex compositing?

Day 27 · Performance & Concurrency

Isolates & compute()

Your app is async everywhere — so why does parsing JSON still freeze the UI? The difference between async/await and a real Isolate, and when you need each one.

Jihed Mrouki
Written by Jihed Mrouki
Flutter Minute ⚡
Day 27 · Performance & Concurrency
Performance
Your app is async everywhere.
So why does parsing JSON still freeze the UI? 🧊
🍳 Analogy: async/await is like ordering at a restaurant and doing other things while you wait. The cooking still happens in the same kitchen. An Isolate is opening a second kitchen entirely — work runs in parallel, on a different CPU core.
Dart is single-threaded. async/await doesn't give you threads — it just schedules work on the same event loop, cooperatively. When your JSON response is 50,000 items and you call jsonDecode() synchronously, you're blocking the main isolate. No frames get painted. Your user sees a frozen screen.
⚡ async / await

Runs on the main isolate (same thread). Yields control between ticks — but CPU work still blocks. Good for I/O: waiting for HTTP, reading files, delay timers.

🧵 Isolate

A separate thread with its own memory heap. True parallelism. No shared state — communicates only by message passing. Good for CPU work: parsing, encryption, image decoding.

1
Use compute() for one-shot CPU work.
compute() is Flutter's convenience wrapper. It spawns an isolate, runs your function, returns the result, then disposes the isolate. Perfect for parsing a large API response.
2
Use Isolate.spawn() for long-running background work.
When you need a persistent worker — streaming data processing, background sync, continuous image manipulation — spawn a full isolate with a ReceivePort for two-way messaging.
3
Isolates share nothing. Pass only plain data.
You can't send your BuildContext, a StatefulWidget or a database connection across isolate boundaries. Send raw maps, lists, strings, or primitive types.
// ❌ Freezes UI: CPU work on the main isolate final products = jsonDecode(response.body) .map((j) => Product.fromJson(j)) .toList(); // ✅ compute() runs the heavy parse in a background isolate final products = await compute(_parseProducts, response.body); // Top-level function (required — closures can't cross isolate boundary) List<Product> _parseProducts(String body) { return (jsonDecode(body) as List) .map((j) => Product.fromJson(j)) .toList(); }
Rule of thumb: if a synchronous operation takes more than ~4ms (one frame at 240fps, ~16ms at 60fps), move it off the main isolate. Profile first with DevTools' CPU flame chart — don't guess which code is slow.
async/await prevents waiting — it doesn't prevent blocking. Heavy CPU work (JSON parsing, encryption, image processing) needs a real isolate. compute() gets you there in one line.
Where in our app are we doing heavy computation on the main isolate right now? JSON parsing? Sorting big lists? Image manipulation? How would you know — and how would you measure it?

Why async/await Isn't Enough

The most common misconception in Flutter development: "I made it async, so it's non-blocking." Async/await is cooperative concurrency — it lets the event loop switch between tasks, but CPU-bound work runs uninterrupted until it's done. A synchronous JSON decode of 50,000 items blocks the UI thread for hundreds of milliseconds regardless of how many awaits surround it. Isolates solve this by moving work to a completely separate thread with its own memory heap. Flutter's compute() function makes this a one-liner for the most common case.

Key insight: The rule is simple — I/O work (HTTP, file reads, timers) belongs on the event loop with async/await. CPU work (parsing, encoding, sorting large datasets, image manipulation) belongs in an Isolate. When in doubt, open the DevTools CPU flame chart and look for long synchronous frames.

Discussion Thread

Where in our app are we doing heavy computation on the main isolate right now? JSON parsing? Sorting big lists? Image manipulation? How would you know — and how would you measure it?

Day 28 · Under the Hood

JIT & AOT Compilation

Phones don't understand Dart — only machine code. Flutter translates your code either before or during execution, and knowing which mode runs when changes how you build and ship.

Talel Briki
Written by Talel Briki
Flutter Minute ⚡
Day 28 · Under the Hood
Performance Internals
Why does Flutter use two different ways
(JIT & AOT) to run your code?
🎤 Context: Phones don't understand Dart — only machine code. Flutter must translate your code either before or during execution.
🌍 Analogy: You wrote a speech in English for a Spanish audience — JIT = live translation, AOT = translated beforehand.
1
JIT (Just-In-Time) = compile while running
Dart code is translated on the fly.
Enables hot reload 🔁 and rapid experimentation 🧪.
But adds runtime work and slightly slower execution.
💡 Think: "translate while speaking."
2
AOT (Ahead-Of-Time) = compile before launch
Dart becomes native machine code during build.
No runtime compilation needed — runs directly on CPU.
Result: faster startup 🚀, smoother performance ⚡, better battery 🔋.
💡 Think: "speech already translated."
3
Why Flutter uses BOTH
🛠️ JIT (Debug) → developer speed + hot reload
🚀 AOT (Release) → user speed + smooth UX
Flutter optimizes for building fast AND running fast.
JIT helps you build fast, AOT helps your app run fast — Flutter uses both to optimize the developer experience AND the user experience.
If Flutter removed JIT and only used AOT, how would your daily development workflow change?

Why Flutter Needs Both Modes

The dual compilation strategy is one of Flutter's most deliberate design decisions. During development, you need instant feedback — hot reload lets you see changes in milliseconds without restarting the app. This is only possible with JIT, which compiles code on demand and can swap updated bytecode in place. But JIT comes at a cost: the compilation overhead adds startup latency and reduces throughput. For a user installing your app from the store, none of that matters — they want instant launch and silky-smooth 60fps. AOT delivers this by pre-compiling everything to native machine code at build time, leaving zero compilation work for the device to do at runtime.

Key insight: When you run flutter run, you're in JIT mode (debug). When you run flutter build apk --release, Dart toolchain switches to AOT. The same Dart code, two completely different execution models — each optimized for its context.

Discussion Thread

If Flutter removed JIT and only used AOT, how would your daily development workflow change? Think about hot reload, debug symbols, and the feedback loop you rely on most.

Day 29 · VM Internals

Dart VM Object Pool

Compiled Dart code doesn't embed objects directly — it references them by index from a shared table. Understanding the Object Pool explains how strings, functions, and constants stay compact and fast at runtime.

Talel Briki
Written by Talel Briki
Flutter Minute
Day 29 · VM Internals
Under the Hood
What is the Dart VM Object Pool, and how are strings, functions, and constants actually referenced at runtime?
Analogy: Think of it as a shared toolbox of objects that compiled code can access by index instead of recreating or searching for them.
1
Central table of reusable references
The Object Pool stores pointers to items the compiled code needs: strings, type objects, function entries, constants, and runtime helpers. Machine code loads them by index instead of embedding full objects.
2
Strings are created once and reused
Literal strings are stored a single time in memory. Multiple occurrences reference the same pooled object, reducing duplication and improving cache efficiency.
3
Functions are referenced indirectly
Instead of embedding call targets directly, the pool holds function objects and entry points. Code retrieves the reference and jumps to it, enabling dynamic dispatch and compact binaries.
4
Only compile-time objects are pooled
Runtime instances you create in loops live on the heap. The pool mainly contains canonical constants, metadata, static fields, and VM stubs needed by compiled code.
The Object Pool keeps compiled code small and fast by centralizing shared objects in one place. Instructions stay lightweight, while data is accessed through quick indexed lookups.
How might this pooling strategy affect reverse engineering, memory usage, or binary size in large Flutter applications?

Why the Object Pool Matters

When Dart compiles your app to native code via AOT, the generated machine instructions can't embed full objects inline — doing so would bloat the binary and make position-independent code impossible. Instead, the compiler builds an Object Pool: a flat array of pointers to every string literal, constant, type object, and function entry point that the compiled code needs. At runtime, a single load instruction with an index offset retrieves the reference in nanoseconds. This design keeps instruction size small (indices, not addresses), enables GC to trace all references from one location, and allows the linker to deduplicate identical constants across compilation units.

Key insight: The Object Pool is also why tools like Blutter can extract string constants and function names from a Flutter binary — those objects are pooled and traceable. Obfuscation renames identifiers, but the pool structure remains.

Discussion Thread

How might the pooling strategy affect reverse engineering, memory usage, or binary size in large Flutter applications? And knowing that string literals are deduplicated, does it change how you think about repeated string constants in your code?

Day 30 · Runtime Instrumentation

Frida Runtime Injection

Frida can modify a Flutter app while it's running — without touching the APK or IPA. Understanding how it works is essential for building apps that don't blindly trust their own client-side logic.

Talel Briki
Written by Talel Briki
Flutter Minute
Day 30 · Runtime Instrumentation
Security Internals
How can tools like Frida modify a Flutter app while it's running — without changing the APK or IPA?
Analogy: It's like entering a live theater backstage and whispering new lines to actors during the performance — the show continues, but behavior changes.
1
Attaching to the running process
Frida connects to the app using system debugging mechanisms, gaining the ability to read memory, write memory, and observe execution — all without restarting the app.
2
Injecting a runtime script
A JavaScript runtime is loaded inside the target process. The script can hook native functions, inspect parameters, modify inputs, or change return values dynamically.
3
Intercepting behavior in real time
Instead of permanently patching code, Frida installs hooks. This enables actions like bypassing checks, tracing execution, or altering responses — only while the app is running.
4
Why Flutter apps are still hookable
Even though Flutter compiles Dart to native code, the result is still a normal process with native libraries and system calls. These layers can be intercepted at runtime.
Frida doesn't modify the installed app — it operates from inside the running process, intercepting function calls and altering behavior dynamically.
If attackers can change behavior at runtime without touching your binary, which protections should rely on server-side validation rather than client logic?

Why Runtime Injection Is Possible

Flutter's AOT compilation produces native machine code, which many developers assume makes the app harder to tamper with. But compilation doesn't change the fundamental runtime model: the app is still a process, running on an OS, using system libraries and kernel calls. Frida exploits this by attaching to the process via ptrace (on Linux/Android) or task APIs (on iOS/macOS), then loading a lightweight JavaScript engine (V8) directly into the app's memory space. From there, scripts can intercept any native function — including Flutter engine internals, platform channels, or TLS certificate verification routines.

Key insight: AOT-compiled Flutter apps are not inherently more secure than any other native app. Security must be enforced server-side. Client-side checks (certificate pinning, root detection, license validation) can always be bypassed by a sufficiently motivated attacker with runtime instrumentation tools.

Discussion Thread

If attackers can change behavior at runtime without touching your binary, which protections should rely on server-side validation rather than client logic? Think about your current app's auth flows, feature flags, and payment validations.

Ramadan 1447 · Series Finale

Hall of Fame

Every contribution made this series possible. Here's to the team that showed up.

🥈
Yassine Sabri
Yassine Sabri
5 episodes
2
🥇
Jihed Mrouki
Jihed Mrouki
6 episodes
1
🥉
Talel Briki
Talel Briki
3 episodes
3
All Contributors
Amine Brahmi Amine Brahmi 9 eps
Habib Soula Habib Soula 1 ep
Rania Gahbiche Rania Gahbiche 1 ep
Ala Makhlouf Ala Makhlouf 1 ep
Mahdi Menaa Mahdi Menaa 1 ep
Skander Mallek Skander Mallek showed up ✦
✦ Every line of Flutter knowledge shared here was a gift to the team.
Ramadan Kareem. Until next year.
✦ A Message to the Team

Thank You — From All of Us

Thirty days. Nine contributors. Twenty-eight episodes of genuine Flutter depth — from widget rebuilds and three trees to Skia, Impeller, AOT compilation, and Frida at runtime. None of this existed before Ramadan 1447. You made it.

Jihed Mrouki
Jihed — six episodes of consistent, thoughtful depth. You showed the team what it looks like to keep showing up week after week.
Yassine Sabri
Yassine — five sharp episodes with real curiosity behind every question. Your contributions added a layer of rigor the series needed.
Talel Briki
Talel — three solid episodes that tackled topics others wouldn't. Every one landed.
Habib Soula
Habib — one episode, all heart. Thank you for stepping up and sharing.
Rania Gahbiche
Rania — your contribution brought a perspective the series was richer for having. Thank you.
Ala Makhlouf
Ala — one episode that showed real ownership. We're glad you made it yours.
Mahdi Menaa
Mahdi — you brought your episode to life with care. That counts more than numbers.
Skander Mallek
Skander — you were here every step. That presence matters too. Next Ramadan, it's your turn. ✦
عيد مبارك
Eid Mubarak

May this Eid bring you rest, joy, and time with the people you love. You gave this team 30 days of knowledge and showed up with generosity during a blessed month. That's something worth celebrating.

Until next Ramadan — Flutter Minute returns. 🚀
Contribute

Share Your Knowledge ✍️

Every team member can contribute! Pick ANY topic you're curious about — Flutter, Mobile, AI, or anything tech-related. Write your card using our template, and share it with the team. From widgets to AI models, all topics are welcome. Have fun exploring!

Why Contribute?

Contributing an episode helps you:

💡 Deepen Understanding — Teaching is the best way to truly learn
🎯 Share Expertise — Your unique perspective helps the whole team
🏆 Build Credibility — Showcase your technical depth
🤝 Learn Together — Spark discussions that elevate everyone

How to Contribute

1 Pick ANY Flutter topic you want to explore — widgets, performance, state, architecture, anything!
2 Use the template below to structure your content (4 sections: Question → Insight → Takeaway → Discussion)
3 Share your filled template with Amine on Slack for review
4 Once approved, it gets published to the website and shared with the team!

Topic Ideas (Choose Your Own!)

Here are some areas to explore — pick anything that interests you, from Mobile to AI:

📱
Flutter & Mobile
Widgets, state, navigation, animations
Performance
Optimization, profiling, memory, frames
🤖
AI & ML
TensorFlow, models, integration, ML Kit
🏗️
Architecture
Clean code, patterns, scalability, testing
🔌
Platform & Backend
APIs, databases, native, cloud
🎨
UI/UX & Design
Animations, theming, accessibility, custom

Episode Template

Use this skeleton to create your episode on ANY topic. Fill in each section — Mobile development, AI integration, performance tricks, whatever you want to share!

Flutter Minute ⚡
Day XX · [Your Topic Area]
Under the Hood
[Write a counterintuitive question about YOUR topic that reveals a surprising truth]
Example: Why doesn't constant rebuilding kill performance?
🎯 Analogy: [Real-world comparison that makes the concept click]
1
[First key point headline]
[Explanation with technical details. Use code for APIs and class names.]
2
[Second key point headline]
[Continue explaining the internals step-by-step]
3
[Third key point headline]
[Wrap up the explanation with the final piece]
[One clear sentence that summarizes the entire insight — what they should remember in a week]
[One thought-provoking question for the team to discuss. Bonus: bridge to tomorrow's topic!]

Content Guidelines

✅ What Makes a Great Episode

Clear Question — Poses a "why" not "what", reveals something non-obvious
Real Analogy — Uses everyday concepts to explain complex internals
Technical Accuracy — All claims are verifiable in Flutter source code
Actionable Insight — Team can apply this understanding to real work
Discussion Bridge — Question connects to broader Flutter concepts

❌ What to Avoid

Don't write tutorials ("How to use StatefulWidget")
Don't make unverified claims about performance
Don't use jargon without explaining it
Don't skip the analogy — it's what makes concepts stick

Study the Published Episodes

Before writing, review our published episodes to see the format in action:

Ready to Write?

Copy the template above (use View Source or Inspect Element), fill in your content, and share it with Amine on Slack for review!

💬 Need Help? Ask in the #flutter-team channel! We're all learning together.