Flutter is Google's UI toolkit for building mobile, web and desktop apps from one Dart codebase. Unlike React Native it does not wrap native controls; it draws every pixel itself with its own rendering engine.
Own renderer: Impeller on iOS and Android (CanvasKit or skwasm on the web) paints the UI, so it looks the same on every device and OS version
Dart: JIT in debug for hot reload, AOT-compiled to native code in release
vs the others: React Native renders real platform views and drives them from JavaScript; .NET MAUI also maps to native controls; Kotlin Multiplatform shares logic and, with Compose Multiplatform, optionally the UI
Strengths: one UI codebase, pixel-identical design, fast iteration, smooth 60/120 fps
Limitations: a larger app (the engine adds a few MB), native look and new OS widgets must be reproduced or wrapped, platform features need plugins or platform channels, and the web build is weaker on SEO and first load
FlutterJunior
Flutter Fundamentals
Widget tree, StatelessWidget, StatefulWidget, BuildContext
Questions closed
0 / 24
- 0Review
- 15Asked often
Questions
Dart runs both JIT (in debug, which gives sub-second hot reload) and AOT (in release, which gives fast startup and predictable speed with no interpreter). Google also owns the language, so it evolves together with the framework.
GC tuned for UI: a generational garbage collector cheaply cleans up the many short-lived widget objects every frame creates
Isolates: no shared-memory threads, so no locks around UI state; heavy work goes to another isolate
Sound null safety: the compiler guarantees non-nullable types, so you do not need null checks everywhere
Why not JavaScript or Kotlin: JavaScript is dynamically typed, cannot be compiled ahead of time to fast native code, and iOS forbids a JIT in release; Kotlin is less suited here, as it is tied to the JVM and Android tooling, while Dart was built for UI and ships its own runtime on every platform
Familiar syntax: reads like Java, Kotlin or TypeScript, so it is quick to learn
The framework (Dart) holds widgets, layout, gestures and animation; the engine (C++) runs the Dart runtime and renders frames with Impeller; the embedder is the per-platform host that gives the engine a surface, threads, input and lifecycle events.
Framework (Dart): widgets, rendering (RenderObject, layout), gestures, animation, Material and Cupertino; ordinary Dart you can read and step into in the debugger
Engine (C++): hosts the Dart runtime, text layout and image decoding, and rasterizes the layer tree with Impeller (CanvasKit or skwasm on the web)
Embedder: FlutterActivity on Android, FlutterViewController on iOS, native hosts on desktop; it supplies the surface, input and lifecycle, and is where plugins and platform channels attach
In imperative UI you change existing views step by step (label.setText(...)); in declarative UI you describe what the screen should look like for the current state and the framework works out the changes. In Flutter, build() returns that description: UI = f(state).
How an update happens: you change state with setState, Flutter calls build again, compares the new widgets with the old ones and updates only what changed
Why it helps: no forgotten branch where one view still shows stale data; the screen is always derived from state
The price: build runs often, so it has to be cheap and free of side effects
Same idea elsewhere: Jetpack Compose, SwiftUI and React work the same way
Every piece of UI (a button, but also padding, alignment, a theme or a gesture handler) is a widget: an immutable description of part of the screen, and the app is a tree of them. Flutter favours composition: each widget does one job, and you get the layout by nesting them.
One Padding class exists instead of a padding argument on several hundred widgets, and any combination works (Padding around Opacity around Align) without the framework anticipating it.
Cheap: a widget is a description, not a view; the RenderObject behind it is created once and reused
The cost: deep nesting; the cure is extracting named widget classes, not fighting indentation
Widget or parameter? If it wraps a child it is a widget; if it changes how one widget paints itself (like TextStyle) it is a parameter
StatelessWidget has no data of its own and redraws only when the parent gives it new values. StatefulWidget holds data that changes and redraws itself when you call setState.
A StatefulWidget is two objects: the widget is throwaway configuration, the State survives every rebuild and holds the real data. That is why a counter keeps its value while the widget around it is recreated.
Stateless: an icon, a label, a card built only from constructor arguments
Stateful: a counter, a text field, an animation, data loaded from the network
Rule of thumb: start stateless, convert only when something must change from inside
Why not stateful everywhere: each one adds a long-lived State object, lifecycle methods to get right and a copy of data that can drift from its source; a stateless widget is just a function of its inputs, easier to test and reuse
BuildContext is a widget's position in the tree; in fact it is that widget's Element. Every .of(context) call uses it to search upwards for something an ancestor provides: a theme, a Navigator, a Scaffold, a provider.
What it gives you: Theme.of(context), Navigator.of(context), MediaQuery.sizeOf(context), ScaffoldMessenger.of(context), a provider's value
Lookups only go up: a context cannot see what is below it; hence "Navigator operation requested with a context that does not include a Navigator" and "Scaffold.of() called with a context that does not contain a Scaffold"
Each widget has its own: the context in build belongs to that widget, not to the children it returns
The fix: move the code that needs the ancestor into its own widget or a Builder, so its context sits below it
**After an await**: check context.mounted (or mounted in a State) before using it; the widget may already be gone
createState → initState → didChangeDependencies → build, then deactivate → dispose. In between, build reruns after every setState, and didUpdateWidget fires when the parent passes new props.
**initState**: once, when the State joins the tree: create controllers and subscriptions; context is not yet safe for .of(context)
**didChangeDependencies**: right after initState, and again when an inherited value (theme, locale, a provider) changes
**build**: returns the widget tree; runs on setState, on a dependency change and when the parent rebuilds
**didUpdateWidget(old): the parent passed new props; compare old and new here, e.g. refetch when the id changed
dispose**: gone for good: cancel subscriptions, dispose controllers, call super.dispose() last
setState is not a stage here: it marks the State dirty so build runs next frame.
main() is the Dart entry point the engine calls first; runApp() takes your root widget, attaches it to the screen and starts producing frames. Code before runApp runs while no Flutter UI exists yet.
**runApp: creates the binding if needed, makes the root widget fill the screen and schedules the first frame
Work before runApp: loading config, initializing Firebase or a database, installing error handlers
ensureInitialized()**: call it first when main uses plugins or platform channels before runApp (Firebase, SharedPreferences, locking orientation); otherwise you get "Binding has not yet been initialized"
Keep it short: everything awaited in main delays the first frame, and meanwhile the user sees only the native splash
MaterialApp sits once at the root and supplies what the whole app needs; Scaffold lays out one screen. Without MaterialApp you get errors like "Navigator operation requested with a context that does not include a Navigator" or "No MaterialLocalizations found".
**MaterialApp gives**: the Navigator that makes Navigator.push work, the themes Theme.of(context) reads, localizations and text direction, and a ScaffoldMessenger so a SnackBar survives a route change
**Scaffold gives**: appBar, body, floatingActionButton, drawer, bottomNavigationBar, and resizeToAvoidBottomInset, which shrinks the body when the keyboard opens
Placement: one MaterialApp near the root, one Scaffold per screen, never one per widget
Hot reload injects the changed code and keeps your app's state; hot restart reloads the code and re-runs main() with all state lost; a full restart stops the app and rebuilds and reinstalls it.
**Hot reload (r): milliseconds; the widget tree is rebuilt and you stay on the same screen with the same data
Hot restart (R)**: a second or two; the app starts from main() with empty state, needed after changing main(), initState, a global initializer or a class's fields
Full restart: needed after native code, adding or removing a plugin, or editing assets and build config in pubspec.yaml
The limit: reload cannot re-run startup code, so values set in main() or in a global stay old
pubspec.yaml is the project manifest: name, version, SDK constraint, dependencies, dev dependencies, and the assets and fonts to bundle. flutter pub get resolves it and records the exact chosen versions in pubspec.lock.
**dependencies vs dev_dependencies**: the first ship with the app; the second (tests, lints, build_runner) are only for development
**^5.9.0**: caret syntax: >=5.9.0 <6.0.0, so compatible updates come in and breaking ones do not; for 0.x versions ^0.20.2 stops before 0.21.0
**version: 1.4.0+12**: before + is the version users see, after it the build number (versionCode on Android, CFBundleVersion on iOS)
**pubspec.lock: commit it for apps, so every machine and CI build gets the same versions
flutter: section**: assets, fonts, uses-material-design, and generate: true for localization
Platform comes from dart:io, which has no web implementation, so reading Platform.isIOS throws UnsupportedError in a browser. Use kIsWeb and defaultTargetPlatform from package:flutter/foundation.dart.
**Check kIsWeb first**: on the web defaultTargetPlatform reports the underlying OS, so a browser on an iPhone answers iOS: right for Cupertino styling, wrong for native APIs
**defaultTargetPlatform**: the TargetPlatform enum, safe to read everywhere including web
UI choices: use Theme.of(context).platform: the same value, but overridable, so a test or a preview can pin a platform
The trap: kIsWeb is a compile-time constant, so if (kIsWeb) branches are tree-shaken; dart:io itself still compiles for JS but throws when used, and platform-specific implementations should be split with a conditional import
Debug with IDE breakpoints, Flutter DevTools and logs. print writes straight to stdout, debugPrint throttles long output so Android's logcat does not drop lines, and log from dart:developer sends structured entries to the DevTools Logging view.
Breakpoints: in VS Code or Android Studio, or debugger() from dart:developer in code
Widget Inspector: pick a widget on the device and see its place in the tree, its constraints and size; the quickest way to understand an overflow
DevTools tabs: Performance for jank, Memory for leaks, Network for HTTP calls
Layout helpers: debugPaintSizeEnabled = true outlines every box; yellow-and-black stripes mark an overflow
Logs in release: print and debugPrint still run in release builds; guard them with kDebugMode so tokens and personal data do not end up in device logs
Debug runs your Dart code on a VM with a JIT compiler, so hot reload works but the app is slow and asserts are on. Profile and release are AOT-compiled to native machine code: release is what ships, profile adds just enough tracing to measure performance.
JIT vs AOT: JIT lets changed code be swapped in while running; AOT compiles ahead of time and cannot change at runtime
Debug: JIT, assert on, DevTools and service extensions attached; never judge performance here
Profile: AOT plus timeline tracing for DevTools; real devices only, not simulators or emulators; use it to measure jank and startup
Release: AOT with asserts and debug tooling stripped; smallest and fastest, the build that goes to the stores
Bugs only in release: asserts vanish, R8 shrinking can strip classes a plugin reflects on, timing differs; always try the release build before shipping
In code: kDebugMode, kProfileMode and kReleaseMode from package:flutter/foundation.dart
build() must be a pure function of the widget's inputs: Flutter calls it again on every change, sometimes many times a second. Anything with a side effect belongs outside it: a one-off load in initState, a user action in a callback.
build runs when: setState fires, the parent rebuilds, an inherited value changes (theme, rotation, keyboard insets, a provider), an animation drives a frame, or you hot reload
**Never in build**: API calls, setState, navigation, dialogs, timers, or creating a controller: a TextEditingController made in build is recreated on every build and leaks the old one
Where it goes instead: initState for one-off loads (keep the Future in a field), didUpdateWidget when a prop changed, callbacks like onPressed for user actions
Common mistake: FutureBuilder(future: fetch()) builds the future inline, so every parent rebuild restarts the request and flashes the loading state
Prefer a separate widget class. A helper method builds into the parent's own element, so its output is rebuilt every time the parent rebuilds and can never be skipped.
Own rebuild scope: a widget class has its own Element; a StatefulWidget calling setState rebuilds only itself, and a const instance is skipped when the parent rebuilds
**Can be const: Flutter compares the two instances, sees they are identical and skips the whole subtree
Its own context**: a class gets a BuildContext below the parent, so lookups resolve against what the parent built (the Scaffold.of error comes from using the context of the build that creates the Scaffold)
When a helper is fine: a small one-off fragment that always changes together with the parent; as soon as it takes arguments or lands in a list, make it a widget
The widget object holding that field is thrown away and rebuilt every time the parent rebuilds. Only the State survives, so data that changes has to live there.
On a rebuild the parent creates a brand-new Counter(); Flutter keeps the existing State and just repoints widget at the new instance. Your field went with the old object.
Widget fields: final configuration from the parent, read as widget.x
State fields: everything that changes while the widget lives
Why it looks random: it resets whenever an ancestor rebuilds (a parent setState, a theme or MediaQuery change, hot reload); a widget whose parents never rebuild seems fine in a demo
The analyzer warns you: a non-final widget field trips must_be_immutable
initState runs before the widget is fully in the tree, so its context cannot read inherited widgets yet, and showDialog needs them (localizations, theme, the Navigator). Schedule the dialog for after the first frame with WidgetsBinding.instance.addPostFrameCallback.
The error: in debug: "dependOnInheritedWidgetOfExactType<…>() or dependOnInheritedElement() was called before _WelcomeState.initState() completed"; inherited lookups are allowed from didChangeDependencies on
The fix: addPostFrameCallback((_) { ... }) runs once, right after the next frame is drawn, when the tree is complete
**Check mounted: the user may already have left the screen when the callback fires
Why not build: it runs on every rebuild, so the dialog would open again and again
Other uses**: reading a widget's size from its RenderBox, scrolling to an item, focusing a field after the layout
Without keys Flutter matches new widgets to existing elements by type and position. After A is removed, B lands in slot 0 and reuses A's State, C reuses B's ticked State. Give each row a stable key: CheckTile(key: ValueKey(item), label: item).
Why only stateful rows break: a stateless row is rebuilt from its new arguments; the bug appears when data lives in a State (a checkbox, a text field, an animation)
**ValueKey(id): the default for list rows; use a real id, never the index, which shifts exactly like the position does
ObjectKey(obj): matches by object identity when there is no single id field
UniqueKey(): never matches, so the widget gets fresh state on every build; use it only to reset on purpose
GlobalKey**: unique across the app and gives access to the State (FormState); costly, not for list rows
initState runs once per State, so _user is loaded only for the first userId. When the parent rebuilds UserDetails with a new id, Flutter keeps the same State and its old Future, so the pane keeps showing the previous user.
**Why the State survives**: the same widget type sits at the same place in the tree, so the element is updated, not recreated; only widget is repointed at the new instance
The fix: override didUpdateWidget(oldWidget), compare oldWidget.userId with widget.userId and start a new load; no setState is needed, because build always runs right after didUpdateWidget
Alternative: key the pane by the id: UserDetails(key: ValueKey(userId), …) makes Flutter create a fresh State, which also resets scroll position and form fields
Catch it next time: a widget test that pumps the screen with id 1, then with 2, and expects the second name; every value copied from widget in initState is a suspect
After the await the screen may already be closed: its State is unmounted, context is gone, and ScaffoldMessenger.of(context) throws. Check if (!mounted) return; right after the await, before you touch context or call setState.
What you see: in debug the assertion "This widget has been unmounted, so the State no longer has a context"; in release a "Null check operator used on a null value" from _save, reported as an unhandled async error
Why: an await is a gap in which the user can press Back or switch tabs; dispose runs and the element leaves the tree, but the Future still resumes your method
How the fix works: mounted is false once the State is disposed; the save itself still completes, only the UI follow-up is skipped. In a helper that takes a BuildContext, check context.mounted
**Capturing ScaffoldMessenger.of(context) before the await** is a valid way to still show the snackbar, but pop() must stay behind the mounted check: a captured Navigator.pop() after the user already left closes the screen underneath
Catch it next time: the use_build_context_synchronously lint from flutter_lints flags exactly these lines; a widget test that pops the route before completing the Future reproduces the crash
Mix WidgetsBindingObserver into your State and Flutter calls didChangeAppLifecycleState whenever the app moves between foreground and background. Register in initState with WidgetsBinding.instance.addObserver(this), remove it in dispose.
The states: resumed (visible, in use), inactive (visible but unfocused: a call, the app switcher), hidden, paused (backgrounded, not drawing), detached
What to do there: pause video, animations, sensors and polling timers; restart them on resume
Save while you can: paused is the last guaranteed moment: flush a draft or an analytics batch, because Android may kill the process from there with no further callback
Common mistake: treating inactive as "the user left"; it fires for every system dialog
Weigh the team, the existing code, how custom the UI is and how much of the product lives in platform features; raw speed rarely decides it. Flutter fits one team with one branded UI, KMP fits shared logic under native UI, and fully native fits a product built on platform features.
What decides it
The team: one Flutter team ships both platforms; Kotlin Android engineers pick up KMP fast, but its iOS side still needs iOS engineers; native means two teams building every feature
Existing code: a new product is quickest in Flutter; an existing native app adopts KMP one module at a time, while Flutter enters through add-to-app with a whole engine
The UI: one custom design everywhere suits Flutter or Compose Multiplatform, which both draw their own pixels on iOS; a look that follows each OS release, such as iOS 26's Liquid Glass, comes free only with native UI
Platform surface: home screen widgets, watch apps, Live Activities and share extensions are native code in every option, so the more the product lives there, the less a shared UI saves
Maturity: Flutter has been stable since 2018 and has the larger UI ecosystem; Compose Multiplatform for iOS went stable in May 2025, so expect more gaps
What each choice costs
Flutter: a few MB of engine, platform channels or Pigeon where plugins stop, a native look reproduced rather than inherited
KMP: Swift calls Kotlin through generated Objective-C headers while Swift export matures, iOS builds get slower, and the iOS team must accept Kotlin in its repository
Native: every feature built and tested twice, and two apps that drift apart in behaviour and release dates
How to answer
One recommendation with its assumption: "five engineers and a custom-branded consumer app: Flutter", what would change it, and a one- or two-week spike of the riskiest screen before the company commits