Welcome to Part 14 -- the grand finale of the Flutter Interview Questions 2025 series! You have made it through 13 parts covering everything from Dart basics to system design, and now we close with two sections that test whether you are truly current with the Flutter ecosystem. First, we cover Flutter 3.x and Dart 3 latest features: Impeller (the new rendering engine), Records and Patterns, sealed classes with exhaustive switch, WebAssembly support for Flutter Web, class modifiers, element embedding, macros (upcoming), Material 3 ColorScheme.fromSeed, and Shorebird OTA updates. Then we finish with 20 rapid-fire one-liner questions that test instant, deep understanding -- the kind of questions where hesitation reveals the difference between memorization and true comprehension. This is part 14 of our comprehensive 14-part series. If you have followed along from Part 1, congratulations -- you have covered the most thorough Flutter interview preparation guide available anywhere.
What's in this part?
- Section 1: Flutter 3.x / Latest Features (10 Qs) -- Flutter 3.x overview (Impeller, Material 3, Wasm), Records and Patterns in Dart 3, sealed classes and exhaustive switch, Impeller deep dive, Flutter Web and WASM status, class modifiers (base, interface, final, sealed, mixin), element embedding, upcoming macros, Material 3 ColorScheme.fromSeed, Shorebird OTA updates
- Section 2: Rapid Fire / One-Liner Tricky Questions (20 Qs) -- StatefulWidget without setState, single vs multi-threaded, widget tree depth limits, Flutter without MaterialApp, build() returning null, mutable state in StatelessWidget, Visibility vs Offstage, animating between different widgets, 120fps support, Flutter without a device, tree shaking, circular dependencies, conflicting dependency versions, Navigator 1.0 + 2.0 together, provider nesting limits, setState in initState, widget appearing twice, raster thread, paint vs compositing, global error catching
SECTION 1: FLUTTER 3.x / LATEST FEATURES QUESTIONS
These questions test whether a candidate stays current with the Flutter ecosystem and understands the "why" behind new features, not just the syntax.
Q1. What's new in Flutter 3.x? (Material 3, Impeller, WebAssembly, etc.)
What the interviewer is REALLY testing:
Whether you actively follow Flutter development, understand the strategic direction, and can articulate why these changes matter -- not just list bullet points.
Answer:
Flutter 3.x brought several paradigm shifts:
1. Impeller (the new rendering engine):
- Replaces Skia on iOS (default since Flutter 3.16), and is available on Android.
- Pre-compiles all shaders at build time, eliminating the "jank on first run" problem (shader compilation stutter).
- Uses Metal on iOS and Vulkan/OpenGL on Android.
- Architectural shift: Skia was designed for 2D graphics in browsers; Impeller was designed specifically for Flutter's rendering model.
2. Material 3 (Material You):
-
useMaterial3: trueis now the default. -
ColorScheme.fromSeed()generates a full, harmonious color palette from a single seed color using the HCT (Hue-Chroma-Tone) color space. - New components:
NavigationBar(replacesBottomNavigationBar),NavigationDrawer,SegmentedButton,SearchBar,Badge. - Dynamic color on Android 12+ pulls colors from the user's wallpaper.
3. Dart 3 language features (see Q2-Q6 for details):
- Records, patterns, sealed classes, class modifiers, exhaustive switch.
- These are not just syntax sugar -- they fundamentally change how you model state and handle data.
4. WebAssembly (Wasm) support for Flutter Web:
- Flutter Web can now compile to Wasm instead of JavaScript.
- Significant performance improvement for compute-heavy web apps.
- Requires a browser that supports WasmGC (Chrome 119+, Firefox 120+).
5. DevTools improvements:
- New "Deep Links" validator for verifying Android App Links and iOS Universal Links.
- Enhanced performance tooling for Impeller.
- Widget inspector improvements with layout explorer.
6. Platform views improvements:
- Better performance for embedding native views (maps, WebView) on Android and iOS.
- Reduced rendering overhead through composition strategies.
7. Custom fragment shaders:
- Write GLSL shaders and use them directly in Flutter via
FragmentProgram. - Enables custom visual effects that would be impossible with the widget API alone.
The strategic narrative: Flutter is maturing from "cross-platform UI toolkit" into a "multi-platform application framework." Impeller makes it a first-class graphics engine. Dart 3 makes it a first-class language for modeling complex domains. Wasm makes web deployment viable for performance-sensitive apps.
Q2. What are Records and Patterns in Dart 3? How do they change Flutter code?
What the interviewer is REALLY testing:
Whether you understand that Records solve the "return multiple values" problem without creating boilerplate classes, and Patterns give Dart structural pattern matching similar to Kotlin/Swift/Rust.
Answer:
Records are anonymous, immutable, structural types for bundling multiple values:
// BEFORE Dart 3: You'd create a class or return a Map
class MinMax {
final double min;
final double max;
MinMax(this.min, this.max);
}
// AFTER Dart 3: Records
(double min, double max) findMinMax(List<double> values) {
return (values.reduce(min), values.reduce(max));
}
// Usage with destructuring
final (minimum, maximum) = findMinMax([3.0, 1.0, 4.0, 1.5]);
// Named fields
({String name, int age}) getUser() => (name: 'Alice', age: 30);
final (:name, :age) = getUser(); // destructure named fields
How Records change Flutter code:
// BEFORE: Returning multiple values from a dialog
Future<(bool confirmed, String? reason)> showConfirmDialog(BuildContext context) async {
// ...
}
final (confirmed, reason) = await showConfirmDialog(context);
if (confirmed) {
// use reason
}
// BEFORE: You'd return a Map or create a DialogResult class for this.
Patterns enable structural matching on objects:
// Switch expressions (not statements) with exhaustive checking
String describeShape(Shape shape) => switch (shape) {
Circle(radius: var r) when r > 100 => 'Large circle',
Circle(radius: var r) => 'Circle with radius $r',
Rectangle(width: var w, height: var h) when w == h => 'Square ${w}x$h',
Rectangle(width: var w, height: var h) => 'Rectangle ${w}x$h',
};
// Destructuring in for loops
final Map<String, int> scores = {'Alice': 95, 'Bob': 87};
for (final MapEntry(:key, :value) in scores.entries) {
print('$key scored $value');
}
// If-case pattern matching
void processResponse(Map<String, dynamic> json) {
// Deep pattern matching on JSON
if (json case {'status': 'ok', 'data': {'users': List users}}) {
// users is extracted and typed as List
for (final user in users) {
print(user);
}
}
}
// List patterns
final [first, second, ...rest] = [1, 2, 3, 4, 5];
// first = 1, second = 2, rest = [3, 4, 5]
Real Flutter impact -- JSON parsing without code generation:
// Pattern matching on API responses
Widget buildFromResponse(Map<String, dynamic> json) {
return switch (json) {
{'type': 'text', 'content': String text} => Text(text),
{'type': 'image', 'url': String url} => Image.network(url),
{'type': 'list', 'items': List items} => ListView(
children: items.map((i) => Text('$i')).toList(),
),
_ => const Text('Unknown content type'),
};
}
Key insight: Records + Patterns together eliminate a whole category of boilerplate data classes and verbose if-else chains. In Flutter specifically, they make state handling more expressive -- you can destructure state objects directly in switch expressions inside build().
Q3. Explain sealed classes and exhaustive switch in Dart 3
What the interviewer is REALLY testing:
Whether you understand algebraic data types, why exhaustiveness checking prevents bugs, and how sealed classes replace the "enum + switch with default" anti-pattern.
Answer:
Sealed classes restrict which classes can extend them -- only classes in the same library (file) can. This lets the compiler know ALL possible subtypes and enforce exhaustive handling.
// All subtypes MUST be in the same file
sealed class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class Authenticated extends AuthState {
final User user;
Authenticated(this.user);
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
Exhaustive switch -- the compiler catches missing cases:
Widget buildAuthUI(AuthState state) {
// The compiler GUARANTEES all cases are handled.
// If you add a new AuthState subtype, every switch breaks until updated.
return switch (state) {
AuthInitial() => const WelcomeScreen(),
AuthLoading() => const CircularProgressIndicator(),
Authenticated(:final user) => HomeScreen(user: user),
AuthError(:final message) => ErrorScreen(message: message),
// NO default needed -- compiler knows this is exhaustive
};
}
Why this matters (the bug it prevents):
// BEFORE sealed classes -- the silent bug pattern:
enum AuthStatus { initial, loading, authenticated, error }
Widget buildOld(AuthStatus status) {
switch (status) {
case AuthStatus.initial: return const WelcomeScreen();
case AuthStatus.loading: return const CircularProgressIndicator();
case AuthStatus.authenticated: return const HomeScreen();
case AuthStatus.error: return const ErrorScreen();
}
// Now someone adds AuthStatus.twoFactorRequired
// This switch SILENTLY falls through or hits a default.
// No compiler error. Bug ships to production.
}
With sealed classes, adding a new subtype is a compile-time error everywhere it's not handled. This is the same safety guarantee that Rust's enum and Kotlin's sealed class provide.
Nested sealed classes for complex state hierarchies:
sealed class LoadState<T> {}
class LoadInitial<T> extends LoadState<T> {}
class LoadInProgress<T> extends LoadState<T> {}
class LoadSuccess<T> extends LoadState<T> {
final T data;
LoadSuccess(this.data);
}
sealed class LoadFailure<T> extends LoadState<T> {}
class NetworkFailure<T> extends LoadFailure<T> {
final int? statusCode;
NetworkFailure(this.statusCode);
}
class ParseFailure<T> extends LoadFailure<T> {
final String rawResponse;
ParseFailure(this.rawResponse);
}
class TimeoutFailure<T> extends LoadFailure<T> {}
Widget build(LoadState<User> state) => switch (state) {
LoadInitial() => const Placeholder(),
LoadInProgress() => const CircularProgressIndicator(),
LoadSuccess(:final data) => UserProfile(user: data),
// Must handle ALL LoadFailure subtypes -- or match on LoadFailure directly
NetworkFailure(:final statusCode) => Text('HTTP $statusCode'),
ParseFailure() => const Text('Invalid response'),
TimeoutFailure() => const Text('Request timed out'),
};
Sealed vs abstract vs enum:
-
enum: Fixed set of values, no associated data per variant. -
abstract: Open hierarchy -- anyone can extend it. No exhaustiveness. -
sealed: Closed hierarchy with associated data. Compiler-enforced exhaustiveness.
Q4. What is Impeller and why was it created?
What the interviewer is REALLY testing:
Whether you understand the shader compilation jank problem that plagued Flutter since its inception, how Impeller solves it architecturally, and the trade-offs.
Answer:
The problem Impeller solves:
Flutter's original rendering engine (Skia) compiles GPU shaders at runtime -- the first time a visual effect is drawn, Skia generates the shader program, compiles it for the specific GPU, and caches it. This compilation takes 20-200ms, causing visible jank (stuttering) the first time a user encounters an animation, transition, or complex widget.
This was Flutter's most infamous issue: "jank on first run." Users reported smooth 60fps after using the app for a while, but the first encounter with each unique visual was jerky. Warm-up strategies (like SkSL shader bundle capture) were workarounds, not solutions.
How Impeller solves it:
Impeller takes a fundamentally different approach:
All shaders are pre-compiled at build time. Impeller uses a small, known set of shader programs (unlike Skia's open-ended shader generation). These shaders are compiled for the target GPU API (Metal, Vulkan, OpenGL) during the Flutter build step.
No runtime shader compilation. Since all possible shaders are already compiled and bundled in the app binary, there is zero shader compilation during app execution. This completely eliminates first-frame jank.
Tessellation on the CPU. Complex paths are tessellated (converted to triangles) on the CPU rather than using GPU-side path rendering. This is a trade-off: slightly more CPU work, but predictable GPU performance with no surprise shader compilations.
Designed for Flutter's rendering model. Skia is a general-purpose 2D graphics library used by Chrome, Android, and many other platforms. Impeller is purpose-built for Flutter's specific rendering needs -- display lists, compositing layers, text rendering, clipping.
Architecture:
Flutter Framework
│
▼
Display List (scene description)
│
▼
Impeller HAL (Hardware Abstraction Layer)
│
├──> Metal backend (iOS, macOS)
├──> Vulkan backend (Android, Linux)
└──> OpenGL ES backend (older Android)
Trade-offs:
- Impeller uses more memory for pre-compiled shader variants than Skia's on-demand compilation.
- Some edge-case rendering features from Skia are still being ported (e.g., certain blend modes, advanced text rendering).
- On very old Android devices without Vulkan, the OpenGL ES fallback may be slower than Skia.
How to check if your app is using Impeller:
// Impeller is default on iOS since Flutter 3.16
// On Android, opt in via:
// flutter run --enable-impeller
// Or in AndroidManifest.xml:
// <meta-data
// android:name="io.flutter.embedding.android.EnableImpeller"
// android:value="true" />
The candidate-differentiating insight: "Impeller doesn't just fix jank -- it changes the performance contract of Flutter from 'eventually smooth' to 'smooth from frame one.' This matters most in first-impression scenarios like app store review, onboarding, and demo environments."
Q5. What's the status of Flutter Web and WASM support?
What the interviewer is REALLY testing:
Whether you understand the two Flutter Web renderers (HTML vs CanvasKit), why Wasm compilation is important, the SEO/accessibility limitations, and when Flutter Web is (and isn't) the right choice.
Answer:
Flutter Web has two (now three) rendering strategies:
HTML renderer: Uses standard HTML/CSS/Canvas elements. Smaller download size (~400KB). Better text rendering and accessibility. But: visual fidelity differs from mobile (some effects don't match exactly).
CanvasKit renderer: Uses Skia compiled to WebAssembly. Pixel-perfect rendering identical to mobile. But: larger download (~2MB for CanvasKit binary), slower initial load, poor SEO, accessibility requires extra work.
Wasm (new): Flutter's Dart code compiles to WebAssembly instead of JavaScript. The rendering still uses CanvasKit/Skia (or Impeller in the future), but the business logic runs as Wasm. Performance improvement: 2-3x faster execution than the JavaScript compilation for compute-heavy code.
Wasm compilation:
# Build Flutter Web with Wasm
flutter build web --wasm
# The output includes both .wasm and .js fallback
# Browser support: Chrome 119+, Firefox 120+ (WasmGC required)
# Safari: not yet supported (as of mid-2025)
When Flutter Web is the RIGHT choice:
- Internal dashboards/admin panels (no SEO needed).
- Apps that need feature parity with mobile (same codebase, identical behavior).
- Highly interactive apps (editors, design tools, data visualization).
- PWAs that complement a mobile app.
When Flutter Web is the WRONG choice:
- Content-heavy sites that need SEO (blog, e-commerce catalog, landing pages).
- Sites where accessibility is paramount (screen reader support is limited vs native HTML).
- Simple CRUD apps where React/Next.js would be faster to build and more SEO-friendly.
- Sites targeting older browsers.
The pragmatic answer in an interview: "Flutter Web is excellent for app-like experiences but poor for document-like web pages. If I'm building a web app that mirrors a mobile app -- like a trading dashboard or a collaborative editor -- Flutter Web is great. If I'm building a marketing site or a blog, I'd use Next.js."
Current limitations:
- No URL-based deeplinking into specific widget states without explicit route setup.
- Initial load time is higher than React/Angular (CanvasKit download).
- Text is not selectable by default (it's painted on a canvas, not HTML text).
- Browser dev tools don't work for inspecting Flutter widgets (must use Flutter DevTools).
Q6. How do class modifiers (base, interface, final, sealed, mixin) work in Dart 3?
What the interviewer is REALLY testing:
Whether you understand API surface control -- these modifiers let library authors restrict how their classes are used by consumers, preventing unintended subclassing or implementation.
Answer:
Dart 3 introduced class modifiers that control how a class can be subclassed or implemented outside of its defining library. Within the same library, there are no restrictions.
| Modifier | Can extend? | Can implement? | Can construct? |
|---|---|---|---|
| (none) | Yes | Yes | Yes |
base |
Yes | No | Yes |
interface |
No | Yes | Yes |
final |
No | No | Yes |
sealed |
Yes (same file) | Yes (same file) | No |
mixin |
- | Yes | No (applied with with) |
mixin class |
Yes | Yes | Yes + usable as mixin |
Why each exists:
// BASE: "You can extend me, but not implement me"
// Use when: your class has internal state/logic that MUST execute
// Prevents: mock implementations that skip validation logic
base class AuthValidator {
bool validate(String token) {
_logValidationAttempt(); // MUST execute
return _checkToken(token);
}
}
// External code CAN: class MyValidator extends AuthValidator { ... }
// External code CANNOT: class FakeValidator implements AuthValidator { ... }
// Why? Because `implements` lets you skip _logValidationAttempt()
// INTERFACE: "You can implement me, but not extend me"
// Use when: you want a contract but don't want inheritance of implementation
interface class Serializable {
Map<String, dynamic> toJson() => {};
factory Serializable.fromJson(Map<String, dynamic> json) => Serializable();
}
// External code CAN: class User implements Serializable { ... }
// External code CANNOT: class User extends Serializable { ... }
// FINAL: "You can use me, but not extend or implement me"
// Use when: your class is a complete, closed implementation
final class DatabaseConnection {
void query(String sql) { /* ... */ }
}
// External code CANNOT extend or implement this.
// Use case: prevent mocking in tests -- force integration tests, or
// prevent subclasses that might break invariants.
// SEALED: "Only I can define subclasses, and they must be in this file"
// (Covered in Q3)
sealed class Result<T> {}
class Success<T> extends Result<T> { final T value; Success(this.value); }
class Failure<T> extends Result<T> { final Exception error; Failure(this.error); }
// MIXIN CLASS: "I can be used as both a class and a mixin"
mixin class Loggable {
void log(String message) => print('[${runtimeType}] $message');
}
// Can be used as: class MyService extends Loggable { ... }
// Or as mixin: class MyService with Loggable { ... }
How this affects Flutter development:
Many Flutter framework classes are now annotated with these modifiers. For example:
-
StatelessWidgetandStatefulWidgetare not markedfinalorsealed, so you CAN extend them (as expected). - Some internal framework classes are now
finalto prevent unintended subclassing that would break in future Flutter versions.
Practical impact for app developers: For most app code, you won't use these modifiers heavily. They matter when you're writing packages/libraries that other developers consume. The main one you'll use daily is sealed for state modeling.
Q7. What is Element embedding in Flutter?
What the interviewer is REALLY testing:
Whether you understand that Flutter can be embedded WITHIN existing web pages as a component (not just as a full-page app), enabling incremental adoption.
Answer:
Element embedding (introduced in Flutter 3.10+) allows you to embed a Flutter app inside a specific HTML element on a web page, rather than taking over the entire page.
Before element embedding:
Flutter Web always took over the entire browser viewport. You couldn't put a Flutter widget alongside normal HTML/React/Angular content.
With element embedding:
<!-- Regular HTML page -->
<h1>My Website</h1>
<p>Regular HTML content...</p>
<!-- Flutter is embedded in just this div -->
<div id="flutter-chart-widget" style="width: 600px; height: 400px;"></div>
<p>More HTML content below Flutter...</p>
<script src="flutter.js"></script>
<script>
_flutter.loader.load({
onEntrypointLoaded: async function(engineInitializer) {
let appRunner = await engineInitializer.initializeEngine({
hostElement: document.getElementById('flutter-chart-widget'),
});
await appRunner.runApp();
},
});
</script>
Use cases:
- Embedding a complex interactive chart built with Flutter inside a React/Angular dashboard.
- Adding a Flutter-powered 3D viewer or custom editor to an existing web app.
- Incremental migration: replace one component at a time with Flutter, not the entire page.
How it works internally: Flutter renders into the specified host element's subtree (typically into a <canvas> inside the host element). The element's size constrains the Flutter app. Multiple Flutter instances can coexist on the same page (each in its own element), though each has its own engine overhead.
Limitations:
- Each embedded instance runs a separate Dart isolate with its own memory. They don't share state natively (use JavaScript interop
postMessageto communicate). - Each instance loads the full Flutter engine, so embedding many instances is heavy.
- Mouse/keyboard event routing between Flutter and the host page can have edge cases.
Q8. What are macros in Dart (upcoming) and how will they change code generation?
What the interviewer is REALLY testing:
Whether you understand the current pain of code generation (build_runner, freezed, json_serializable) and how macros will replace it with compile-time metaprogramming that's faster, more integrated, and doesn't produce generated files.
Answer:
The problem macros solve:
Currently, to generate code in Dart, you use build_runner:
dart run build_runner build
This produces .g.dart and .freezed.dart files. The problems:
-
Slow: Runs an entire separate build process. On large projects,
build_runnercan take minutes. -
Generated file clutter: You commit
.g.dartfiles or add them to.gitignore(both have downsides). - Fragile: Changing one model requires regenerating, which developers forget.
-
IDE disconnect: Until you run
build_runner, the IDE shows errors for missing generated code.
How macros will work:
Macros are compile-time metaprogramming. They execute during compilation and augment classes directly -- no separate build step, no generated files.
// HYPOTHETICAL (API may change before final release):
// Instead of:
@freezed
class User with _$User {
factory User({
required String name,
required int age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// + running build_runner to generate user.freezed.dart and user.g.dart
// WITH MACROS:
@JsonCodable()
class User {
final String name;
final int age;
}
// The @JsonCodable macro AUGMENTS the class at compile time with:
// - fromJson factory
// - toJson method
// No generated files. No build_runner. Instant IDE support.
How macros work internally:
- The compiler encounters
@JsonCodable()annotation. - It invokes the
JsonCodablemacro, which is Dart code that runs at compile time. - The macro introspects the class (reads fields, types) and produces augmentation code.
- The augmentation is merged into the class at compile time.
- The final compiled output includes the macro-generated code, but no intermediate files exist on disk.
Dart augmentation syntax (the mechanism macros use):
// main file
class User {
final String name;
final int age;
}
// augmentation (generated by macro, never seen as a file)
augment class User {
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
age = json['age'] as int;
Map<String, dynamic> toJson() => {'name': name, 'age': age};
}
Impact on Flutter ecosystem:
| Current (build_runner) | Future (macros) |
|---|---|
json_serializable + build_runner
|
@JsonCodable() built into Dart |
freezed + build_runner
|
@data or similar macro |
auto_route code gen |
Macro-based route generation |
injectable + build_runner
|
Macro-based DI |
retrofit + build_runner
|
Macro-based API client |
Current status (as of early 2026): Macros are still experimental. The @JsonCodable macro is available as a preview. The full macro system is under active development. Production readiness is expected in a future Dart release.
Q9. How does the new Material 3 ColorScheme.fromSeed work?
What the interviewer is REALLY testing:
Whether you understand the HCT color space, how a single seed color generates 30+ harmonious colors, and why this is better than manually picking colors.
Answer:
ColorScheme.fromSeed takes a single color and generates an entire, harmonious color scheme using the HCT color space (Hue, Chroma, Tone) developed by Google's Material Design team.
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4), // just one color
brightness: Brightness.light,
);
// This generates ALL of these automatically:
// colorScheme.primary (main brand color)
// colorScheme.onPrimary (text/icons on primary)
// colorScheme.primaryContainer (lighter variant for backgrounds)
// colorScheme.onPrimaryContainer
// colorScheme.secondary (complementary accent)
// colorScheme.onSecondary
// colorScheme.secondaryContainer
// colorScheme.onSecondaryContainer
// colorScheme.tertiary (third accent, harmonized)
// colorScheme.error / onError / errorContainer
// colorScheme.surface / onSurface
// colorScheme.surfaceContainerHighest (replaces old background)
// ... and more (30+ colors total)
How HCT works:
Traditional color spaces (RGB, HSL) don't account for perceptual uniformity. A "50% brightness yellow" looks much brighter than a "50% brightness blue" to the human eye. HCT fixes this:
- Hue: The color angle (red=0, green=120, blue=240).
- Chroma: Color intensity/saturation, but perceptually uniform.
- Tone: Perceived lightness from 0 (black) to 100 (white).
The algorithm:
- Convert the seed color to HCT.
- Generate a tonal palette -- the same hue at tones 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100.
- Generate harmonized secondary and tertiary palettes by rotating the hue.
- Map specific tones to specific roles (e.g.,
primary= tone 40 in light mode, tone 80 in dark mode).
// You can also customize individual roles:
final customScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
primary: const Color(0xFF00FF00), // override just primary
// The rest is still auto-generated from the seed
);
// Or generate from a dynamic source (e.g., album art):
Future<ColorScheme> schemeFromImage(ImageProvider image) async {
final palette = await ColorScheme.fromImageProvider(provider: image);
return palette;
}
Why this matters in practice: Before fromSeed, teams spent hours picking accessible color combinations. Now you give it your brand color and get a guaranteed-accessible, perceptually harmonious scheme. The generated on* colors always meet WCAG contrast requirements against their corresponding * colors.
Override strategy for brands:
// Start with seed, then override specific colors for brand compliance
ThemeData buildBrandTheme(Color brandPrimary, Color brandAccent) {
final scheme = ColorScheme.fromSeed(
seedColor: brandPrimary,
).copyWith(
secondary: brandAccent, // brand requirement: specific accent color
);
return ThemeData(colorScheme: scheme, useMaterial3: true);
}
Q10. What is Shorebird and how does it enable OTA updates?
What the interviewer is REALLY testing:
Whether you understand the code push / OTA update paradigm, how it works technically for AOT-compiled Flutter apps, the app store policy implications, and the trade-offs.
Answer:
Shorebird is an official code push solution for Flutter that enables over-the-air (OTA) updates to Dart code without going through app store review.
The problem it solves:
Fixing a typo, adjusting business logic, or patching a bug in a Flutter app normally requires:
- Build new binary.
- Submit to App Store / Play Store.
- Wait for review (hours to days).
- Users must update (many don't for weeks).
How Shorebird works:
┌─────────────┐ ┌────────────────┐ ┌──────────────┐
│ Developer │────>│ Shorebird │────>│ User Device │
│ pushes │ │ Cloud │ │ │
│ patch │ │ (stores │ │ App checks │
│ │ │ patches) │ │ for patches │
│ │ │ │ │ on launch │
└─────────────┘ └────────────────┘ └──────────────┘
- Initial release: Build and publish your app normally. Shorebird embeds a thin updater library.
-
Patch: Run
shorebird patchwhich compiles only the changed Dart code and uploads a diff. - Device side: On app launch, the Shorebird runtime checks for patches. If found, it downloads the diff and applies it before the Dart VM starts.
# Release a new version
shorebird release android
# Push a patch to that version
shorebird patch android
What CAN be updated via Shorebird:
- All Dart code (business logic, UI, state management).
- Asset changes (images, fonts).
What CANNOT be updated:
- Native code (Kotlin/Swift/platform channels).
- Native plugin code (if you update a plugin version that changes native code).
-
AndroidManifest.xmlorInfo.plistchanges. - New permissions.
App store policy implications:
- Google Play: Allows OTA updates for interpreted/bytecode code (Dart AOT is in a gray area, but Shorebird's approach has been accepted).
- Apple App Store: Rule 3.3.2 historically blocked code push. Shorebird works by not changing native code and not downloading executable code -- it updates the Dart AOT snapshot. Apple has not blocked Shorebird-powered apps to date, but the policy risk remains.
Technical mechanism:
Flutter compiles Dart to AOT (Ahead-of-Time) machine code. Shorebird's patching works at the level of the Dart AOT snapshot -- it diffs the old and new snapshots and transmits only the delta. On the device, the snapshot is updated before the Dart VM loads it. This is different from React Native's CodePush which updates interpreted JavaScript -- Flutter's approach is more complex because it's working with compiled code.
Trade-offs:
- Adds a dependency on Shorebird's cloud service.
- Patches are platform-specific (separate patch for Android arm64, Android arm32, iOS).
- First launch after a patch is slightly slower (patch application).
- Cannot update native code, so major changes still require store releases.
When to use Shorebird:
- Hotfixes for critical bugs.
- A/B testing UI variations.
- Updating feature flags or business rules.
- Fixing localization typos.
- NOT for major feature releases (those should go through proper store review).
SECTION 2: RAPID FIRE / ONE-LINER TRICKY QUESTIONS
These quick questions test instant, deep understanding. A candidate who truly understands Flutter will answer confidently; one who memorized docs will hesitate or give wrong answers.
Q1. Can you have a StatefulWidget without calling setState ever?
What the interviewer is REALLY testing:
Whether you understand that setState is not the only reason to use StatefulWidget. The State object provides lifecycle methods, a mutable state that persists across rebuilds, and the ability to hold controllers, subscriptions, and animation tickers.
Answer:
Yes, absolutely. Common scenarios:
-
Using a
TickerProviderStateMixinfor animations -- the AnimationController is created ininitStateand drives rebuilds viaAnimatedBuilder, notsetState. -
Holding a
TextEditingController,ScrollController, orFocusNode-- these need creation ininitStateand disposal indispose. The widget rebuilds because of parent rebuilds, notsetState. -
Listening to InheritedWidgets --
didChangeDependenciesfires when a dependency changes, which can trigger logic withoutsetState. -
Using streams or ValueListenable --
StreamBuilderorValueListenableBuilderinsidebuild()handles updates withoutsetState.
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyState();
}
class _MyState extends State<MyWidget> {
late final TextEditingController _controller;
late final StreamSubscription _sub;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_sub = someStream.listen((data) { /* process data */ });
}
@override
void dispose() {
_controller.dispose();
_sub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(controller: _controller); // no setState anywhere
}
}
Q2. Is Flutter single-threaded or multi-threaded?
What the interviewer is REALLY testing:
Whether you understand the nuance -- Dart is single-threaded (one isolate = one thread), but Flutter the framework uses multiple threads at the platform level.
Answer:
Both. Dart code runs on a single thread (the main isolate), but Flutter uses multiple platform threads:
- UI thread (Dart): Runs your Dart code, builds the widget tree, computes the layout, generates the display list. Single-threaded.
- Raster thread (GPU): Takes the display list from the UI thread and rasterizes it into pixels. Runs on a separate OS thread.
- Platform thread: Handles platform channel communication, native plugin calls, and OS events. Separate OS thread.
- I/O thread: Handles asynchronous I/O operations like file reads and network requests at the native level.
So "Flutter is single-threaded" is an oversimplification. Your Dart code is single-threaded. The rendering pipeline is multi-threaded. And you can spawn additional Dart isolates for compute-heavy work, each of which gets its own OS thread.
Q3. What's the maximum depth of the widget tree Flutter can handle?
What the interviewer is REALLY testing:
Whether you know there's no hard-coded limit, but there ARE practical limits (stack overflow from recursive build, and performance degradation).
Answer:
There is no artificial limit. Flutter doesn't enforce a maximum depth. However:
- Extremely deep trees (thousands of levels) will cause a stack overflow because
build(),createElement(), and layout calls are recursive. The OS thread stack is typically 1-8MB. - Performance degrades with unnecessary depth because each level involves method calls for build, layout, and paint.
- In practice, real-world apps rarely exceed 50-100 levels of depth, and the framework handles this effortlessly.
The real concern isn't depth but breadth (too many children at one level) and unnecessary nesting (wrapping a widget in 5 Padding/Container layers when one would do).
Q4. Can you use Flutter without MaterialApp or CupertinoApp?
What the interviewer is REALLY testing:
Whether you understand what MaterialApp actually provides and whether it's truly required.
Answer:
Yes. MaterialApp and CupertinoApp are convenience widgets that bundle:
Navigator-
Theme/CupertinoTheme MediaQueryLocalizationsDefaultTextStyleOverlay
You can use WidgetsApp (which both extend) or even no app widget at all:
void main() {
runApp(
const Directionality(
textDirection: TextDirection.ltr,
child: Text('Hello, raw Flutter!'),
),
);
}
This runs. No MaterialApp, no CupertinoApp. But you lose theming, navigation, media queries, localization, and accessibility support. For games or highly custom UIs, some developers use WidgetsApp directly with only the features they need.
Q5. What happens if build() returns null?
What the interviewer is REALLY testing:
Whether you know that build() CANNOT return null. Its return type is Widget, not Widget?.
Answer:
It won't compile. The build() method's return type is Widget (non-nullable since Dart null safety). The Dart compiler rejects return null; at compile time.
If you want to render "nothing," return const SizedBox.shrink() or const SizedBox(). These are zero-sized widgets that occupy no space. Some developers use const Placeholder() during development, but SizedBox.shrink() is the production convention.
Note: Container() without arguments also renders as an empty box, but it's less semantically clear and has extra overhead (it's actually a combination of multiple widgets internally).
Q6. Can a StatelessWidget have mutable state?
What the interviewer is REALLY testing:
Whether you understand that "stateless" means "no mutable state managed by the framework" -- but technically, nothing stops you from putting mutable fields in a StatelessWidget. It's a design violation, not a compile error.
Answer:
Technically yes, practically it's a bug. Nothing in the language prevents this:
class BrokenWidget extends StatelessWidget {
int counter = 0; // mutable field in a StatelessWidget
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
counter++; // mutates, but build() is never re-called
print(counter); // prints 2, 3, 4... but UI shows stale value
},
child: Text('$counter'),
);
}
}
The counter increments in memory, but the UI never updates because there's no setState() mechanism. The framework doesn't re-call build() when counter changes.
Additionally, Flutter may reuse or recreate StatelessWidget instances at any time, so the mutable state can be silently lost. The build() method must be a pure function of context and the widget's constructor parameters.
Q7. What's the difference between Visibility and Offstage?
What the interviewer is REALLY testing:
Whether you know the subtle but critical layout difference between these two.
Answer:
Visibility (visible: false) |
Offstage (offstage: true) |
|
|---|---|---|
| Painted? | No | No |
| Takes up space? | Yes (by default) | No |
| Hit testable? | No | No |
| Child's build() runs? | Yes | Yes |
| Child participates in layout? | Yes | No |
// Visibility: hides the widget but preserves its space in the layout
Visibility(
visible: false,
child: Text('Hidden'), // space is reserved, like CSS visibility: hidden
)
// Offstage: completely removes from layout, like CSS display: none
Offstage(
offstage: true,
child: Text('Gone'), // no space reserved
)
The critical detail: both still build the child widget and its entire subtree. If you want to avoid building the child entirely, use a conditional:
if (isVisible) Text('Only built when visible')
Visibility has a maintainState, maintainAnimation, maintainSize, and maintainInteractivity parameter for fine-grained control. Offstage is simpler -- it's just "in layout or not."
Q8. Can you animate between two completely different widgets?
What the interviewer is REALLY testing:
Whether you know about AnimatedSwitcher and Hero, and understand that Flutter can cross-fade or transition between widgets even if they have completely different types.
Answer:
Yes, using AnimatedSwitcher:
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _showFirst
? const Icon(Icons.check, key: ValueKey('check'))
: const Text('Hello', key: ValueKey('text')),
)
When the child changes (detected by key), AnimatedSwitcher cross-fades between the old and new child. The widgets can be completely different types (an Icon and a Text, or an Image and a ListView).
You can customize the transition:
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: _currentWidget,
)
For even more control, use AnimatedCrossFade (explicitly between exactly two children) or build custom transitions with AnimationController.
For shared element transitions across screens, use Hero.
Q9. Does Flutter support 120fps?
What the interviewer is REALLY testing:
Whether you know that Flutter can render at the device's native refresh rate, and the nuances involved.
Answer:
Yes. Flutter automatically renders at the device's native refresh rate. On a 120Hz display (like recent iPhones and many Android flagships), Flutter targets 120fps.
How it works:
- Flutter's engine subscribes to the platform's
vsyncsignal. - On a 120Hz display, vsync fires every ~8.3ms (instead of ~16.7ms for 60Hz).
- Flutter's scheduler attempts to complete build + layout + paint within each vsync interval.
Caveats:
- Your frame budget drops from 16.7ms to 8.3ms. Code that was fine at 60fps may cause jank at 120fps.
-
AnimationControlleradapts automatically --vsyncparameter fromTickerProviderensures animations are tied to the display refresh rate. - On Android, apps may need to opt into high refresh rates in some cases (depends on OEM implementation).
Q10. Can you run Flutter without a device/emulator?
What the interviewer is REALLY testing:
Whether you know about Flutter's test framework, flutter test, and headless rendering.
Answer:
Yes, in multiple ways:
-
flutter test-- runs unit and widget tests without a device. Widget tests use a virtual rendering environment. -
flutter run -d web-server-- runs in a browser (no physical device). -
flutter run -d linux/windows/macos-- runs as a desktop app on your development machine. -
Dart-only tests --
dart testruns pure Dart unit tests without any Flutter infrastructure.
Widget tests are particularly interesting: Flutter creates a fake rendering environment with a configurable screen size, and you can pump frames, tap widgets, and assert on the rendered output -- all without a single pixel being displayed on screen.
Q11. What is tree shaking in Flutter context?
What the interviewer is REALLY testing:
Whether you understand dead code elimination in Dart's AOT compiler and how it affects app size.
Answer:
Tree shaking is dead code elimination. Dart's AOT compiler analyzes the entire call graph starting from main() and removes any code that is unreachable. This includes:
- Unused classes, methods, and functions.
- Unused parts of imported packages (import
package:foobut only use one class -- the rest is removed). - Code behind
kReleaseMode/kDebugModechecks.
// This assert and its closure body are tree-shaken in release builds
assert(() {
debugPrint('Expensive debug-only validation');
return true;
}());
Flutter-specific tree shaking:
- Unused Material icons are removed (but only if you import specific icons, not the whole icon font).
-
dart:mirrorsis not available in Flutter (no reflection), partly because it would defeat tree shaking. - Packages like
font_awesome_fluttercan bloat app size if they include all icons; good packages support tree shaking by using individual icon references.
Impact: A typical Flutter app that imports dozens of packages but uses only a fraction of their code ships a binary that contains only the used code. This is why Flutter apps are ~5-10MB for a basic app despite the rich framework.
Q12. Can you have circular dependencies in Dart?
What the interviewer is REALLY testing:
Whether you know that Dart ALLOWS circular imports between files (unlike some languages) but circular CONSTRUCTOR dependencies will cause a stack overflow.
Answer:
Circular imports between files: Allowed. Dart resolves them.
// file_a.dart
import 'file_b.dart';
class A { B? b; }
// file_b.dart
import 'file_a.dart';
class B { A? a; }
This compiles and runs fine.
Circular package dependencies: Not allowed. pub will reject a pubspec.yaml that creates a circular dependency between packages.
Circular constructor/initialization dependencies: Cause a stack overflow at runtime.
class A {
final B b;
A() : b = B(); // creates B, which creates A, which creates B...
}
class B {
final A a;
B() : a = A(); // infinite recursion -> stack overflow
}
This compiles but crashes at runtime. The solution is to break the cycle with lazy initialization, dependency injection, or making one reference nullable.
Q13. What happens if pubspec.yaml has conflicting dependency versions?
What the interviewer is REALLY testing:
Whether you understand Dart's version resolution algorithm and the difference between resolvable conflicts (version ranges overlap) and unresolvable conflicts (ranges are disjoint).
Answer:
Dart's pub solver uses a version resolution algorithm that finds a set of package versions satisfying all constraints simultaneously.
Resolvable conflict (pub finds a middle ground):
# Your app
dependencies:
package_a: ^2.0.0 # needs http >=0.13.0 <1.0.0
package_b: ^1.5.0 # needs http >=0.13.0 <2.0.0
# pub resolves: http 0.13.x (satisfies both)
Unresolvable conflict:
# Your app
dependencies:
package_a: ^2.0.0 # needs http ^0.13.0
package_b: ^1.5.0 # needs http ^1.0.0 (different major version)
# pub FAILS with:
# Because package_a depends on http ^0.13.0 and package_b depends on http ^1.0.0,
# package_a is incompatible with package_b.
Solutions for unresolvable conflicts:
- Upgrade one of the packages to a version that uses the compatible dependency.
-
Use
dependency_overrides(forces a specific version, but may cause runtime errors if APIs changed):
dependency_overrides:
http: ^1.0.0
- Fork and update the outdated package.
Q14. Can you use both Navigator 1.0 and 2.0 in the same app?
What the interviewer is REALLY testing:
Whether you understand that Navigator 2.0 (Router API) is built ON TOP of Navigator 1.0, not a replacement.
Answer:
Yes, and most apps do. Navigator 2.0 (the Router + RouteInformationParser + RouterDelegate API, used by go_router, auto_route, etc.) still uses Navigator 1.0 widgets internally. They're layers, not alternatives.
A common pattern:
- Use
go_router(Navigator 2.0) for top-level routing (tabs, main screens). - Use
Navigator.of(context).push()(Navigator 1.0) for modals, dialogs, and sub-flows within a screen.
// go_router handles top-level routes
GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
],
)
// Inside HomeScreen, use Navigator 1.0 for a modal
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DetailModal()),
);
They coexist because GoRouter's RouterDelegate creates and manages a Navigator widget internally. Your imperative push() calls work on that same Navigator.
Q15. Is there a limit to how many providers you can nest?
What the interviewer is REALLY testing:
Whether you understand that MultiProvider is syntactic sugar for nested providers, and whether you've encountered the practical performance implications.
Answer:
No hard limit. MultiProvider from package:provider is syntactic sugar that unfolds to nested Provider widgets. 10, 50, or 100 providers all work.
// This:
MultiProvider(
providers: [
Provider<A>(create: (_) => A()),
Provider<B>(create: (_) => B()),
Provider<C>(create: (_) => C()),
],
child: const App(),
)
// Becomes this internally:
Provider<A>(
create: (_) => A(),
child: Provider<B>(
create: (_) => B(),
child: Provider<C>(
create: (_) => C(),
child: const App(),
),
),
)
Practical concerns at scale:
- Each
Provideradds a level to the widget tree and anInheritedWidget. Looking up a provider viacontext.watch<T>()walks up the tree to find the nearestInheritedWidgetof typeT. This is O(depth) per lookup, but the constant factor is tiny. - The real performance concern isn't nesting depth but unnecessary rebuilds -- a provider near the root that changes frequently will rebuild a large subtree.
- Riverpod avoids this entirely by using a flat container outside the widget tree.
Q16. What happens if you call setState in initState?
What the interviewer is REALLY testing:
Whether you know the framework's lifecycle timing and whether this crashes.
Answer:
It works, but it's pointless. Calling setState() in initState() doesn't crash because the framework marks the element as dirty, and it's already going to build for the first time. The setState call queues a rebuild, but the first build hasn't happened yet, so the rebuild is coalesced into the initial build.
@override
void initState() {
super.initState();
setState(() {
_counter = 42; // works, but unnecessary
});
// Equivalent to just:
// _counter = 42;
// because initState runs before the first build() anyway
}
However, calling setState in initState with a delayed callback IS meaningful:
@override
void initState() {
super.initState();
Future.microtask(() {
setState(() => _isReady = true); // this triggers a SECOND build
});
}
Here, the microtask runs after the first build completes, and setState correctly triggers a rebuild. This pattern is sometimes used to trigger a post-first-frame animation.
Q17. Can a widget appear twice in the widget tree?
What the interviewer is REALLY testing:
Whether you understand the difference between the widget instance and the element. Widget objects can be referenced from multiple places, but elements (and their state) have a 1:1 relationship with their position in the tree.
Answer:
The same widget instance can appear multiple times:
const myText = Text('Hello');
Column(
children: [
myText, // same instance
myText, // same instance, different position -> different Element
],
)
This works. Flutter creates two separate Element objects for the two positions, even though they reference the same Widget object. Each Element has its own lifecycle.
But a GlobalKey widget CANNOT appear twice:
final key = GlobalKey();
Column(
children: [
Container(key: key),
Container(key: key), // CRASH: Duplicate GlobalKey
],
)
GlobalKey enforces uniqueness in the tree. This crashes with "Multiple widgets used the same GlobalKey."
StatefulWidget subtlety: If the same StatefulWidget instance appears in two positions, each position gets its own State object. The State is tied to the Element (position), not the Widget (configuration).
Q18. What is the raster thread in Flutter?
What the interviewer is REALLY testing:
Whether you understand Flutter's multi-threaded rendering pipeline beyond the single-threaded Dart story.
Answer:
The raster thread (previously called the "GPU thread") takes the layer tree (display list) produced by the UI thread's paint phase and converts it into GPU commands that produce actual pixels on screen.
Pipeline:
UI Thread (Dart) Raster Thread (C++)
───────────────── ────────────────────
build()
layout()
paint() → Layer Tree ──> Rasterize layers
Composite
Submit to GPU
→ Pixels on screen
Why it matters for performance:
- The UI thread and raster thread run in parallel. If your Dart code takes 10ms and rasterization takes 8ms, the total frame time is 10ms (they overlap), not 18ms.
- Jank happens when either thread exceeds the frame budget. The Performance overlay shows two bars: UI thread (top) and raster thread (bottom). A red bar in either one means a dropped frame.
- Heavy painting (complex shadows, clip paths, lots of
saveLayer) bogs down the raster thread even if your Dart code is fast. -
RepaintBoundaryhelps by caching rasterized layers so they don't need to be re-rasterized every frame.
Q19. What's the difference between paint and compositing in Flutter rendering?
What the interviewer is REALLY testing:
Whether you understand the full rendering pipeline: build -> layout -> paint -> compositing, and why compositing exists as a separate step.
Answer:
Paint is the process of recording drawing commands (draw a rectangle, draw text, draw an image) into Layer objects. When a RenderObject.paint() is called, it uses the Canvas API to record commands. No pixels are produced yet -- it's just a list of instructions.
Compositing is the process of assembling these layers into a final scene. Layers are like transparent sheets stacked on top of each other. Compositing determines:
- The z-order (which layer is on top).
- How layers are blended (opacity, blend modes).
- Which layers are cached and which need re-painting.
- How to handle hardware-accelerated operations (transforms, opacity) that the GPU can do directly on layers without re-rasterizing.
Why the distinction matters:
// This creates a compositing layer (cheap to animate):
Opacity(opacity: _value, child: HeavyWidget())
// The HeavyWidget is painted once into a layer.
// On each frame, only the opacity compositing parameter changes.
// The GPU applies opacity without re-painting HeavyWidget.
// Versus this (re-paints on every frame):
CustomPaint(
painter: MyPainter(_value), // changes every frame
// re-records all drawing commands every frame
)
RepaintBoundary forces a new compositing layer. Everything inside it can be cached as a rasterized texture and composited without re-painting. This is why placing RepaintBoundary around expensive-to-paint subtrees improves performance -- the raster thread reuses the cached texture.
The saveLayer trap: Calling Canvas.saveLayer() (which Opacity, ClipRRect, ColorFiltered and others do internally) creates an offscreen buffer that is composited later. Each saveLayer is expensive because it allocates GPU memory and requires an extra compositing pass. Nesting multiple widgets that each use saveLayer (like nested Opacity widgets) causes a performance cliff.
Q20. Can you catch all errors in a Flutter app globally?
What the interviewer is REALLY testing:
Whether you know the three error-catching zones (Zone, FlutterError, PlatformDispatcher) and their coverage, plus the fact that some errors are truly uncatchable.
Answer:
Almost all, using three mechanisms together:
void main() {
// 1. Zone: catches uncaught async errors from Dart code
runZonedGuarded(
() {
WidgetsFlutterBinding.ensureInitialized();
// 2. FlutterError.onError: catches framework errors (build, layout, paint)
FlutterError.onError = (details) {
// Log to crash reporting
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
};
// 3. PlatformDispatcher.onError: catches errors from platform channels
// and errors that escape both Zone and FlutterError
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack);
return true; // true = handled, don't propagate
};
runApp(const MyApp());
},
(error, stack) {
// Uncaught async errors (e.g., unhandled Future rejections)
FirebaseCrashlytics.instance.recordError(error, stack);
},
);
}
What these three DON'T catch:
- Native crashes (segfaults, Kotlin/Swift exceptions in native plugin code). These are caught by the OS crash reporter, not Dart.
- Errors in the engine itself (Skia/Impeller crashes). These are native crashes.
- ANR (Application Not Responding) events. These are OS-level timeouts, not exceptions.
For complete coverage, you also need:
- Firebase Crashlytics NDK for native crash reporting on Android.
- PLCrashReporter or equivalent for native crashes on iOS.
-
Isolate.current.addErrorListenerif you spawn isolates -- each isolate has its own error zone.
The practical conclusion: The three mechanisms above catch ~99% of errors a Flutter developer encounters. The remaining 1% are native-level crashes that require native crash reporting tools.
Congratulations -- You Made It!
If you have followed this series from Part 1 through Part 14, you have now covered over 400 Flutter and Dart interview questions spanning everything from basics to system design, from Dart language internals to the latest Flutter 3.x features.
Go ace that interview. You are ready.
Top comments (0)