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!
✍️ Ready to share? Check out the Contribute section — it has everything you need to get started.
The VDS Flutter Team — Building amazing apps and learning Flutter's secrets together 🚀
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.
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.
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:
📖 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.
Widget Rebuilds & Efficiency
Flutter rebuilds widgets constantly. Most developers assume that means work — it doesn't. Here's why.
So why doesn't your app feel like it's on fire? 🔥
Calling
build() creates a tiny config object. It costs almost
nothing. Think of it as updating a description, not painting pixels.
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.
Layout, paint, compositing — that's all in the RenderObject. Flutter only calls
updateRenderObject() with the new config. No recreation. No drama.
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.
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.
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.
What does each one do — and why can't one tree just do it all?
Widgets are immutable config objects. Every
build() call produces
a fresh widget tree. They describe the UI, they don't own it.
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.
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 |
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.
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.
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?
What if one mixin could fix that in 3 lines? 🚀
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.
| 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 |
cache:
sizeEstimator limits memory by byte budget. Background TTL sweep evicts stale tiles
silently.
| 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 |
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.
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.
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.
Do you actually know what it is? 🌳
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.
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.
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.
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.
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.
mounted before using
context after an await.
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.
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.
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.
So how does your UI ever actually update? 🤔
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.
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.
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.
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.
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.
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.
and its first ever demo was a spinning square. 🌀
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.
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.
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.
From a spinning square to a production-ready framework used by Google, Alibaba, and BMW. In just 4 years.
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.
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?
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.
bloc_concurrency lets you choose.
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.
stream_transformEach 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.
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.
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.
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.
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.
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.
hydrated_bloc adds a SSD layer: every state change is auto-saved to disk and silently
restored the next time the Bloc wakes up.
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.
HydratedBloc / HydratedCubit, implement 2
methodsOverride
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.
storagePrefix in production — or lose your dataBy 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.
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();
}
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.
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.
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.
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.
But what happens when you lift it too high? 🏗️
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.
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.
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.
// ❌ 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 |
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.
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?
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.
You didn't write it — you just forgot a Key. 🔑
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.
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.
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.
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.
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.
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.
ValueKey.
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.
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.
Exception Lifecycle
Your Bloc should never use try/catch — here's why, and where exceptions actually belong in your architecture.
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.
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.
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.
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; }
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')); } }
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
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.
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.
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.
Three fingers? Three times faster. 🖐️
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.
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.
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.
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.
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.
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.
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.
Picking the wrong one costs you days of pain. 🎭
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.
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.
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.
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.
Manual instantiation creates tight coupling.
Manual instantiation makes services harder to test.
final AuthService authService;
LoginViewModel(
this.authService,
);
}
// Usage
final viewModel = LoginViewModel(
AuthService(ApiService()),
);
MultiProvider(
providers: [
Provider(
create: (_) => ApiService(),
),
Provider(
create: (context) => AuthService(
context.read<ApiService>(),
),
),
],
child: MyApp(),
);
final getIt = GetIt.instance;
void setup() {
getIt.registerLazySingleton<ApiService>(
() => ApiService(),
);
getIt.registerLazySingleton<AuthService>(
() => AuthService(
getIt<ApiService>(),
),
);
}
// Usage
final auth = getIt<AuthService>();
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.
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.
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.
Il est toujours en train de tourner. 💀
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.
dispose() est le seul vrai off switch.Appeler
_controller.dispose()
déclenche une chaîne :
AnimationController → Ticker → SchedulerBinding.
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.
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().
dispose() est le seul vrai off switch pour tout ce qui tick, écoute, ou respire dans ton
State.
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.
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.
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.
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.
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.
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.
runtimeType and Key
both match the existing one — control those two, and you control whether State lives or dies.
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.
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?
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.
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.
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.
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.
// Both classes exist // in your source. class PdfExporter { void export() { /*...*/ } void compress() { /*...*/ } void encrypt() { /*...*/ } } class CsvExporter { void export() { /*...*/ } }
void main() { // Only CsvExporter // is reachable from // the call graph. CsvExporter().export(); // PdfExporter has // zero references → // absent from binary. }
$ 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)
flutter build --release. Write clean, explicit Dart and the compiler will quietly discard
everything your users never need.
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.
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?
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.
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.
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.
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.
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.
on a recipe app
management factions
that ended it all
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.
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?
Skia vs Impeller
Flutter owned its renderer from day one — so why did Skia eventually become the bottleneck, and what makes Impeller the future?
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.
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.
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.
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
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.
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.
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.
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?
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?
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.
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.
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.
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.
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?
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?
--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.
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.
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.
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.
--obfuscate hides symbol names, not data. Design
your architecture so that the APK contains nothing an attacker could use alone.
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.
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?
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.
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.
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.
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.
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.
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?
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.
Exactly what happens between that line and the pixel? 🖥
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.
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).
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.
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?
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.
So why does parsing JSON still freeze the UI? 🧊
jsonDecode() synchronously, you're blocking the main isolate. No frames get painted. Your
user sees a frozen screen.
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.
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.
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.
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.
You can't send your
BuildContext, a StatefulWidget
or a database connection across isolate boundaries. Send raw maps, lists, strings, or primitive
types.
compute() gets you there in one line.
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.
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?
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.
(JIT & AOT) to run your code?
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."
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."
🛠️ JIT (Debug) → developer speed + hot reload
🚀 AOT (Release) → user speed + smooth UX
Flutter optimizes for building fast AND running fast.
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.
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.
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.
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.
Literal strings are stored a single time in memory. Multiple occurrences reference the same pooled object, reducing duplication and improving cache efficiency.
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.
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.
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.
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?
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.
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.
A JavaScript runtime is loaded inside the target process. The script can hook native functions, inspect parameters, modify inputs, or change return values dynamically.
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.
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.
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.
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.
Hall of Fame
Every contribution made this series possible. Here's to the team that showed up.
Amine Brahmi
9 eps
Habib Soula
1 ep
Rania Gahbiche
1 ep
Ala Makhlouf
1 ep
Mahdi Menaa
1 ep
Skander Mallek
showed up ✦
Ramadan Kareem. Until next year.
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.
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.
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:
🎯 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
Topic Ideas (Choose Your Own!)
Here are some areas to explore — pick anything that interests you, from Mobile to AI:
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!
Example: Why doesn't constant rebuilding kill performance?
[Explanation with technical details. Use
code for APIs and class
names.]
[Continue explaining the internals step-by-step]
[Wrap up the explanation with the final piece]
Content Guidelines
✅ What Makes a Great Episode
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!