Top 103 Flutter Interview Questions
Top 100 Flutter interview questions covering Dart, widgets, state management, navigation, animations, and performance optimization.
Flutter uses Dart, a client-optimized programming language developed by Google. Dart supports both JIT (Just-in-Time) compilation during development — enabling Hot Reload — and AOT (Ahead-of-Time) compilation for fast, high-performance production builds. Its syntax is clean and familiar to developers coming from Java, Kotlin, or JavaScript backgrounds, making it easy to pick up quickly.
Flutter was developed by Google and first released as a beta in 2017, with the stable 1.0 release in December 2018. It is open-source and maintained by Google along with a large community of contributors. The framework has grown to support Android, iOS, web, macOS, Windows, and Linux from a single codebase.
In Flutter, everything is a Widget. Widgets are the fundamental building blocks of the UI — they describe what the view should look like given the current configuration and state. From a simple Text label to complex screen layouts, gestures, and animations, everything is a widget. Flutter UIs are built by composing a tree of widgets together, each responsible for a small part of the display.
A StatelessWidget has no mutable state — it renders based solely on the constructor arguments passed to it and never changes after being built. It is ideal for static UI elements like labels or icons. A StatefulWidget maintains a separate State object that can change over time in response to user interactions or data updates, triggering a UI rebuild. When state changes, you call setState() inside the State object to notify Flutter to rebuild the widget.
The pubspec.yaml file is the configuration manifest for a Flutter project. It defines the project name, version, Dart SDK constraints, and all external dependencies (packages) the app needs. It also declares assets such as images and custom fonts. After modifying pubspec.yaml, you run flutter pub get to download and install the declared packages from pub.dev.
Hot Reload is one of Flutter's most powerful development features. It injects updated source code into the running Dart VM and rebuilds the widget tree while preserving the current app state, usually in under one second. This allows developers to immediately see the effect of UI changes without restarting the app or losing their current navigation position or form data. It is triggered by pressing r in the terminal or clicking the Hot Reload button in the IDE.
Hot Restart fully re-runs the Flutter application from scratch. Unlike Hot Reload, it discards the current Dart state, re-initializes all objects and variables, and rebuilds the widget tree from the beginning. It is slower than Hot Reload but necessary when you make changes to initialization logic, global state, or native code. Hot Restart is triggered with R (capital) in the terminal.
Scaffold is a Material Design layout widget that implements the basic visual structure of a screen. It provides named slots for common UI elements: appBar for the top app bar, body for the main content, drawer for a slide-in navigation menu, bottomNavigationBar, floatingActionButton, and more. Using Scaffold ensures your screen follows Material Design guidelines without building every component from scratch.
MaterialApp is the top-level widget that wraps the entire Flutter application with Material Design support. It configures the app-wide theme, navigation routes, locale, and the home screen. It also sets up the Navigator, which manages the stack of screens. Most Flutter apps start with a MaterialApp as the root widget returned by runApp(). For iOS-style apps, the equivalent is CupertinoApp.
runApp() is the starting point of every Flutter application. It takes a root widget and inflates it to fill the entire screen, making it the root of the widget tree. It is called inside the main() function, which is Flutter's entry point. Example: void main() => runApp(MyApp());. The framework then calls build() on the root widget to construct the initial UI.
The Column widget arranges its child widgets vertically in a top-to-bottom sequence. Its main axis is vertical and the cross axis is horizontal. You control alignment using mainAxisAlignment (e.g., center, spaceBetween) and crossAxisAlignment (e.g., start, stretch). By default, Column takes the minimum vertical space needed unless you constrain it. For a horizontal equivalent, use Row.
The Row widget arranges its child widgets horizontally from left to right. Its main axis is horizontal and the cross axis is vertical. Like Column, it supports mainAxisAlignment and crossAxisAlignment for controlling how children are positioned and spaced. If children overflow the screen width, Flutter will show a yellow-striped overflow error — use Flexible or Expanded inside a Row to prevent this.
The Stack widget allows its children to overlap each other, positioned on top of one another like layers. It is used to create overlapping UI elements such as a badge on an icon, a floating label over an image, or a custom overlay. Use the Positioned widget inside a Stack to place children at specific coordinates relative to the stack's boundaries. The last child in the list is drawn on top.
ListView is a scrollable list of widgets arranged linearly. For short, static lists, use ListView(children: [...]). For large or infinite lists, use ListView.builder(), which lazily builds only the widgets currently visible on screen — significantly improving performance and memory usage. ListView.separated() adds a separator widget between items, and ListView.custom() provides full control over the building behavior.
GridView displays a 2D scrollable grid of widgets. GridView.count creates a grid with a fixed number of columns, while GridView.extent creates one where each tile has a maximum width. For dynamic data, GridView.builder() lazily builds grid items on demand, making it memory-efficient for large datasets. The crossAxisCount and childAspectRatio properties control the grid layout.
The Padding widget inserts empty space around its child widget. You define the padding using an EdgeInsets object, which supports all() for equal padding on all sides, symmetric() for horizontal/vertical padding, and only() for padding on specific sides. Example: Padding(padding: EdgeInsets.all(16), child: Text("Hello")). You can also use the padding property directly on the Container widget.
The Text widget displays a string of text on the screen. You can style it using the style property with a TextStyle object, which controls font size, color, weight, letter spacing, and more. For multiple styles in a single line, use RichText with TextSpan children. The overflow property controls what happens when the text is too long (e.g., ellipsis, clip, fade).
The Image widget displays an image in Flutter. It has several named constructors for different sources: Image.network(url) loads from the internet, Image.asset(path) loads from the app's assets folder, Image.file(file) loads from the device filesystem, and Image.memory(bytes) loads from raw bytes. The fit property (BoxFit) controls how the image is scaled within its bounds.
ElevatedButton is the standard Material Design button with elevation (shadow), indicating it is raised above the surface. It is the modern replacement for the deprecated RaisedButton. It requires a child (usually a Text widget) and an onPressed callback. Setting onPressed: null automatically disables the button and applies the disabled style. Customize its appearance using the style property with ElevatedButton.styleFrom().
TextField is the primary widget for accepting text input from the user. It is controlled by a TextEditingController, which lets you read the current text (controller.text), set text programmatically, and listen for changes. The decoration property (using InputDecoration) adds label, hint, prefix/suffix icons, and borders. Use keyboardType to specify the keyboard layout (email, phone, number, etc.).
A Future in Dart represents a value or error that will be available at some point in the future — similar to a Promise in JavaScript. It is used for asynchronous operations like network requests or file reads that take time to complete. You work with Futures using async/await for readable sequential code, or the .then(), .catchError() chain. Futures complete exactly once with either a value or an error.
async/await is syntactic sugar for working with Futures in a readable, sequential style without nested callbacks. Marking a function with async makes it return a Future. Using await inside an async function pauses execution at that point until the Future completes, then resumes with the result. Critically, await does not block the UI thread — it suspends only the current async function, freeing the event loop for other work.
AppBar is a Material Design app bar displayed at the top of the screen, usually inside a Scaffold. It includes a title, optional leading icon (usually a back or menu button), and actions (a row of icon buttons on the right). You can add a bottom widget (like a TabBar) and a flexibleSpace for custom background content. The elevation property controls the shadow depth.
CircularProgressIndicator shows a spinning circular animation to indicate that the app is working on something. For indeterminate loading (unknown duration), use it without a value property. For determinate progress (known percentage), set value between 0.0 and 1.0. The color property customizes the indicator color. For a horizontal loading bar, use LinearProgressIndicator.
BuildContext is a handle to the location of a widget within the widget tree. Every build() method receives a BuildContext, which is used to look up ancestor widgets like Theme.of(context), MediaQuery.of(context), and Navigator.of(context). It is important to understand that BuildContext is tied to the widget's position — using a stale or wrong context (e.g., from a disposed widget) is a common source of bugs.
The Drawer widget creates a sliding navigation panel that appears from the left edge of the screen. It is assigned to the drawer property of a Scaffold and is typically revealed when the user taps the hamburger menu icon in the AppBar. A Drawer usually contains a DrawerHeader (for user info) and a ListView of ListTile navigation items. Close it programmatically with Navigator.pop(context).
Both final and const declare variables that cannot be reassigned, but they differ in when the value is determined. A final variable is assigned once at runtime — its value can be computed when the program runs (e.g., from a function call or user input). A const variable must have a value known at compile time and creates a deeply immutable, canonicalized object. Using const for widgets and values that never change improves Flutter rendering performance.
The Icon widget renders a Material Design icon from the built-in Icons collection, which includes hundreds of common icons. You specify the icon with Icons.home, Icons.search, etc., and control its size, color, and semanticLabel. For custom SVG or image icons, use ImageIcon instead. Icons are rendered as font glyphs, making them sharp at any resolution.
FloatingActionButton (FAB) is a circular button that floats above the main content, representing the primary action of the screen. It is placed in the floatingActionButton slot of a Scaffold and automatically positions itself. It requires a child (usually an Icon) and an onPressed callback. Use FloatingActionButton.extended() for a wider button with both an icon and a label. Only use one FAB per screen.
Null safety, introduced in Dart 2.12, is a type system feature that makes all types non-nullable by default. This means the Dart compiler guarantees that a variable of type String will never contain null, eliminating an entire class of runtime null-reference errors. To allow null, you explicitly opt in with a ? suffix: String? name. The compiler then forces you to handle the null case before accessing the value, making code more robust and self-documenting.
A SnackBar is a brief message that appears at the bottom of the screen to provide lightweight feedback about an operation (e.g., "Item deleted"). Show it using ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Message"))). You can add an action (like "Undo") to the SnackBar. It automatically disappears after a short duration, or you can set a custom duration. SnackBars are queued — only one is shown at a time.
GridView arranges widgets in a 2D scrollable grid (rows and columns), while ListView arranges them in a single-column (or single-row) scrollable list. GridView is ideal for displaying image galleries, product catalogs, or any content that benefits from a tiled layout. Both support lazy building via their .builder() constructors. The key GridView-specific property is crossAxisCount (number of columns) or maxCrossAxisExtent (maximum tile width).
setState() is a method on the State class of a StatefulWidget that notifies the Flutter framework that the internal state has changed and the widget needs to be rebuilt. You pass a callback to setState() that contains the actual state mutation. Flutter schedules a rebuild, calling build() again to generate an updated widget tree. Calling setState() outside of an active State (e.g., after dispose) will throw an error, so always check mounted if the widget might have been removed.
SizedBox is a simple widget with a fixed width and/or height. It is commonly used in two ways: to give a child widget specific dimensions (SizedBox(width: 200, height: 50, child: Button())), or as an empty spacer between widgets without needing a child (SizedBox(height: 16)). It is more semantically explicit than using Container with just a size, and the Flutter linter prefers it for simple spacing.
MaterialApp is the root widget for Material Design apps. It configures app-wide settings including the theme (colors, fonts, shapes), routes (named navigation routes), locale (language/region), debugShowCheckedModeBanner, and the initial screen (home). It also sets up the Navigator for managing screen transitions. Every Material app should have exactly one MaterialApp at its root.
The Expanded widget makes its child fill all available space along the main axis of its parent Row or Column. If multiple Expanded widgets are used, the flex property determines how the available space is distributed proportionally — a child with flex: 2 gets twice the space of one with flex: 1. It is essential for creating responsive layouts that adapt to different screen sizes without hardcoding dimensions.
The late keyword is a modifier for non-nullable variables that are initialized after their declaration, not at the point of declaration. It tells the Dart compiler "I promise this will be set before it is used — trust me." If you access a late variable before it is initialized, a LateInitializationError is thrown at runtime. It is commonly used for controllers in StatefulWidgets: late TextEditingController _controller; initialized in initState().
BottomNavigationBar displays a row of navigation destinations at the bottom of the screen, following Material Design conventions. It is placed in the bottomNavigationBar slot of a Scaffold. Each destination is a BottomNavigationBarItem with an icon and a label. The currentIndex property tracks the selected tab, and onTap receives the tapped index. For more than 4 items or complex navigation, consider using NavigationBar (Material 3) instead.
flutter pub get reads the pubspec.yaml file and downloads all declared dependencies (and their transitive dependencies) from pub.dev, placing them in the local pub cache. It also generates the pubspec.lock file, which pins the exact versions used, ensuring consistent builds across machines and over time. You run this command after adding a new package to pubspec.yaml or after cloning a project for the first time.
TextEditingController is used to read, write, and listen to changes in a TextField. Assign it to the controller property of the TextField. Read the current text with controller.text, set text with controller.text = "new value", and listen for changes by adding a listener: controller.addListener(() { ... }). Always call controller.dispose() in the widget's dispose() method to free resources and prevent memory leaks.
The dispose() method is called by the Flutter framework when a State object is permanently removed from the widget tree. It is the correct place to release resources and prevent memory leaks: cancel subscriptions, close streams, dispose controllers (TextEditingController, AnimationController, ScrollController), and unsubscribe from listeners. Always call super.dispose() at the end of your override. Not implementing dispose() for long-lived resources is a very common source of Flutter memory leaks.
initState() is called exactly once when the State object is first inserted into the widget tree, before the first call to build(). It is the right place to initialize controllers, subscribe to streams, and trigger one-time data fetching. Always call super.initState() first. You cannot use BuildContext for things like navigation or showing dialogs directly in initState() — instead, use WidgetsBinding.instance.addPostFrameCallback() to run code after the first frame.
A Stream in Dart is a sequence of asynchronous events delivered over time — like an ongoing pipe of data rather than a single Future value. Streams can deliver zero or more data events followed by an optional error or done event. They are used for continuous data like WebSocket messages, real-time database updates (Firebase), or user input events. Use StreamBuilder in Flutter to reactively rebuild UI whenever the stream emits a new value.
The Wrap widget is like a Row or Column but automatically wraps its children to the next line when there is no more space. This makes it perfect for displaying a dynamic set of items like filter chips, tags, or badge lists that need to flow naturally across multiple lines. You can control the spacing between items horizontally and runSpacing between rows, as well as the direction (horizontal or vertical) and alignment.
Both Expanded and Flexible are used inside a Row or Column to control how children share space. Expanded forces its child to fill all remaining space (it uses FlexFit.tight). Flexible allows its child to be smaller than the allocated space — the child can choose its own size up to the available space (it uses FlexFit.loose by default). Use Flexible when you want the child to shrink-wrap its content but still participate in flex layout.
State management refers to how an app handles, shares, and responds to changes in data across its widget tree. In simple apps, setState() within a single widget is sufficient. But as apps grow, data needs to be shared between multiple screens and widgets. Poor state management leads to deeply nested callbacks, duplicated data, and hard-to-debug UI bugs. Flutter has several approaches ranging from simple (Provider) to powerful (Riverpod, BLoC) — choosing the right one depends on your app's complexity.
Provider is the officially recommended state management package for Flutter. It builds on top of InheritedWidget to propagate data down the widget tree efficiently. You wrap a part of the tree with a ChangeNotifierProvider, expose a ChangeNotifier model, and any widget can access it via context.read() (one-time access) or context.watch() (subscribes to changes). When notifyListeners() is called, only the widgets watching that provider rebuild.
The BLoC (Business Logic Component) pattern separates business logic from the UI using streams. The UI dispatches Events to a BLoC, the BLoC processes the event and emits a new State, and the UI rebuilds in response to the new state. The flutter_bloc package makes this concrete with Cubit (simpler, state-based) and Bloc (full event-to-state mapping). BLoC excels in large teams because the business logic is completely decoupled and easily testable.
InheritedWidget is Flutter's built-in mechanism for efficiently propagating data down the widget tree. Any descendant widget can access the data by calling context.dependOnInheritedWidgetOfExactType(), and it will automatically be rebuilt when the data changes. It is the foundation that powers Theme, MediaQuery, Navigator, and most state management packages like Provider. While powerful, InheritedWidget has verbose boilerplate — packages like Provider exist to simplify working with it.
FutureBuilder builds its UI based on the current state of an asynchronous Future. It rebuilds whenever the Future changes state. The builder receives an AsyncSnapshot with a connectionState (none, waiting, done) and the resulting data or error. You use this to show a loading spinner while data is being fetched, an error message if it fails, and the actual UI when the data is available. Important: pass a pre-created Future or the FutureBuilder will restart the future on every rebuild.
The Hero animation creates a smooth "shared element" transition where a widget appears to fly from one screen to another during navigation. Both the source and destination widgets must be wrapped in a Hero widget with the same tag value. Flutter automatically handles the animation between the two positions and sizes. It is commonly used for image galleries or product cards where the item image expands into a detail view. The tag must be unique among all visible Hero widgets at any given time.
A Tween (short for in-between) defines how to interpolate between a begin and end value over the course of an animation. It maps the animation controller's 0.0–1.0 range to the actual value range. For example, Tween<double>(begin: 0, end: 300) produces values from 0 to 300 as the animation progresses. Tweens exist for many types: ColorTween, SizeTween, RectTween. You animate a Tween by calling tween.animate(controller) to get an Animation<T>.
AnimationController is the core class that drives animations in Flutter. It generates values between 0.0 and 1.0 over a specified duration and requires a vsync parameter (usually the State class implementing TickerProviderStateMixin) to synchronize with the screen refresh rate. Use .forward() to play forward, .reverse() to play backward, and .repeat() to loop. Always call controller.dispose() in the widget's dispose() method.
CustomPainter lets you draw custom graphics directly on a canvas, similar to onDraw() in Android. You subclass it and override paint(Canvas, Size) to draw shapes, paths, text, and images using the Canvas API. The shouldRepaint() method tells Flutter whether to repaint when the painter is rebuilt — return false if the output has not changed. Use CustomPainter inside a CustomPaint widget. It is ideal for custom charts, progress indicators, and complex graphics.
The Navigator manages a stack of routes (screens) in a Flutter app. Navigator.push() pushes a new route on top of the stack (navigates forward), and Navigator.pop() removes the top route (navigates back). Navigator.pushReplacement() replaces the current route — useful for the login → home transition so the user cannot go back to login. For named routes, use Navigator.pushNamed(context, "/home"). Flutter 2.0 introduced the Navigator 2.0 (Router) API for URL-based navigation.
SharedPreferences is a Flutter plugin for persisting simple key-value data on the device. It supports bool, int, double, String, and List<String>. It uses NSUserDefaults on iOS and SharedPreferences on Android under the hood. Usage: final prefs = await SharedPreferences.getInstance(); prefs.setString("token", token);. It is asynchronous and persists data across app restarts. Do not store sensitive data like passwords here — use flutter_secure_storage instead.
A mixin is a way to reuse code across multiple class hierarchies without inheritance. Declare a mixin with mixin MyMixin { ... } and apply it with the with keyword: class MyClass extends BaseClass with MyMixin. Mixins cannot have constructors. A class can apply multiple mixins. They are widely used in Flutter for capabilities like TickerProviderStateMixin (provides vsync for animations) and AutomaticKeepAliveClientMixin (preserves scroll position).
sqflite is the most popular Flutter plugin for using SQLite, a local relational database. It is supported on Android, iOS, and macOS. You can create tables, run SQL queries (SELECT, INSERT, UPDATE, DELETE), use transactions, and perform migrations. It works well for structured data like user profiles, cached API responses, or offline-first apps. For a more Dart-idiomatic ORM approach on top of sqflite, consider using the drift (formerly Moor) package.
MediaQuery provides information about the device screen and accessibility settings. Access it via MediaQuery.of(context). Key properties include size (screen dimensions), padding (safe area insets for notches/home bar), viewInsets (keyboard height when visible), orientation, textScaleFactor (user font scaling), and platformBrightness (light/dark mode). It is essential for building responsive layouts and respecting the device's safe areas.
Dio is a powerful HTTP client for Dart/Flutter that goes beyond the basic http package. It supports interceptors (for adding auth headers or logging to every request), request cancellation, FormData for file uploads, download progress tracking, and timeout configuration. Interceptors are particularly useful — you can automatically refresh tokens and retry requests transparently. It is the preferred choice for production apps that need fine-grained control over HTTP communication.
ChangeNotifier is a simple class from the Flutter foundation that provides a notification mechanism. You extend it in your state model class, and call notifyListeners() whenever the state changes. Widgets that are watching this notifier (via Provider's context.watch() or Consumer) will automatically rebuild. It is the backbone of Provider-based state management. For more scalable alternatives, Riverpod's NotifierProvider and BLoC are commonly used.
A Key is an identifier that helps Flutter's reconciliation algorithm correctly match widget instances across rebuilds, especially in lists. Without keys, when you reorder a list of StatefulWidgets, Flutter may incorrectly reuse State objects from the old positions, causing visual glitches or lost data. Use ValueKey(id) when identity is based on a unique value, ObjectKey(object) when the object itself is the identifier, and UniqueKey() when you want to force a widget to always be treated as new.
StreamBuilder is a widget that rebuilds its UI whenever the subscribed Stream emits a new event. It is essential for reactive UI — when the stream emits data, error, or completes, the builder receives an updated AsyncSnapshot and rebuilds. Common use cases include real-time data from Firebase Firestore, WebSocket connections, and Bluetooth device events. Unlike FutureBuilder, streams can emit multiple values over time. StreamBuilder automatically subscribes and unsubscribes from the stream.
A factory constructor is a constructor that does not always create a new instance of the class. Instead, it can return an existing instance (singleton pattern) or a subtype. It is declared with the factory keyword. The most common use is JSON deserialization: factory User.fromJson(Map<String, dynamic> json) => User(name: json["name"]). Factory constructors cannot use this to refer to the instance being created, so they rely on delegating to other constructors or returning existing instances.
TabBar displays a horizontal row of tab indicators (text or icons) for switching between related views. It works with DefaultTabController (simple) or a TabController (programmatic control). Place the TabBar in the bottom property of an AppBar and a TabBarView in the Scaffold body. The TabController synchronizes the tab selection with the displayed view. Each Tab in the TabBar corresponds to a child in the TabBarView.
SliverAppBar is a special app bar designed to work inside a CustomScrollView. It can expand when scrolled to the top and collapse as the user scrolls down, creating a collapsible header effect. Key properties: expandedHeight (height when fully expanded), flexibleSpace (content to show when expanded, often a hero image), pinned: true (keeps the collapsed bar visible), and floating: true (re-appears when scrolling up). It enables modern, polished scroll effects seen in many popular apps.
json_serializable is a code generation package that automatically generates fromJson() and toJson() methods for Dart model classes. You annotate your class with @JsonSerializable() and run flutter pub run build_runner build. This eliminates hand-written boilerplate serialization code and reduces the risk of typos in JSON key strings. Combined with the json_annotation package, it supports field renaming, type converters, and null handling customization.
LayoutBuilder provides access to the parent widget's constraints inside the build method, enabling truly responsive layouts. Its builder function receives a BoxConstraints object with maxWidth and maxHeight, letting you make layout decisions based on the available space. For example, show a two-column layout when maxWidth > 600, otherwise single-column. This is more reliable than using MediaQuery for nested layouts because it reflects the actual available space, not the full screen size.
GestureDetector is a widget that detects touch gestures and triggers callbacks — without adding any visual indicator. It wraps any widget and provides callbacks like onTap, onDoubleTap, onLongPress, onPanUpdate (drag), onScaleUpdate (pinch), and more. It is the primary way to add interactivity to custom widgets that are not inherently tappable. For standard interactive elements like buttons, use built-in widgets like ElevatedButton or InkWell instead, as they include accessibility support.
PageView is a scrollable widget where each child fills the entire viewport and users swipe horizontally (or vertically) to navigate between them. It is perfect for onboarding screens, image carousels, and tabbed content without a visible tab bar. Control it with a PageController, which lets you programmatically jump to a specific page with controller.animateToPage() or check the current page with controller.page. Add a PageView.builder() for large or infinite page sets.
Riverpod is a state management library created by the same author as Provider but designed to fix its limitations. Key improvements: providers are globally accessible without requiring a BuildContext, there is no risk of ProviderNotFoundError, providers are strongly typed and testable, and it natively supports asynchronous state with FutureProvider and AsyncNotifier. Riverpod also has compile-time safety with code generation (@riverpod annotation). It is one of the most popular choices for production Flutter apps today.
AnimatedContainer is the simplest way to create animations in Flutter. It is identical to a regular Container but automatically animates any property change (width, height, color, padding, alignment, decoration) over a given duration. Just call setState() to change any property and the animation happens automatically — no AnimationController needed. Use it for interactive elements that need to change size, color, or shape in response to user actions, like toggle switches or expanding cards.
Dart is single-threaded, but you can achieve true parallelism using Isolates — independent workers that have their own memory heap and Dart event loop. Isolates do not share memory; they communicate by passing messages through ports. This design eliminates race conditions. Use isolates for CPU-intensive work like parsing large JSON, image processing, or complex computations that would otherwise freeze the UI. The simpler compute() function is a shortcut for spawning a one-off isolate and returning its result.
compute() is a helper function that runs a given function in a background isolate and returns the result to the main isolate as a Future. It takes a top-level or static function and a single argument: final result = await compute(parseJson, jsonString). This is the recommended way to offload heavy synchronous work (like parsing a large JSON response) off the main UI thread without the full complexity of manually managing isolates. The function and its argument must be serializable for isolate message passing.
GetX is a lightweight, all-in-one package that combines state management, dependency injection, and route management. For state, it uses Rx variables (reactive) with the Obx widget for automatic rebuilds, or GetBuilder for simpler manual updates. Navigation works without BuildContext: Get.to(Screen()). Dependency injection: Get.put(Controller()). GetX is popular for its minimal boilerplate, but some developers prefer BLoC or Riverpod for their more explicit, testable patterns.
Extensions allow you to add new methods to existing types without modifying their source code or using inheritance. Declare an extension with extension StringUtils on String { ... } and then call its methods on any String. For example: extension on DateTime { bool get isToday => day == DateTime.now().day; }. Extensions are resolved statically at compile time and do not actually modify the original class. They are widely used to add utility helpers to built-in types like String, List, and DateTime.
ValueNotifier is a specialized ChangeNotifier that holds a single value and automatically notifies its listeners whenever the value changes. Set the new value with notifier.value = newValue. It is a lightweight alternative to Provider for simple, local state: final counter = ValueNotifier<int>(0);. Use it with ValueListenableBuilder to rebuild only the widget that needs to react, avoiding unnecessary rebuilds of the full widget tree.
Navigator.pushReplacement() navigates to a new screen while simultaneously removing the current screen from the navigation stack. This means the user cannot press the back button to return to the previous screen. It is the correct choice for post-login navigation (from login screen to home) or when transitioning between onboarding steps where going back does not make sense. For clearing the entire stack and starting fresh, use Navigator.pushAndRemoveUntil().
AnimationController generates a sequence of values from 0.0 to 1.0 over a specified duration. It requires a vsync parameter (the State class mixed with TickerProviderStateMixin) to tie animations to the screen's 60fps refresh rate, preventing off-screen animations from consuming CPU. Control playback with .forward(), .reverse(), .stop(), and .repeat(). Combine it with a Tween and an AnimatedBuilder or AnimatedWidget to drive actual UI changes.
Flutter maintains three synchronized trees internally. The Widget Tree is made up of immutable configuration objects — lightweight descriptions of the UI that are rebuilt frequently. The Element Tree is the live, mutable counterpart that persists across rebuilds, managing the lifecycle of widgets and connecting Widgets to RenderObjects. The RenderObject Tree handles the actual layout (measuring size and position) and painting (drawing to the canvas). Understanding this separation explains why Flutter is efficient — rebuilding widgets is cheap because only elements and render objects that actually changed are updated.
A RenderObject is the core low-level object in Flutter's rendering pipeline responsible for layout and painting. It receives BoxConstraints from its parent, calculates its own size, positions its children, and paints to the Canvas. Most developers never interact with RenderObjects directly — they use Widgets, which create and manage RenderObjects internally. Custom RenderObjects are created when building highly optimized custom layouts or widgets that cannot be achieved by composing existing widgets.
Tree shaking is a build-time optimization performed by the Dart compiler in release mode. It statically analyzes the code and removes all unused functions, classes, and even individual Material/Cupertino icons that are never referenced in your code. This significantly reduces the final app size. For Material icons, Flutter only includes the specific icon glyphs you use — importing the full icons font without tree shaking would add several MB. Always test your release build size, as debug builds include the full code and are not tree-shaken.
MethodChannel is the primary mechanism for calling native platform code (Java/Kotlin on Android, Objective-C/Swift on iOS) from Dart, and vice versa. Both sides register a handler with a matching channel name (a string identifier). From Dart, you call channel.invokeMethod("methodName", args) which sends a message to the native side, where the registered handler executes native code and returns a result. MethodChannel supports basic types (String, int, bool, List, Map) that are automatically serialized across the boundary.
A Package contains only pure Dart code — no platform-specific implementations. Examples include provider, dio, and intl. A Plugin is a special type of package that contains Dart code plus platform-specific implementations (Java/Kotlin for Android, Swift/Objective-C for iOS) that communicate through platform channels. Examples include camera, geolocator, and firebase_core. Plugins are necessary when you need to access device hardware or native OS APIs that are not available in pure Dart.
When you use const constructors, Dart creates the widget object at compile time and canonicalizes it — meaning the same const widget with the same arguments is the exact same object in memory. Flutter's reconciliation algorithm can then detect that a const widget has not changed and skip rebuilding its entire subtree. This is a significant optimization in large widget trees where a parent setState() would otherwise force all child widgets to rebuild unnecessarily. Always use const for widgets that never change: const Text("Hello"), const SizedBox(height: 16).
Flutter DevTools is a browser-based suite of performance and debugging tools. The Widget Inspector visualizes the widget tree and helps identify layout issues. The Performance view shows frame rendering times and identifies jank (frames that take longer than 16ms). The Memory view tracks heap allocations and helps detect memory leaks. The Network view monitors HTTP calls. The CPU Profiler shows which functions consume the most CPU time. Launch DevTools from the IDE or by running dart devtools in the terminal.
ValueKey uses a value's equality to identify a widget — two ValueKeys are equal if their values are equal (e.g., ValueKey(user.id) is best when you have a stable identifier). ObjectKey uses an object's reference identity — two ObjectKeys are equal only if they wrap the exact same object instance in memory. UniqueKey is never equal to any other key — it forces Flutter to treat a widget as entirely new every time, which can be used to force a widget to reset its state. Use UniqueKey sparingly as it prevents state preservation.
Flutter's rendering pipeline executes in this order each frame: Build → Layout → Paint → Composite. In the Build phase, Flutter calls build() on dirty widgets to produce the widget tree. In the Layout phase, RenderObjects receive constraints from their parents, calculate their sizes, and position their children. In the Paint phase, RenderObjects paint to their Canvas layers. Finally, in the Composite phase, the GPU takes the painted layers and composites them into the final frame displayed on screen. Understanding this pipeline helps you optimize at the right stage.
RepaintBoundary promotes its child widget to its own compositing layer. When only the child changes, Flutter can repaint just that layer and composite it with the other unchanged layers, without repainting the rest of the screen. Use it around widgets that repaint frequently but are visually isolated — like a custom animated widget, a video player, or a rapidly updating progress indicator. The tradeoff is extra memory for the additional layer. You can verify if your RepaintBoundary is helping by enabling the "Show repaint rainbow" in DevTools.
Flutter achieves consistent frame rates by owning every pixel on the screen through its own graphics engine (Skia, now transitioning to Impeller). Unlike React Native which bridges to native components, Flutter renders its own widgets directly to the canvas — there is no OEM widget translation layer that could cause jank. The Dart VM's low-latency garbage collector avoids long GC pauses. Flutter's animation system is synchronized with the display refresh rate (vsync). And const widgets, RepaintBoundary, and lazy list builders prevent unnecessary work each frame.
Impeller is Flutter's new rendering engine designed to replace the aging Skia renderer. The key problem it solves is shader compilation jank — with Skia, complex shaders are compiled at runtime on first use, causing a visible stutter. Impeller pre-compiles all shaders to Metal (iOS) and Vulkan (Android) at build time, guaranteeing smooth first-frame rendering. Impeller is the default on iOS since Flutter 3.10 and is being rolled out to Android. It also enables more advanced visual effects and better GPU utilization.
Flutter Flavors allow you to build multiple variants of the same app from the same codebase — typically dev, staging, and production. Each flavor has its own app ID, display name, app icon, Firebase project, and API base URL. On Android, flavors use Gradle's product flavors. On iOS, they use schemes and targets in Xcode. You define flavor-specific Dart constants via --dart-define flags. This is essential for professional app development where you need separate apps for testing without affecting production users.
Debug mode uses the Dart JIT compiler, enabling Hot Reload and Hot Restart. It includes assertion checks, widget inspector support, and detailed error messages, but runs significantly slower than release. Profile mode uses AOT compilation like release but retains profiling hooks, allowing DevTools to measure real-world performance without debug overhead — always profile on a real device in this mode. Release mode uses full AOT compilation with all optimizations, tree shaking, and no debugging overhead — this is what ships to users and is dramatically faster than debug.
EventChannel is used for continuous, streaming communication from native platform code to Dart — unlike MethodChannel which is a one-shot request/response. The native side uses a StreamHandler to push events (sensor readings, Bluetooth states, connectivity changes) into an event sink. On the Dart side, you listen to the channel as a Stream: EventChannel("my_channel").receiveBroadcastStream().listen((event) { ... }). EventChannel is the correct choice for any native data that updates continuously over time.
build_runner is a Dart build system that generates source files based on annotations in your code. Run it with flutter pub run build_runner build for a one-time generation or watch for continuous generation during development. It powers several major packages: json_serializable (JSON models), freezed (immutable classes), injectable (dependency injection), hive_generator (Hive adapters), and riverpod_generator. Generated files end in .g.dart or .freezed.dart and should be committed to version control.
Hive is a fast, lightweight, pure-Dart NoSQL database for local storage. It stores data in typed "boxes" (key-value stores) and uses binary serialization, making it significantly faster than SharedPreferences or SQLite for object storage. Hive requires no native dependencies, making it cross-platform. For storing custom objects, you generate a TypeAdapter using build_runner. Hive is excellent for caching API responses, storing user settings, or persisting domain objects locally. For complex queries and relationships, sqflite or Drift is more appropriate.
flutter_secure_storage stores sensitive data using the platform's secure storage mechanisms: iOS Keychain and Android Keystore (via EncryptedSharedPreferences on API 23+). Data is encrypted at rest and protected by the device's hardware security module where available. Use it for authentication tokens, refresh tokens, PINs, and passwords — any data that would be a security risk if accessed from a compromised or rooted device. Never use SharedPreferences or Hive for sensitive credentials, as they store data unencrypted.
freezed is a code generation package that creates immutable data classes with proper ==, hashCode, toString(), and a copyWith() method. Annotate a class with @freezed and run build_runner. It also supports union types (sealed classes), making it ideal for modeling BLoC/Cubit states: @freezed class AuthState with _$AuthState { const factory AuthState.loading() = _Loading; ... }. The generated when() and map() methods provide exhaustive pattern matching over union variants.
The Element tree is Flutter's internal, mutable representation of the widget tree that persists across rebuilds. While widgets are recreated on every build() call, elements are long-lived — they hold the actual state of StatefulWidgets and connect widgets to their corresponding RenderObjects. When Flutter rebuilds, it performs a reconciliation: it compares the new widget configuration against the existing element. If the widget type and key match, the element is updated in place (much cheaper than destroying and recreating it). This is why Flutter can be fast despite frequently rebuilding widgets.
Flutter renders its own widgets on its own canvas using Skia/Impeller — it does NOT use native platform widgets. This means a Flutter app looks identical on Android and iOS by default. To follow platform conventions, Flutter provides separate widget sets: Material widgets follow Android/Google design guidelines, and Cupertino widgets follow iOS design guidelines. You can use Platform.isIOS or defaultTargetPlatform to conditionally render different widgets. For embedding native views (like Maps or WebView), use PlatformView or established plugins.
InheritedNotifier combines InheritedWidget and Listenable. It is a generic InheritedWidget that automatically marks dependent widgets as dirty whenever the wrapped Listenable (like a ValueNotifier or AnimationController) calls its listeners. This means you get the efficiency of InheritedWidget (only rebuilds widgets that depend on it) combined with the simplicity of Listenable-based state. It is used internally by the Flutter framework and is the building block for creating custom, efficient state management solutions without relying on external packages.
flutter_gen is a code generation tool that creates type-safe Dart accessors for all assets declared in pubspec.yaml. Instead of using error-prone string paths like Image.asset("assets/images/logo.png"), you get compile-time checked references like Assets.images.logo.image(). If you rename or delete an asset, the code fails to compile immediately rather than crashing at runtime. It also generates type-safe accessors for fonts, colors, and other resources, making asset management significantly more robust in large projects.
Navigator.pushAndRemoveUntil() navigates to a new route and removes all existing routes from the stack that satisfy a given predicate. To clear the entire stack: Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => HomeScreen()), (route) => false). This is used after logout (to clear the authenticated stack before showing login), after completing an onboarding flow, or after a payment confirmation where you do not want the user to navigate back to intermediate screens.