Everything the adaptive question bank can teach and test in Flutter, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
The difference is when the value is known. A runtime-assigned binding is fine for anything computed from input; a compile-time one can be canonicalised, which is what lets the framework recognize the identical instance across rebuilds and skip work. Trying to mark something compile-time whose value depends on a variable is a compile error, not a slow path.
The guarantee is enforced by the compiler before the program runs, so there is no check at runtime to skip or fail. What that buys is that an unexpected null becomes a compile error at the point of the mistake rather than an exception somewhere downstream — and it is why adding a question mark to silence an error usually just moves the problem.
The short-circuiting one abandons the whole chain, not just the step it is attached to, so a long chain returns null rather than throwing part-way. The fallback operator only fires on null and not on empty or zero, which is the usual surprise: an empty string is a perfectly good value and will not trigger it.
Deferring the initializer trades a compile-time guarantee for a runtime one — you are promising to assign before any read, and the error arrives only if you break that promise. It is genuinely useful where a value needs the widget's context and so cannot be set at construction; it is a hazard anywhere the assignment sits behind a branch.
Promotion works on locals because nothing else can change them between the check and the use. A public field or a getter could return something different on the second call, so the compiler refuses to narrow it — the fix is to copy the value into a local first and test that, which is what makes the narrowing sound.
Every named argument is optional by default, which is why a widget constructor takes a long list and callers supply only what they need. Marking one required restores the compile-time check. This is the mechanism behind the readable widget call sites — arguments name themselves at the point of use rather than depending on position.
Because there is no overloading, a class needing several ways to be built exposes them as names — and the factory form additionally allows returning something other than a fresh instance: a cached object, a subtype, or the result of parsing. That is the seam where a constructor stops being a guarantee of newness.
By default two structurally identical objects are unequal, so they behave as distinct keys in a set or a map and comparisons in tests fail confusingly. Overriding just one of the pair is worse than neither: equal objects with different hashes break the containers that rely on the contract holding between them.
Inlining conditionals and expansion into the literal keeps a children list declarative, so there is no helper method building a list off to the side that a reader has to go and find. The null-aware form of the spread is what makes an optional section a single line instead of a branch.
B. The async model: futures, streams and isolates
Everything runs on one thread: synchronous code first, then the microtask queue drained to empty, then a single event. Rendering a frame is one of those events, so anything that keeps the loop busy postpones it — and microtasks are drained before events, which means a self-scheduling microtask can starve rendering entirely.
The body starts executing immediately and only suspends at the first suspension point, which is why work placed before it happens during the caller's turn. That ordering catches people out in initialization code: two calls issued back to back may already have run half their bodies before either returns.
Three things go wrong at once and none of them raises an error: the value is the pending operation rather than its result, the code after it runs too early, and a failure inside it escapes the surrounding try. The last is the expensive one — the error surfaces later, detached from the call that caused it.
Independent requests issued together overlap, so the total is roughly the slowest rather than the sum. Awaiting each in turn serializes work that had no reason to be ordered. The caveat is failure: by default one rejection surfaces immediately while the others continue unobserved, so error handling needs deciding explicitly.
A try block only covers what it actually awaits, so a call left unawaited inside it is outside the guard entirely — the failure escapes to the zone's unhandled handler, typically as a console message with no stack that points at your code. Either await it or attach a handler at the point it is started.
The single-listener kind rejects a second subscription for its whole lifetime, which is the error people meet when a widget rebuilds and subscribes again. Converting to the fan-out kind fixes the symptom and changes the semantics: a late subscriber misses everything emitted before it arrived.
A generator is lazy — nothing in the body runs until something subscribes, and it pauses between emissions while the consumer is busy. That laziness is the feature and the confusion: a generator nobody listens to performs no work at all, which looks exactly like a bug in the generator.
Separate heaps mean messages are copied rather than shared, so there is no shared mutable state and no locking. It also means a closure, an open connection or a platform handle cannot be sent across — the boundary carries data, and anything that is not plain data has to be recreated on the other side.
Moving a CPU-bound function to another isolate keeps the UI loop free to render, which is the difference between a progress indicator that spins and one that freezes. The cost is the copy in and out, so it pays for genuinely expensive work and loses on small jobs where serialization dominates.
Suspending returns control to the loop, so other events can run — but a long synchronous stretch inside an async function never suspends and blocks exactly as it would anywhere else. Sprinkling suspension points through a tight loop is not the fix either; the work belongs on another isolate.
C. Widgets and the three trees
Because a widget only describes what should exist, constructing thousands of them per frame is cheap — they are throwaway values, not objects with lifetimes. That is what makes rebuilding a subtree an acceptable response to a change, and why performance work targets layout and paint rather than construction.
Separating configuration, identity and rendering is what allows widgets to be discarded every frame while state and layout survive. Almost every confusing behavior in the framework — why state persisted, why it did not, why a key mattered — is explained by which of the three a thing belongs to.
The element is the long-lived thing: it holds the position in the tree and owns the State object, while each rebuild hands it a fresh description to compare against the old one. So state outliving a rebuild is not a special case, it is the normal arrangement — losing it means the element was replaced.
Only a small number of widgets create one; most compose others and contribute nothing to layout themselves. Knowing which is which explains why wrapping something in another widget is usually free, and why a custom layout means dropping to this level rather than nesting more of the existing pieces.
The distinction is whether anything needs to survive a rebuild. A widget rendering purely from its arguments is simpler, cheaper to reason about, and cannot go stale. Reaching for the stateful form by default is a common habit that makes every one of those properties harder to rely on.
Behavior is added by wrapping rather than subclassing, which is why padding, alignment, and constraints are all widgets in their own right rather than properties on everything. The trade is deep trees, and it is why reading a build method means reading it as a chain of transformations.
It can be called many times per second and at times you did not trigger, so starting a request, mutating state or showing a dialog from inside it produces duplicated work and errors that look random. Anything with an effect belongs in a lifecycle method or a callback, not in the description of the UI.
A compile-time widget is canonicalised, so the same instance is handed back on every rebuild — and since the framework compares identity first, it can skip that subtree entirely. It is the cheapest optimization available, and it is lost the moment any argument depends on a runtime value.
D. StatefulWidget and the State lifecycle
It runs once per element, not once per widget, which is the whole point: the returned object outlives every widget instance the parent later produces. Putting initialization logic here rather than in the first-build hook is a common slip, because this method runs before the object is attached to anything.
It runs before the first build and before the element is fully wired to its ancestors, so an inherited lookup here either throws or returns something stale. That is the one restriction that catches everyone — anything depending on theme, media queries or a provider belongs in the dependency hook or later.
It fires immediately after first-time initialization and again every time an inherited widget this State subscribed to changes, which makes it the right place for work that depends on those values. It can therefore run many times, so anything expensive here needs a guard comparing against the previous value.
It fires when the parent rebuilds with a new description of the same type and key, which is the only signal that the incoming arguments changed. Reacting unconditionally is the trap — the method runs on every parent rebuild, so the comparison against the previous widget is what keeps the work proportional.
Anything with a lifetime of its own — controllers, subscriptions, tickers, timers — leaks unless released here, and the leak is silent until a callback fires against a torn-down tree. Note that removal happens first and can be reversed if the subtree is reinserted elsewhere, so final teardown is a separate step.
It schedules a rebuild for the next frame rather than rebuilding immediately, so reading state right after calling it still sees the old frame's tree. Its callback is synchronous by contract: making it asynchronous means the framework marks the element dirty before the mutation has happened.
An awaited operation can complete after the element is gone, and calling into a disposed State throws. The check is cheap and belongs immediately after every suspension point in a callback, not once at the top — the state can change during any of the awaits, not only the first.
The framework replaces the reference on every rebuild, so reading through it always gives current values while copying those values into fields during initialization freezes them at their first value. That is the source of the classic bug where a screen keeps showing the arguments it was first given.
Deferring until after layout is the only way to act on measured sizes, positions or scroll extents, none of which exist while the tree is being described. It is also the escape hatch for anything that must not run during a build — showing a dialog, or marking something dirty that is already being built.
E. Keys and element reuse
Reuse is decided by comparing type and key against the previous description at the same position; matching means the element and its state carry over, and a mismatch tears everything below it down. Every question about why state survived or vanished reduces to what this comparison returned.
Without an identifier the comparison is positional, so reordering or deleting leaves each element matched to whatever now occupies its slot — and since state lives on the element, the checkbox ticks, animation progress and scroll offsets stay behind while the data moves. The data looks right and the state is wrong.
Identity is compared at a position in the tree, so the key has to sit on the outermost widget of the item's subtree. Placed on something inside, the wrapper is still matched positionally and torn down, taking the keyed child with it — the key ends up describing a subtree that was already replaced.
One compares by equality, so it is right when items have a stable identifier such as a database id; the other compares by identity, which suits objects that are mutated in place and have no meaningful equality. Choosing the equality form for a value class with no equality override quietly degrades to identity.
Constructing a fresh unique key during a build guarantees it differs from last frame's, so the comparison always fails and the subtree is rebuilt from scratch — state lost, animations restarted, focus dropped. It is occasionally what you want, deliberately, to force a remount; as a habit it defeats the mechanism.
It provides access to a State from outside its subtree and allows moving a subtree between parents without losing it, which nothing else does. The costs are real: uniqueness across the whole tree is required, duplication throws, and the lookup is heavier than the ordinary local comparison.
F. BuildContext and the inherited lookup
It is a handle on one specific position in the tree, which is why two contexts are not interchangeable and why holding one past its lifetime is a bug. Thinking of it as an application-wide object is what produces lookups that fail for no visible reason — the question is always where in the tree it points.
The search goes from that position toward the root and stops at the first match, so nothing below and nothing sideways is reachable. That single fact explains most lookup failures: the thing being looked for is a sibling, or it was provided by the same widget doing the looking rather than by an ancestor.
The context inside a build method refers to the widget being built, which sits above anything that method returns — so looking up something the method itself provides walks past it. Interposing a builder creates a position below, which is why that one-line wrapper fixes a lookup that otherwise cannot succeed.
One registers a dependency so the element rebuilds when the value changes; the other reads without subscribing. Using the subscribing form inside a callback either fails or creates a dependency nobody intended, and using the non-subscribing form in a build method produces a value that never updates.
Between issuing an asynchronous call and its completion the user may have navigated away, so the element is gone and using its context throws. The check has to come after the await and before the use — and after each one, since a sequence of awaits gives several opportunities for the tree to change.
G. Constraints go down, sizes go up
Three steps in a fixed order — constraints down, size chosen within them, position assigned by the parent — and almost every layout surprise is one of the three not being what you assumed. A child cannot know its position, and a parent cannot learn a size it did not allow, which is why some arrangements are impossible.
A requested size is a preference expressed within the allowed range, not an instruction, so under a tight constraint it is ignored entirely and the widget renders at the imposed size. The fix is never a bigger number on the child — it is changing what the parent passes down.
Tight means the minimum equals the maximum, so there is exactly one legal size and the child has no say. Loose means a minimum of zero, so the child may be as small as it likes. Most layout work is arranging for the right one to arrive, which is what the alignment and sizing widgets actually do.
Alignment widgets pass a loosened version of the constraints they received, so the child is free to take its natural size and is then positioned within the leftover space. That is why wrapping something in an alignment widget makes a stretched child suddenly shrink to fit — the constraint changed, not the child.
An infinite maximum means there is no size to fill, so a widget that tries to expand into it has no finite answer and throws rather than guessing. It arises from scrollables and from flex children without a bound — the remedy is to supply a finite constraint, not to make the child smaller.
With no child it expands to fill what it is given; with a child it shrinks to that child. The behavior changes depending on what is inside it, which is why adding a child to a previously full-width box makes it collapse — and why an explicit size or alignment is the way to stop the rule applying.
It defers building until the incoming constraints are known, so its subtree can adapt to the space available. What it cannot do is influence that space: the constraints arrive from above, so it can respond to its parent and never negotiate with it — which is the difference from a widget that sizes to its child.
Asking a subtree how large it would like to be requires laying it out speculatively, which is documented as costly and grows worse with depth. Occasionally there is no alternative, but a layout reaching for it often has a simpler formulation — a fixed size, a flex share, or restructuring so the question is not asked.
H. The layout widgets
Which axis is which flips with the flex direction, so the same alignment value means opposite things in a row and a column — the commonest source of a widget being centerd in the direction you did not intend. Naming the axis rather than thinking in horizontal and vertical is what makes the code readable.
By default the flex claims all the space available along its direction, so a column inside a dialog stretches to the full height and the content sits at the top. Switching to the minimum setting makes it wrap its children — but only within a bounded parent, since minimum of an unbounded axis is still a problem.
A column hands its non-flexible children an unbounded height, because it is laying them out one after another with no ceiling for any individual one. A scrollable given that constraint has no viewport to size and throws — which is why the fix is a flexible wrapper or an explicit height rather than a property on the list.
Both claim a share of the free space; one forces the child to occupy all of its share and the other allows it to be smaller. Using the forcing variant around content that wants its natural size stretches it, and using the permissive one where you wanted a filled cell leaves a gap — the same share either way.
Inflexible children are measured first and the flex values only divide what is left, so the ratio describes the remainder rather than the whole. Adding a fixed-size sibling therefore changes every flexible child's actual size without any of their numbers changing, which makes the proportions look wrong for no visible reason.
These widgets attach layout information to a parent that knows how to read it, so they are meaningless anywhere else and the framework throws rather than ignoring them. The error usually means an intermediate wrapper crept in — a padding or a conditional — between the flex and the child that was annotating it.
The stack takes its size from the children that are not positioned, so a stack containing only positioned children collapses and everything disappears. Positioned children are then placed relative to its edges, which means the layout depends on something the stack itself is not sized by.
One imposes an exact size, overriding whatever the child would have chosen; the other only narrows the range and lets the child pick inside it. Reaching for the exact form when you meant an upper bound is what produces a child stretched to fill a space it did not need.
The builder forms create children only as they scroll into view, so a long list costs what is visible rather than what exists. The eager constructor builds everything up front, which is fine for a handful of items and becomes a startup freeze at a few hundred — with no error to suggest which one you chose.
I. Interactivity, gestures and forms
It recognizes gestures and paints nothing, so an area with no painted child has nothing to hit-test against and taps land on whatever is behind it. Setting the behavior to treat its whole bounds as opaque is what makes an empty region tappable — a transparent gap is not a target by default.
The ripple is painted onto an ancestor material surface rather than by the widget itself, so with none above it the touch is registered and nothing appears. The tell is a control that works but feels dead — and the fix is providing that ancestor rather than reaching for a different gesture widget.
Competing recognizers enter an arena and exactly one wins, which is why nesting a tap inside a drag makes one of them stop firing. The resolution depends on which recognizer claims victory first, so the fix is usually restructuring the nesting or using a recognizer that declares its intent differently.
The controller holds the text, the selection and the composing range, so it is the source of truth rather than the field. It also carries listeners and must be released with the State — an undisposed controller is one of the easiest leaks to create and one of the hardest to notice.
The form coordinates its descendant fields: one call runs every validator and returns a single verdict, with each field showing its own message. The key is what provides access to that state from outside the form's own subtree, which is the piece people leave out before discovering validation cannot be triggered.
J. State management
The test is how many widgets need to read it. A checkbox's tick, a page controller's index and an animation's progress concern nobody else, and hoisting them into a shared store adds ceremony and widens every rebuild. The mistake runs both ways — shared data kept locally is what forces prop-drilling through six layers.
Moving shared state to the nearest common ancestor makes one place authoritative, with data flowing down and events flowing back up as callbacks. It is the cheapest arrangement that works, and its limit is distance: once the ancestor is many levels above the consumers, every widget in between is carrying arguments it does not use.
It is the framework's own mechanism for exposing data down a subtree, with lookup proportional to nothing — the element keeps a map — and rebuilds confined to the widgets that actually depended on it. Every state-management package is built on this rather than replacing it.
Dependents rebuild only when the replacing instance says something changed, so a naive implementation returning true always rebuilds everything that depends on it, and one comparing references misses a mutation made in place. It is the single method deciding how much work a data change costs.
It is a minimal observable: listeners register, a call notifies all of them, and nothing is batched or deduplicated. Because listeners hold references, it must be disposed — and because notification is manual, mutating the underlying data without calling it produces a change no listener ever hears.
A single value plus a builder that listens to it rebuilds only the builder's subtree, which is finer-grained than marking a whole State dirty and needs no package at all. It suits exactly one changing value; expressing several of them this way is where a real state solution starts to earn its place.
Subscribing belongs in a build method, where a later change should produce a rebuild. The one-shot read belongs in callbacks and lifecycle methods, where subscribing would either throw or register a dependency from the wrong place. Mixing them up gives a UI that does not update, or one that rebuilds on every unrelated change.
Keeping providers out of the widget tree means a missing dependency is a compile-time error rather than a lookup that throws at runtime, and it makes the same state readable from code that has no context at all. The trade is a second graph to reason about, separate from the one the UI is built from.
Modeling the flow as events in and states out puts every transition in one place that can be tested without a widget, and makes the history of a screen inspectable. The cost is ceremony proportional to the screen — for a form with one toggle it is overhead, and for a complex flow it is what keeps it tractable.
K. Navigation and routing
Pushing covers the current route rather than replacing it, so the one underneath stays alive and keeps its state — which is why returning to it is instant and why a screen left behind can still be holding a subscription. Popping uncovers it again, and popping the last route leaves the navigator empty.
The call completes when the route is popped, carrying whatever value the pop supplied, which makes the round trip to a picker or a confirmation a single await. It completes with null when the user dismisses with the system back gesture, so the null case is the cancellation path and needs handling.
A static map covers the fixed screens; anything needing arguments parsed from the name, or a fallback for an unknown path, goes through the generator instead. Mixing the two is normal — the map is not a complete routing solution and was never meant to handle dynamic paths.
Replacing rather than pushing is what stops a back gesture returning to a login or splash screen that is no longer valid. Removing until a predicate matches is the version for clearing several at once, which is what the end of a multi-step flow needs so the user cannot walk back into a completed wizard.
One mutates a stack with calls; the other derives the whole stack from application state and rebuilds it. The second is what makes deep links, browser history and restoring a session tractable, because any state can be expressed as a stack — the cost is that every navigation becomes a state change rather than a call.
The animation only happens when the identical tag is found on both the outgoing and incoming route, so a mismatch produces an ordinary transition with no error. A duplicate tag within a single route throws instead — which is the case that arises from putting a hero inside a list without varying the tag per item.
L. Animations and transitions
These widgets interpolate whenever they are rebuilt with a different target, so animating is a matter of changing a value and letting the rebuild happen — no controller, no lifecycle, nothing to dispose. That makes them the right default, and the reason to reach past them is coordination rather than complexity.
Each one animates its own properties between the old and new value, so it cannot cross-fade a swapped child, sequence two effects, or be paused and reversed. Those need an explicit controller — the tell is wanting to know where in the animation you are, which the implicit form deliberately does not expose.
It produces a value moving between zero and one over a duration and must be driven by a ticker, which is why it needs a provider mixin on the State. Everything else is mapping: the controller is time, and what that time means visually is decided by the curves and ranges layered on top of it.
Binding the ticker to the widget means it stops when the widget is off-screen, so an animation on an invisible page does not burn frames. The controller still owns resources and must be released on teardown — an undisposed one keeps ticking and eventually produces a callback against a State that no longer exists.
It is a pure mapping from the controller's normalized progress onto real values — colors, offsets, sizes — and holds no state of its own, so the same instance can be reused and the controller remains the single source of progress. Two tweens over one controller is how several properties stay in step.
A curve reshapes how time passes without changing how long it takes, which is what separates a mechanical linear motion from one that feels deliberate. It is applied as a wrapper around the animation rather than as a property of the duration, so the same controller can drive several differently-eased values.
Only the builder function re-runs each tick, so an expensive subtree handed in as the pre-built child is constructed once and reused for the whole animation. Omitting that argument and building the subtree inside the closure is the difference between rebuilding one wrapper per frame and rebuilding everything.
M. Assets, fonts and pubspec
An asset in the repository is invisible at runtime until it is declared, and the failure is a load exception rather than a build error — so it appears when the screen using it opens. Indentation is significant in that file, so a correctly spelled entry at the wrong depth is silently not an asset declaration.
A trailing slash includes the files directly inside that directory and does not recurse, so nested folders each need their own entry. The result is the same silent runtime failure as an undeclared file, and it is easy to miss because the top-level images work while everything one level deeper does not.
Sibling folders named for a scale factor are selected automatically from the device's pixel ratio, so only the base path is referenced in code. The naming and placement are the whole mechanism — a variant in the wrong location is simply not found, and the base image is scaled up instead.
One resolves from the shipped bundle and is effectively instant and infallible; the other is a request that can be slow, fail, or return something unexpected. The second therefore needs a placeholder and an error path, and a screen that looks fine in development is usually one tested on a fast connection.
The family is declared once with a separate asset entry per weight and style, because each file carries one. Omitting a weight does not fail — the platform synthesizes an approximation by shearing or thickening the one it has, which is why a bold that looks subtly wrong usually means its file was never declared.
The caret means compatible-with, and compatibility is judged on the leftmost non-zero number. For a stable package that is the major version, so the range runs up to the next one; below version one it is the minor number instead, which makes the same syntax far stricter than people expect.
N. Platform channels and plugins
Matching is on the string alone, with no compile-time link between the two sides, so a typo or a rename on one side produces a silent mismatch rather than an error. Declaring the name as a shared constant in each codebase is the usual mitigation, since nothing else will tell you they disagree.
Arguments and results are serialized by a standard codec, so only the supported primitive, list and map types cross — a custom object has to be reduced to those first. The call is asynchronous in both directions, which means even a trivial native getter returns a future and cannot be read during a build.
One means your native handler ran and reported a failure; the other means nothing was registered for that channel at all. The second is nearly always a build or registration problem — a fresh install needed, a platform not implemented, or a test environment where no native side exists.
A method channel answers one call at a time, so anything continuous — sensor readings, connectivity changes, download progress — needs the streaming form instead. It is subscribed rather than invoked, and the subscription has to be canceled, which is where the leak lives if the widget forgets to.
The handler runs on the platform's UI thread, so slow native work blocks the platform's own rendering, not just yours. Moving it to a background thread and posting the reply back is the native-side equivalent of keeping work off the UI isolate — and it is a separate discipline from anything Dart does.
Calling native code directly has no serialization and no message hop, so it suits a tight numeric interface into a C library. Channels carry structured messages and are the right shape for platform services and anything asynchronous. The choice is about what is on the other side, not about which is faster.
O. Testing
Logic tests need no framework and run in milliseconds; widget tests mount a tree in a synthetic environment with no real device; integration tests run the whole app for real and are slow enough to be reserved for the paths that matter most. Choosing the wrong level is what makes a suite slow and flaky at the same time.
It mounts the tree and renders a single frame, which is why a widget expecting inherited ancestors fails immediately with an error about a missing directionality or localization. Supplying a minimal application wrapper is the usual fix, and it is worth keeping minimal — a full app in every test is a slow suite.
Each call renders exactly one frame, optionally after advancing a fake clock, so an animation only progresses as far as you ask it to. That determinism is the point: a test can land precisely mid-transition, which is impossible against a real clock and is where interesting assertions live.
It repeats until nothing more is scheduled, which is the convenient default and hangs forever on anything that never settles — a looping indicator, a repeating animation. The failure is a timeout rather than an explanation, so a test that stalls here usually means something on screen never stops animating.
Dispatching an event does not rebuild anything by itself, so asserting straight after a tap tests the frame before it. The missing pump is the single most common cause of a widget test that fails while the app plainly works — and adding one turns it green without anything else changing.
Finders are lazy descriptions rather than snapshots, so the same one re-evaluates against whatever is on screen at each use — which is what allows defining it once and asserting before and after an interaction. It also means a finder that matched a moment ago can legitimately match nothing now.
The count matchers turn a finder into an assertion, and the one asserting nothing matched is the one that catches regressions the positive ones cannot — it fails when a widget that should have gone away is still mounted. Asserting exactly one is also stricter than it looks and catches accidental duplication.
Time, timers, network and platform channels are all synthetic, so nothing real fires and nothing waits. That makes tests fast and deterministic and it means any code path depending on a real platform response has to be faked explicitly — a channel with no registered fake simply returns null.
P. Common errors and debugging
The stripes mean children were laid out beyond the space available along the flex direction, and the render is otherwise correct — it is telling you the arithmetic did not fit. The fix is deciding what should give: a child made flexible, the content made scrollable, or text allowed to wrap or truncate.
A scrollable needs a bounded extent to know how much of its content is visible, and a column hands its children an unbounded one. So the error names the scrollable while the cause is the parent — which is why giving the list a height or a flexible wrapper fixes it and changing the list itself does not.
Marking an element dirty while a build is in progress would invalidate work already done, so the framework refuses. It usually comes from notifying a listener inside a build method or from a synchronous callback during construction, and deferring to a post-frame callback is the standard remedy.
Creating the future inside the build method starts a new one on every rebuild, so the operation reruns, the builder flips back to its loading state, and results can arrive out of order. Creating it once during initialization and holding it in a field is the fix — the builder should receive a future, not make one.
The annotation widget is attached to a parent that does not know how to read it, which means something came between it and the flex or stack it belongs to. The intervening widget is often conditional, so this can appear only in a particular state and look like an intermittent bug.
Two mounted elements claiming the same global key is unresolvable, since the key is meant to identify exactly one, so the framework throws. The usual source is a key held in a field and reused across list items, or a subtree that is momentarily present in two places during a transition.
Debug builds show the red error screen with the exception; release builds substitute a gray box that says nothing, so a build error in production looks like a rendering glitch. Replacing the builder is how a shipped app reports something useful instead — and it is worth doing before the first release, not after.
Q. Performance and the rebuild traps
A rebuild covers everything below the State that was marked, so where state lives determines how much work each change costs. Hoisting a value to a high ancestor for convenience is the usual cause of a screen that rebuilds entirely on every keystroke — the data flow is fine and the scope is wrong.
A compile-time child is literally the same instance each frame, so the framework's identity check short-circuits and the subtree is skipped. It costs nothing and applies widely, which makes it the first thing to reach for — and it is silently lost the moment an argument becomes runtime-dependent.
A subtree that does not depend on the animating value can be built once and passed in, so it is reused on every tick rather than reconstructed. The saving is proportional to how expensive that subtree is, which means the pattern matters most exactly where the animation is otherwise most likely to drop frames.
Constructing widgets is inexpensive by design; what misses the frame budget is layout, painting and rasterization. That reorders the optimization list — reducing rebuilds helps least where the expensive work is a blur, a shadow or a large image, and the profiler is what tells you which regime you are in.
The two are measured separately, and which one is over budget points at a different fix: long build and layout time is a widget-tree problem, long raster time is a painting problem. Reading only the combined frame time is how people spend a day reducing rebuilds on a jank caused by a shadow.
Several innocuous-looking effects force the renderer to allocate an offscreen buffer, composite into it and then draw it back, which is among the most expensive operations available. Knowing which ones do it is what lets you reach for a cheaper equivalent — a pre-faded color rather than a wrapper, or an unclipped shape.
Isolating a frequently repainting subtree stops its neighbors repainting with it, at the cost of an additional layer to composite. That trade can go the wrong way — too many boundaries make compositing the bottleneck — so it is a measured change rather than a default to sprinkle around.
Debug builds run unoptimized with assertions everywhere, so their timings are not merely slower but differently shaped, and a bottleneck measured there may not exist in a release. Profile mode keeps the tracing while compiling like a release, which is the only configuration where the numbers mean anything.
R. Build, release and tooling
The three differ in what is compiled and what is kept: assertions and hot reload in development, tracing without the debugging overhead for measurement, and neither in the shipped build. Confusing them is why a performance complaint about a debug build is not evidence of anything.
Changed code is injected and the tree is rebuilt while state is preserved, so the entry point and initialization hooks do not run again. That is the feature and the trap: a change to code that only executes at startup appears to do nothing, and the state you are looking at predates the edit.
Everything is discarded and the entry point runs again, which is what a change to initialization, to a global, or to anything structural requires. It is slower than a reload and faster than a full rebuild, and reaching for it when a reload behaves oddly is usually quicker than working out why.
Store distribution expects a bundle, from which the store generates per-device packages so the download carries one architecture. A universal package carries them all and is correspondingly larger — useful for sideloading and testing, and the wrong artifact to upload.
A release build must be signed with a key you control and keep, since the development key is rejected on publication. Losing that key means no longer being able to update the listing, which makes its storage a real operational concern rather than a step in a build script.
Renaming symbols makes a stack trace unreadable, so the mapping file emitted alongside becomes the only way to interpret a crash from the field. It has to be archived per release build — without the file matching that exact build, obfuscated reports are permanently undecipherable.
One source tree builds against different identifiers, assets and endpoints, so a staging build can sit on the same device as production without collision. The value is that the difference is declared in the build configuration rather than in a branch or a commented-out constant somebody has to remember to change.
The documented tools are a size analysis to find what actually dominates, tree shaking that drops unused icon glyphs, and deferring components that are not needed at launch. Measuring first is the point — the intuition about what is large is usually wrong, and the analysis takes one command.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.