DEV Community

Cover image for Flutter Interview Questions Part 2: UI Widgets & Layouts
Anurag Dubey
Anurag Dubey

Posted on

Flutter Interview Questions Part 2: UI Widgets & Layouts

Welcome to Part 2 of the Flutter Interview Questions series! Now that you have the Dart and Flutter fundamentals from Part 1, it is time to master the UI layer. This part covers everything you need to know about building beautiful, responsive Flutter interfaces from basic layout widgets like Row and Column to advanced topics like Slivers, CustomPainter, and responsive design patterns. This is part 2 of a 14-part series. If you have not read Part 1, start there first.

What's in this part?

  • Layout widgets: Row, Column, Stack, Wrap, Flex, Expanded, Flexible, Container, SizedBox
  • Scrolling widgets: ListView, GridView, CustomScrollView, Slivers
  • Navigation: Navigator 1.0, Navigator 2.0, named routes, go_router, deep linking
  • Forms and input: TextField, TextFormField, validation, FocusNode
  • Buttons: ElevatedButton, TextButton, OutlinedButton, IconButton, FAB, InkWell vs GestureDetector
  • Dialogs, BottomSheets, SnackBars, Drawers
  • Themes and styling: ThemeData, ColorScheme, Material 3, TextTheme, dark mode
  • Custom painting: CustomPainter, Canvas, Paint, ClipPath
  • Responsive design: MediaQuery, LayoutBuilder, OrientationBuilder, breakpoints
  • Images and assets: Image widget, caching, SVG, performance optimization

1. Layout Widgets (Row, Column, Stack, Wrap, Flex, Expanded, Flexible, SizedBox, Container)

Q1. What is the difference between Row and Column in Flutter?

Answer:
Row and Column are both flex-based layout widgets that arrange children linearly. The only difference is the axis:

  • Row arranges children horizontally (left to right). Its main axis is horizontal and cross axis is vertical.
  • Column arranges children vertically (top to bottom). Its main axis is vertical and cross axis is horizontal.

Both share the same properties: mainAxisAlignment, crossAxisAlignment, mainAxisSize, and children. They do not scroll by default -- if children overflow, you get a yellow-black striped overflow error. To fix overflow, wrap overflowing children in Expanded or Flexible, or use a scrollable widget.

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [Icon(Icons.star), Icon(Icons.star), Icon(Icons.star)],
)

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [Text('Title'), Text('Subtitle')],
)
Enter fullscreen mode Exit fullscreen mode

Q2. What is the difference between Expanded and Flexible?

Answer:
Both Expanded and Flexible are used inside Row, Column, or Flex to control how a child widget takes up remaining space.

  • Flexible allows its child to be at most as large as the available space but can be smaller. It uses FlexFit.loose by default.
  • Expanded is a shorthand for Flexible(fit: FlexFit.tight). It forces its child to fill all the available space allocated to it.

The flex property on both determines the ratio of space distribution. A child with flex: 2 gets twice as much space as a child with flex: 1.

Row(
  children: [
    Expanded(flex: 2, child: Container(color: Colors.red)),   // 2/3 of space
    Expanded(flex: 1, child: Container(color: Colors.blue)),  // 1/3 of space
  ],
)

Row(
  children: [
    Flexible(child: Container(color: Colors.red, width: 100)),
    // Takes up to available space but can be smaller (100px here)
  ],
)
Enter fullscreen mode Exit fullscreen mode

Key point: Expanded = must fill space. Flexible = can fill space but doesn't have to.


Q3. Explain the Stack widget and when you would use it.

Answer:
Stack allows you to overlay widgets on top of each other. Children are painted in order, with the first child at the bottom and the last child on top. It is used when you need overlapping UI elements -- such as a text label on top of an image, a badge on an icon, or a floating overlay.

Key properties:

  • alignment: Controls where non-positioned children are placed (default: AlignmentDirectional.topStart).
  • fit: Determines how non-positioned children are sized. StackFit.loose gives them the constraints of the Stack; StackFit.expand forces them to fill the Stack.
  • clipBehavior: Whether to clip children that overflow the Stack bounds.

Use Positioned widget to place children at specific coordinates within the Stack.

Stack(
  alignment: Alignment.center,
  children: [
    Image.asset('background.jpg'),
    Positioned(
      bottom: 16,
      right: 16,
      child: Text('Overlay Text', style: TextStyle(color: Colors.white)),
    ),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q4. What is the Wrap widget and how does it differ from Row?

Answer:
Wrap is like a Row (or Column) that automatically wraps children to the next line when there is not enough space on the current line. With Row, if the children exceed the available horizontal space, you get an overflow error. Wrap handles this gracefully by flowing content to the next row.

Key properties:

  • direction: Axis.horizontal (default) or Axis.vertical.
  • spacing: Gap between children in the main axis.
  • runSpacing: Gap between lines (runs) in the cross axis.
  • alignment: How children are aligned within a run.
  • runAlignment: How the runs themselves are aligned in the cross axis.

Common use case: displaying a list of tags, chips, or filter options.

Wrap(
  spacing: 8.0,
  runSpacing: 4.0,
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Material Design')),
    Chip(label: Text('Widgets')),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q5. What is the Container widget and what are its most important properties?

Answer:
Container is a convenience widget that combines common painting, positioning, and sizing functionality. It is one of the most frequently used widgets.

Key properties:

  • child: The widget inside the container.
  • padding: Space inside the container, between the border and the child.
  • margin: Space outside the container, between the container and its parent.
  • decoration: A BoxDecoration for background color, border, border radius, gradient, box shadow, image, and shape.
  • color: Background color (shorthand; cannot be used with decoration).
  • width / height: Fixed dimensions.
  • constraints: Minimum and maximum width/height via BoxConstraints.
  • alignment: How to align the child within the container.
  • transform: A Matrix4 to apply a transformation (rotate, scale, translate).
Container(
  width: 200,
  height: 100,
  margin: EdgeInsets.all(16),
  padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2))],
  ),
  child: Text('Hello', style: TextStyle(color: Colors.white)),
)
Enter fullscreen mode Exit fullscreen mode

Important: A Container with no child tries to be as big as possible. A Container with a child sizes itself to the child.


Q6. What is SizedBox and how does it differ from Container?

Answer:
SizedBox is a simple widget that forces its child to have a specific width and/or height. It is more lightweight than Container because it has no decoration, padding, margin, or transform -- it only deals with sizing.

Common uses:

  1. Fixed-size box: SizedBox(width: 100, height: 50, child: ...).
  2. Spacing in Row/Column: SizedBox(width: 16) or SizedBox(height: 16) as a gap between children.
  3. Expand to fill: SizedBox.expand() makes the child fill all available space.
  4. Shrink to nothing: SizedBox.shrink() creates a zero-size box.
Column(
  children: [
    Text('First item'),
    SizedBox(height: 20), // 20px vertical gap
    Text('Second item'),
  ],
)
Enter fullscreen mode Exit fullscreen mode

When to use which: Use SizedBox for simple sizing or spacing. Use Container when you need decoration, padding, margin, or transforms.


Q7. Explain Flutter's layout constraint system. What does "constraints go down, sizes go up, parent sets position" mean?

Answer:
This is the fundamental rule of Flutter layout. The process works in three steps:

  1. Constraints go down: A parent widget passes constraints (min/max width and height) to its child, telling the child the range of sizes it is allowed to be.
  2. Sizes go up: The child chooses its own size within those constraints and reports it back to the parent.
  3. Parent sets position: The parent decides where to place the child based on the child's size and the parent's alignment/positioning logic.

A BoxConstraints object has four values: minWidth, maxWidth, minHeight, maxHeight. A "tight" constraint is when min equals max, forcing the child to be exactly that size. A "loose" constraint has min of 0, giving the child freedom.

This is why you sometimes see unexpected behavior: a widget that "ignores" the size you give it is usually being overridden by a tight constraint from its parent. Understanding this model is the key to debugging layout issues.

// Example: ConstrainedBox wrapping a Container
ConstrainedBox(
  constraints: BoxConstraints(maxWidth: 200, maxHeight: 100),
  child: Container(color: Colors.red), // Will be at most 200x100
)
Enter fullscreen mode Exit fullscreen mode

Q8. What is the Flex widget? How does it relate to Row and Column?

Answer:
Flex is the base class for both Row and Column. It takes a required direction parameter (Axis.horizontal or Axis.vertical) that determines the main axis.

  • Row is essentially Flex(direction: Axis.horizontal, ...).
  • Column is essentially Flex(direction: Axis.vertical, ...).

You would use Flex directly when the axis direction needs to be dynamic -- for example, switching between horizontal and vertical layout based on screen orientation or a runtime condition.

Flex(
  direction: isLandscape ? Axis.horizontal : Axis.vertical,
  children: [
    Expanded(child: Widget1()),
    Expanded(child: Widget2()),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q9. What is mainAxisSize and when would you change it?

Answer:
mainAxisSize controls how much space a Row or Column occupies along its main axis.

  • MainAxisSize.max (default): The Row/Column takes up all available space along the main axis.
  • MainAxisSize.min: The Row/Column shrinks to be only as large as the total size of its children.

Changing it to min is useful when you want a Row or Column to wrap tightly around its children instead of stretching to fill the parent. This is common inside a Card, Dialog, or any situation where you want the container to hug its content.

Row(
  mainAxisSize: MainAxisSize.min, // Shrink-wrap the children
  children: [
    Icon(Icons.star),
    SizedBox(width: 4),
    Text('Favorite'),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q10. How do you handle overflow in Row and Column?

Answer:
When children exceed available space, Flutter shows a yellow-and-black striped overflow warning. Solutions include:

  1. Use Expanded or Flexible to make children share available space proportionally.
  2. Wrap with SingleChildScrollView (with scrollDirection matching the axis) to allow scrolling.
  3. Use Wrap instead to automatically flow children to the next line.
  4. Use Overflow / clipBehavior to clip overflowing content visually (though the content is still lost).
  5. Use FittedBox to scale down content to fit.
  6. Use Expanded with Text + overflow: TextOverflow.ellipsis for text truncation.
Row(
  children: [
    Expanded(
      child: Text(
        'Very long text that might overflow the row',
        overflow: TextOverflow.ellipsis,
      ),
    ),
    Icon(Icons.chevron_right),
  ],
)
Enter fullscreen mode Exit fullscreen mode

2. Scrolling Widgets (ListView, GridView, SingleChildScrollView, CustomScrollView, Slivers)

Q1. What are the different constructors of ListView and when should you use each?

Answer:
ListView has four constructors:

  1. ListView(children: [...]) -- Takes an explicit list of widgets. Builds all children at once. Use only for small, fixed lists (under ~20 items). Simple but not performant for large lists.

  2. ListView.builder(itemBuilder, itemCount) -- Builds items lazily on demand as the user scrolls. Only the visible items (plus a few extra) are in memory. Use for large or infinite lists. This is the most commonly used constructor.

  3. ListView.separated(itemBuilder, separatorBuilder, itemCount) -- Like builder but also takes a separatorBuilder to insert dividers or spacing between items. Perfect for lists with dividers.

  4. ListView.custom(childrenDelegate) -- Takes a SliverChildDelegate for complete control over child creation. Rarely used directly.

// Large list -- use builder
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
)

// List with dividers
ListView.separated(
  itemCount: 50,
  itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
  separatorBuilder: (context, index) => Divider(),
)
Enter fullscreen mode Exit fullscreen mode

Q2. What is the difference between ListView and SingleChildScrollView with a Column?

Answer:

Feature ListView.builder SingleChildScrollView + Column
Lazy loading Yes -- only builds visible items No -- builds ALL children at once
Performance Excellent for large lists Poor for large lists, fine for small content
Separator support Yes (via ListView.separated) Manual
Use case Homogeneous, large/dynamic lists Small content that might overflow screen

SingleChildScrollView is ideal when you have a finite amount of content (like a form) that might not fit on screen. It renders everything at once.

ListView.builder is ideal when you have many items (potentially thousands) because it only builds and keeps in memory the items currently visible on screen, plus a small buffer.

Common mistake: Putting a ListView inside a Column without wrapping it in Expanded or giving it a fixed height. Since both try to be as tall as possible, you get an unbounded height error.

// Correct: ListView inside a Column
Column(
  children: [
    Text('Header'),
    Expanded(
      child: ListView.builder(
        itemCount: 100,
        itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
      ),
    ),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q3. Explain GridView and its different constructors.

Answer:
GridView displays items in a 2D scrollable grid. It has several constructors:

  1. GridView.count -- Creates a grid with a fixed number of columns (crossAxisCount). Simple and straightforward.

  2. GridView.extent -- Creates a grid where each item has a maximum cross-axis extent. Flutter calculates how many columns fit.

  3. GridView.builder -- Lazily builds items on demand. Takes a gridDelegate (either SliverGridDelegateWithFixedCrossAxisCount or SliverGridDelegateWithMaxCrossAxisExtent) and an itemBuilder. Best for large datasets.

  4. GridView.custom -- Full control with a SliverChildDelegate and SliverGridDelegate.

// Fixed 3 columns
GridView.count(
  crossAxisCount: 3,
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  children: List.generate(20, (i) => Card(child: Center(child: Text('$i')))),
)

// Items with max width of 150px
GridView.extent(
  maxCrossAxisExtent: 150,
  children: [...],
)

// Lazy builder for large grids
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 3 / 2,
  ),
  itemCount: 100,
  itemBuilder: (context, index) => Card(child: Center(child: Text('$index'))),
)
Enter fullscreen mode Exit fullscreen mode

Q4. What is CustomScrollView and why would you use it?

Answer:
CustomScrollView is a scroll view that lets you combine multiple scrollable areas and effects using slivers. It is the most flexible scrolling widget in Flutter.

You would use it when you need:

  • A collapsible/expandable app bar (SliverAppBar).
  • Multiple lists or grids in a single scrollable area.
  • Mixing different types of scrollable content (a grid followed by a list).
  • Sticky headers, pull-to-refresh with custom effects, etc.

Everything inside a CustomScrollView must be a Sliver widget.

CustomScrollView(
  slivers: [
    SliverAppBar(
      expandedHeight: 200,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(title: Text('My App')),
    ),
    SliverGrid(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
      delegate: SliverChildBuilderDelegate(
        (context, index) => Card(child: Center(child: Text('Grid $index'))),
        childCount: 6,
      ),
    ),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('List item $index')),
        childCount: 20,
      ),
    ),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q5. What are Slivers? Explain SliverList, SliverGrid, SliverAppBar, and SliverToBoxAdapter.

Answer:
Slivers are the low-level building blocks of scrollable areas in Flutter. They implement a special sliver protocol that provides efficient lazy rendering and scroll effects. All scrollable widgets in Flutter (ListView, GridView) are built on slivers internally.

Key sliver widgets:

  • SliverList -- A sliver that displays a linear list of children. Takes a delegate (usually SliverChildBuilderDelegate or SliverChildListDelegate).

  • SliverGrid -- A sliver that displays a 2D grid. Takes both a delegate and a gridDelegate.

  • SliverAppBar -- A material design app bar that can expand, collapse, float, and pin as the user scrolls. Key properties:

    • expandedHeight: Maximum height when fully expanded.
    • pinned: Stays visible at the top when scrolled.
    • floating: Reappears immediately when scrolling up.
    • snap: Works with floating to fully animate in/out.
    • flexibleSpace: Usually a FlexibleSpaceBar with a background image or title.
  • SliverToBoxAdapter -- Wraps a regular (non-sliver) widget so it can be placed inside a CustomScrollView. Use this for any widget that is not a sliver.

  • SliverPadding -- Adds padding around a sliver.

  • SliverFillRemaining -- Fills the remaining space in the viewport. Useful for empty state messages or footers.

CustomScrollView(
  slivers: [
    SliverAppBar(expandedHeight: 200, pinned: true,
      flexibleSpace: FlexibleSpaceBar(title: Text('Title'), background: Image.network(url, fit: BoxFit.cover)),
    ),
    SliverToBoxAdapter(child: Padding(padding: EdgeInsets.all(16), child: Text('Section Header'))),
    SliverList(delegate: SliverChildBuilderDelegate((ctx, i) => ListTile(title: Text('Item $i')), childCount: 20)),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q6. What is shrinkWrap in ListView and when should you use it (and when not)?

Answer:
shrinkWrap: true makes a ListView size itself based on the total size of its children instead of taking up all available space.

When to use:

  • When a ListView is nested inside another scrollable or inside a Column and you do NOT want it to be scrollable independently.
  • When you want the ListView to be only as tall as its content.

When NOT to use:

  • For large lists. With shrinkWrap: true, the ListView must measure all children to determine its own size, which defeats the purpose of lazy loading. This causes serious performance issues with long lists.

Better alternatives for nesting:

  • Wrap with Expanded inside a Column.
  • Use CustomScrollView with slivers to combine multiple scrollable areas.
  • Use NeverScrollableScrollPhysics() with shrinkWrap: true if you must nest.
// Nesting ListView in Column (acceptable for small lists only)
Column(
  children: [
    ListView(
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      children: [Text('A'), Text('B'), Text('C')],
    ),
  ],
)

// Better approach: use CustomScrollView
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(child: Text('Header')),
    SliverList(delegate: SliverChildBuilderDelegate(...)),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Q7. What is the ScrollController and how do you use it?

Answer:
ScrollController allows you to programmatically control and listen to scroll events of a scrollable widget.

Common use cases:

  • Scroll to a position: controller.animateTo(offset) or controller.jumpTo(offset).
  • Scroll to top/bottom: controller.animateTo(0) or controller.animateTo(controller.position.maxScrollExtent).
  • Listen to scroll position: controller.addListener(callback) or controller.offset to get current position.
  • Detect if user scrolled to bottom: For infinite scrolling / pagination.
class _MyListState extends State<MyList> {
  final ScrollController _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) {
        // Near bottom -- load more data
        _loadMore();
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _controller,
      itemCount: items.length,
      itemBuilder: (context, index) => ListTile(title: Text(items[index])),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Important: Always dispose the ScrollController in dispose() to avoid memory leaks.


Q8. How do you implement infinite scrolling / pagination in Flutter?

Answer:
There are several approaches:

Approach 1: ScrollController listener
Listen for when the user scrolls near the bottom and trigger loading more data.

_controller.addListener(() {
  if (_controller.position.pixels >= _controller.position.maxScrollExtent * 0.9) {
    loadNextPage();
  }
});
Enter fullscreen mode Exit fullscreen mode

Approach 2: NotificationListener
Wrap the ListView with NotificationListener<ScrollNotification> to intercept scroll events.

NotificationListener<ScrollNotification>(
  onNotification: (notification) {
    if (notification is ScrollEndNotification &&
        notification.metrics.extentAfter < 100) {
      loadNextPage();
      return true;
    }
    return false;
  },
  child: ListView.builder(...),
)
Enter fullscreen mode Exit fullscreen mode

Approach 3: Packages
Use packages like infinite_scroll_pagination which handle loading indicators, error states, and edge cases.

In all approaches, you typically show a CircularProgressIndicator at the bottom while loading and append the new data to your list.


Q9. What is ScrollPhysics and what are the common types?

Answer:
ScrollPhysics determines the scroll behavior -- how the scrollable responds to user input, overscroll, and momentum.

Common types:

  • BouncingScrollPhysics -- iOS-style bounce effect when scrolling past the edge. Default on iOS.
  • ClampingScrollPhysics -- Android-style glow effect at the edge, no bounce. Default on Android.
  • NeverScrollableScrollPhysics -- Disables scrolling entirely. Useful when nesting a ListView inside another scrollable.
  • AlwaysScrollableScrollPhysics -- Always allows scrolling even when content fits on screen. Useful for pull-to-refresh.
  • PageScrollPhysics -- Snaps to page boundaries. Used internally by PageView.
ListView(
  physics: BouncingScrollPhysics(), // Always bounce like iOS
  children: [...],
)

ListView(
  physics: NeverScrollableScrollPhysics(), // Disable scrolling
  shrinkWrap: true,
  children: [...],
)
Enter fullscreen mode Exit fullscreen mode

Q10. What is the difference between SliverList and SliverFixedExtentList?

Answer:

  • SliverList -- Each child can have a different height. The sliver must measure each child to determine scroll offsets. Slightly less efficient because it cannot predict exact positions.
  • SliverFixedExtentList -- All children have the same fixed height (specified by itemExtent). Because the height is known in advance, Flutter can calculate the exact scroll position of any item without measuring it. This is more efficient, enables instant jumpTo for any index, and is ideal for uniform lists.
// Variable height items
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => ListTile(title: Text('Item $index')),
    childCount: 100,
  ),
)

// Fixed height items (more performant)
SliverFixedExtentList(
  itemExtent: 56.0,
  delegate: SliverChildBuilderDelegate(
    (context, index) => ListTile(title: Text('Item $index')),
    childCount: 100,
  ),
)
Enter fullscreen mode Exit fullscreen mode

Similarly, ListView has an itemExtent property for the same optimization.


3. Navigation (Navigator 1.0, Navigator 2.0 / Router, Named Routes, onGenerateRoute, go_router)

Q1. How does Navigator 1.0 work in Flutter? Explain push and pop.

Answer:
Navigator 1.0 uses an imperative, stack-based approach. The Navigator widget manages a stack of Route objects. You push routes onto the stack to navigate forward and pop them to go back.

Key methods:

  • Navigator.push(context, route) -- Pushes a new route onto the stack.
  • Navigator.pop(context) -- Removes the top route from the stack.
  • Navigator.pushReplacement(context, route) -- Replaces the current route with a new one.
  • Navigator.pushAndRemoveUntil(context, route, predicate) -- Pushes a route and removes all routes below until the predicate returns true.
  • Navigator.popUntil(context, predicate) -- Pops routes until the predicate is satisfied.
// Push a new screen
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailScreen(id: 42)),
);

// Pop back
Navigator.pop(context);

// Push and return data
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// result contains whatever SelectionScreen passed to Navigator.pop(context, result)

// Replace current screen
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomeScreen()),
);

// Push and remove all previous routes (e.g., after login)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => HomeScreen()),
  (route) => false, // Remove all routes
);
Enter fullscreen mode Exit fullscreen mode

Q2. What are named routes and what are their limitations?

Answer:
Named routes let you navigate using string identifiers instead of constructing MaterialPageRoute directly. You define routes in MaterialApp:

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/details': (context) => DetailsScreen(),
    '/settings': (context) => SettingsScreen(),
  },
)

// Navigate
Navigator.pushNamed(context, '/details');
Enter fullscreen mode Exit fullscreen mode

Passing arguments:

Navigator.pushNamed(context, '/details', arguments: {'id': 42});

// In DetailsScreen:
final args = ModalRoute.of(context)!.settings.arguments as Map;
Enter fullscreen mode Exit fullscreen mode

Limitations of named routes:

  1. No type safety -- Arguments are passed as Object?, requiring casting.
  2. No compile-time checking -- Typos in route names cause runtime errors.
  3. Difficult deep linking -- Cannot easily parse URL parameters.
  4. Cannot dynamically control the route stack -- Limited declarative control.
  5. The Flutter team itself discourages named routes for anything beyond simple apps.

For these reasons, most production apps use onGenerateRoute or a routing package like go_router.


Q3. What is onGenerateRoute and why is it preferred over the routes map?

Answer:
onGenerateRoute is a callback on MaterialApp that is called whenever Navigator.pushNamed is used. It gives you full control over route creation, allowing you to parse route names, extract parameters, and return the appropriate Route.

MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => HomeScreen());
      case '/details':
        final args = settings.arguments as Map<String, dynamic>;
        return MaterialPageRoute(
          builder: (_) => DetailsScreen(id: args['id']),
        );
      case '/user':
        // Parse path parameters: /user/123
        return MaterialPageRoute(
          builder: (_) => UserScreen(id: settings.arguments as int),
        );
      default:
        return MaterialPageRoute(builder: (_) => NotFoundScreen());
    }
  },
)
Enter fullscreen mode Exit fullscreen mode

Advantages over the routes map:

  1. Argument parsing -- You can extract and validate arguments in one place.
  2. 404 handling -- The default case handles unknown routes.
  3. Dynamic routes -- You can parse path patterns (e.g., /user/:id).
  4. Transition customization -- Return different route types (e.g., CupertinoPageRoute).
  5. Centralized routing logic -- All routing decisions are in one function.

Q4. What is Navigator 2.0 (Router) and how does it differ from Navigator 1.0?

Answer:
Navigator 2.0 (introduced in Flutter 1.22) is a declarative routing API designed to give apps full control over the route stack and support deep linking, web URLs, and platform-level navigation.

Key differences:

Aspect Navigator 1.0 Navigator 2.0
Paradigm Imperative (push/pop) Declarative (state-driven)
Route stack Implicit Explicitly defined as a list of Page objects
Deep linking Difficult Built-in support
Web URL sync Manual Automatic via RouteInformationProvider
Back button Automatic Customizable via PopScope

Navigator 2.0 is built on several components:

  • Router widget -- Configures routing.
  • RouteInformationParser -- Parses route information (URL) into app-specific state.
  • RouterDelegate -- Builds the Navigator widget based on app state.
  • RouteInformationProvider -- Provides route information from the platform.
MaterialApp.router(
  routerDelegate: myRouterDelegate,
  routeInformationParser: myRouteInformationParser,
)
Enter fullscreen mode Exit fullscreen mode

In practice, Navigator 2.0's raw API is verbose and complex. Most developers use go_router (the officially recommended package) which wraps Navigator 2.0 in a much simpler API.


Q5. What is go_router and why is it recommended?

Answer:
go_router is the officially recommended routing package for Flutter, maintained by the Flutter team. It wraps Navigator 2.0 in a simple, declarative, URL-based API.

Key features:

  • URL-based routing with path parameters and query parameters.
  • Deep linking support out of the box.
  • Nested navigation (ShellRoute) for bottom navigation bars and tab layouts.
  • Redirect logic for authentication guards.
  • Type-safe routes (with code generation).
  • Transition animations per route.
  • Web URL synchronization.
final router = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final isLoggedIn = authService.isLoggedIn;
    if (!isLoggedIn && state.matchedLocation != '/login') return '/login';
    return null; // No redirect
  },
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomeScreen(),
      routes: [
        GoRoute(
          path: 'details/:id',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return DetailsScreen(id: id);
          },
        ),
      ],
    ),
    ShellRoute(
      builder: (context, state, child) => ScaffoldWithNavBar(child: child),
      routes: [
        GoRoute(path: '/feed', builder: (context, state) => FeedScreen()),
        GoRoute(path: '/profile', builder: (context, state) => ProfileScreen()),
      ],
    ),
    GoRoute(path: '/login', builder: (context, state) => LoginScreen()),
  ],
);

// In MaterialApp
MaterialApp.router(routerConfig: router);

// Navigate
context.go('/details/42');
context.push('/details/42'); // pushes onto stack
context.pop();
Enter fullscreen mode Exit fullscreen mode

Q6. How do you pass data between screens in Flutter?

Answer:
There are several approaches:

1. Constructor parameters (recommended for Navigator 1.0):

Navigator.push(context, MaterialPageRoute(
  builder: (_) => DetailScreen(item: myItem),
));
Enter fullscreen mode Exit fullscreen mode

2. Named route arguments:

Navigator.pushNamed(context, '/detail', arguments: myItem);
// Receive: ModalRoute.of(context)!.settings.arguments as MyItem
Enter fullscreen mode Exit fullscreen mode

3. Return data with pop:

// Push and await result
final result = await Navigator.push(context, MaterialPageRoute(
  builder: (_) => SelectionScreen(),
));

// In SelectionScreen:
Navigator.pop(context, selectedValue);
Enter fullscreen mode Exit fullscreen mode

4. Path/query parameters (go_router):

context.go('/detail/42?tab=reviews');
// Read: state.pathParameters['id'], state.uri.queryParameters['tab']
Enter fullscreen mode Exit fullscreen mode

5. State management:
For complex data sharing, use a state management solution (Provider, Riverpod, Bloc) that makes data available to any screen without passing it through navigation.


Q7. What is PopScope (formerly WillPopScope) and how is it used?

Answer:
PopScope (renamed from WillPopScope in Flutter 3.16) allows you to intercept the system back button and programmatic pops. It is used to show confirmation dialogs, prevent accidental navigation away from unsaved forms, or custom back behavior.

PopScope(
  canPop: false, // Prevent default pop behavior
  onPopInvokedWithResult: (didPop, result) async {
    if (didPop) return; // Already popped, nothing to do

    // Show confirmation dialog
    final shouldPop = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Discard changes?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, false), child: Text('Cancel')),
          TextButton(onPressed: () => Navigator.pop(context, true), child: Text('Discard')),
        ],
      ),
    );
    if (shouldPop ?? false) {
      Navigator.pop(context);
    }
  },
  child: Scaffold(...),
)
Enter fullscreen mode Exit fullscreen mode

Key change from WillPopScope: The new PopScope API uses canPop (a boolean) plus onPopInvokedWithResult callback, rather than the old onWillPop future-based approach. This aligns better with the platform's predictive back gesture on Android 14+.


Q8. How do you implement nested navigation (e.g., bottom navigation with separate stacks)?

Answer:
Nested navigation means each tab in a bottom navigation bar maintains its own navigation stack, so switching tabs preserves each tab's history.

Approach 1: Multiple Navigator widgets

Scaffold(
  body: IndexedStack(
    index: _currentIndex,
    children: [
      Navigator(key: _homeNavKey, onGenerateRoute: ...),
      Navigator(key: _searchNavKey, onGenerateRoute: ...),
      Navigator(key: _profileNavKey, onGenerateRoute: ...),
    ],
  ),
  bottomNavigationBar: BottomNavigationBar(
    currentIndex: _currentIndex,
    onTap: (index) => setState(() => _currentIndex = index),
    items: [...],
  ),
)
Enter fullscreen mode Exit fullscreen mode

Approach 2: ShellRoute in go_router (recommended)

ShellRoute(
  builder: (context, state, child) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(...),
    );
  },
  routes: [
    GoRoute(path: '/home', builder: (_, __) => HomeScreen()),
    GoRoute(path: '/search', builder: (_, __) => SearchScreen()),
    GoRoute(path: '/profile', builder: (_, __) => ProfileScreen()),
  ],
)
Enter fullscreen mode Exit fullscreen mode

For independent stacks per tab, use StatefulShellRoute in go_router, which preserves each branch's state.


Q9. How do you implement deep linking in Flutter?

Answer:
Deep linking allows your app to be opened via a URL (e.g., myapp://details/42 or https://myapp.com/details/42).

Steps:

  1. Configure platform (Android/iOS):

    • Android: Add intent filters in AndroidManifest.xml for your URL schemes.
    • iOS: Add URL types in Info.plist and Associated Domains for universal links.
  2. Handle in Flutter:

    • With go_router, deep links are handled automatically -- URLs map to your route definitions.
    • With Navigator 1.0, use onGenerateRoute to parse incoming paths.
  3. Test: Use adb (Android) or xcrun (iOS) to simulate deep links.

// go_router automatically handles deep links
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductScreen(id: id);
      },
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

For Flutter web, URLs work natively since the browser address bar drives navigation.


Q10. What are route transitions and how do you customize them?

Answer:
Route transitions are the animations played when navigating between screens. By default, MaterialPageRoute uses a slide-from-right animation on Android and a slide-up animation on iOS.

To customize, use PageRouteBuilder:

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(opacity: animation, child: child);
    },
    transitionDuration: Duration(milliseconds: 300),
  ),
);
Enter fullscreen mode Exit fullscreen mode

Common transitions:

  • FadeTransition -- Fade in/out.
  • SlideTransition -- Slide from any direction.
  • ScaleTransition -- Zoom in/out.
  • RotationTransition -- Rotate in/out.

With go_router:

GoRoute(
  path: '/details',
  pageBuilder: (context, state) => CustomTransitionPage(
    child: DetailsScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(opacity: animation, child: child);
    },
  ),
)
Enter fullscreen mode Exit fullscreen mode

4. Forms & Input (TextField, TextFormField, Form, Validation, FocusNode)

Q1. What is the difference between TextField and TextFormField?

Answer:
Both are text input widgets, but they serve different purposes:

  • TextField is a standalone text input widget. It uses a TextEditingController to read/set its value. It has an onChanged callback for reacting to changes. It does not integrate with Form.

  • TextFormField is a TextField wrapped in a FormField. It adds a validator property and integrates with the Form widget. When Form.validate() is called, all TextFormField validators run automatically. It also supports onSaved callback.

// TextField -- standalone
TextField(
  controller: _nameController,
  onChanged: (value) => print('Typed: $value'),
  decoration: InputDecoration(labelText: 'Name'),
)

// TextFormField -- works with Form
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) return 'Required field';
    return null; // Valid
  },
  onSaved: (value) => _name = value!,
  decoration: InputDecoration(labelText: 'Name'),
)
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Use TextFormField when you have a Form with validation. Use TextField for simple standalone inputs like search bars.


Q2. How does the Form widget work with validation?

Answer:
The Form widget groups multiple TextFormField widgets and provides validation and save functionality via a GlobalKey<FormState>.

Steps:

  1. Create a GlobalKey<FormState>.
  2. Wrap TextFormField widgets in a Form widget.
  3. Call formKey.currentState!.validate() to trigger all validators.
  4. Call formKey.currentState!.save() to trigger all onSaved callbacks.
  5. Call formKey.currentState!.reset() to clear all fields.
class MyForm extends StatefulWidget {
  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();
  String _email = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: 'Email'),
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value == null || value.isEmpty) return 'Email is required';
              if (!RegExp(r'^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
                return 'Enter a valid email';
              }
              return null;
            },
            onSaved: (value) => _email = value!,
          ),
          TextFormField(
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
            validator: (value) {
              if (value == null || value.length < 8) return 'Minimum 8 characters';
              return null;
            },
            onSaved: (value) => _password = value!,
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                _formKey.currentState!.save();
                // Use _email and _password
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

autovalidateMode options:

  • AutovalidateMode.disabled -- Only validate when validate() is called.
  • AutovalidateMode.onUserInteraction -- Validate as the user types (after first interaction).
  • AutovalidateMode.always -- Validate continuously.

Q3. What is TextEditingController and why do you need to dispose it?

Answer:
TextEditingController controls the text being edited in a TextField or TextFormField. It lets you:

  • Read the current text: controller.text.
  • Set the text programmatically: controller.text = 'new value'.
  • Listen to changes: controller.addListener(callback).
  • Control selection: controller.selection.
  • Clear the field: controller.clear().
class _MyState extends State<MyWidget> {
  final _controller = TextEditingController(text: 'Initial value');

  @override
  void dispose() {
    _controller.dispose(); // MUST dispose to avoid memory leaks
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: _controller),
        ElevatedButton(
          onPressed: () => print(_controller.text),
          child: Text('Print value'),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why dispose? TextEditingController registers listeners and holds references. If not disposed, it causes memory leaks. The dispose() method releases those resources. Always call it in the State.dispose() method.


Q4. What is FocusNode and how is it used?

Answer:
FocusNode controls the focus state of an input widget -- whether it currently has keyboard focus. It lets you:

  • Request focus programmatically: focusNode.requestFocus().
  • Remove focus (dismiss keyboard): focusNode.unfocus() or FocusScope.of(context).unfocus().
  • Listen to focus changes: focusNode.addListener(callback) or use hasFocus.
  • Move focus to next field: FocusScope.of(context).nextFocus().
class _MyFormState extends State<MyForm> {
  final _emailFocus = FocusNode();
  final _passwordFocus = FocusNode();

  @override
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _emailFocus,
          textInputAction: TextInputAction.next,
          onSubmitted: (_) => _passwordFocus.requestFocus(),
          decoration: InputDecoration(labelText: 'Email'),
        ),
        TextField(
          focusNode: _passwordFocus,
          textInputAction: TextInputAction.done,
          onSubmitted: (_) => _passwordFocus.unfocus(),
          decoration: InputDecoration(labelText: 'Password'),
        ),
        // Dismiss keyboard by tapping outside
        GestureDetector(
          onTap: () => FocusScope.of(context).unfocus(),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Best practice: Use textInputAction (next, done, go, search) combined with onSubmitted and FocusNode to create a smooth form UX where pressing "Next" on the keyboard moves to the next field.


Q5. How do you dismiss the keyboard in Flutter?

Answer:
Several approaches:

1. Unfocus the current focus node:

FocusScope.of(context).unfocus();
Enter fullscreen mode Exit fullscreen mode

2. Request focus on an empty FocusNode (older approach):

FocusScope.of(context).requestFocus(FocusNode()); // Not recommended anymore
Enter fullscreen mode Exit fullscreen mode

3. Wrap the Scaffold body in a GestureDetector:

GestureDetector(
  onTap: () => FocusScope.of(context).unfocus(),
  child: Scaffold(
    body: Form(...),
  ),
)
Enter fullscreen mode Exit fullscreen mode

4. Use FocusManager:

FocusManager.instance.primaryFocus?.unfocus();
Enter fullscreen mode Exit fullscreen mode

The most common pattern is wrapping the body in a GestureDetector that calls FocusScope.of(context).unfocus() on tap, which dismisses the keyboard whenever the user taps outside any text field.


Q6. How do you implement real-time search with TextField?

Answer:
Use onChanged with debouncing to avoid making API calls on every keystroke:

class _SearchState extends State<SearchScreen> {
  final _controller = TextEditingController();
  Timer? _debounce;
  List<String> _results = [];

  void _onSearchChanged(String query) {
    _debounce?.cancel();
    _debounce = Timer(Duration(milliseconds: 500), () {
      _performSearch(query);
    });
  }

  Future<void> _performSearch(String query) async {
    if (query.isEmpty) {
      setState(() => _results = []);
      return;
    }
    final results = await apiService.search(query);
    setState(() => _results = results);
  }

  @override
  void dispose() {
    _controller.dispose();
    _debounce?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _controller,
          onChanged: _onSearchChanged,
          decoration: InputDecoration(
            hintText: 'Search...',
            prefixIcon: Icon(Icons.search),
            suffixIcon: IconButton(
              icon: Icon(Icons.clear),
              onPressed: () {
                _controller.clear();
                _onSearchChanged('');
              },
            ),
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _results.length,
            itemBuilder: (context, index) => ListTile(title: Text(_results[index])),
          ),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key concept: The 500ms debounce prevents firing a search request on every keystroke. The search only fires after the user stops typing for 500ms.


Q7. What is InputDecoration and what are its key properties?

Answer:
InputDecoration controls the visual appearance of a TextField or TextFormField. Key properties:

  • labelText -- Floating label that moves above the field when focused.
  • hintText -- Placeholder text shown when the field is empty.
  • helperText -- Persistent text below the field.
  • errorText -- Error message below the field (overrides helperText).
  • prefixIcon / suffixIcon -- Icons at the start/end of the field.
  • prefix / suffix -- Widgets at the start/end (inside the field).
  • border -- OutlineInputBorder, UnderlineInputBorder, or InputBorder.none.
  • enabledBorder / focusedBorder / errorBorder -- State-specific borders.
  • filled / fillColor -- Background fill.
  • contentPadding -- Padding inside the field.
  • counterText -- Text at the bottom-right (e.g., character count).
TextField(
  decoration: InputDecoration(
    labelText: 'Email',
    hintText: 'you@example.com',
    helperText: 'Enter your registered email',
    prefixIcon: Icon(Icons.email),
    suffixIcon: Icon(Icons.check_circle),
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
    filled: true,
    fillColor: Colors.grey.shade100,
  ),
)
Enter fullscreen mode Exit fullscreen mode

You can set global InputDecoration defaults via ThemeData.inputDecorationTheme.


Q8. How do you create a custom form field (e.g., a date picker field)?

Answer:
Extend FormField<T> to create any custom field that integrates with Form validation:

class DatePickerFormField extends FormField<DateTime> {
  DatePickerFormField({
    super.key,
    super.validator,
    super.onSaved,
    DateTime? initialValue,
    InputDecoration decoration = const InputDecoration(),
  }) : super(
    initialValue: initialValue,
    builder: (FormFieldState<DateTime> state) {
      return InkWell(
        onTap: () async {
          final picked = await showDatePicker(
            context: state.context,
            initialDate: state.value ?? DateTime.now(),
            firstDate: DateTime(2000),
            lastDate: DateTime(2100),
          );
          if (picked != null) {
            state.didChange(picked);
          }
        },
        child: InputDecorator(
          decoration: decoration.copyWith(errorText: state.errorText),
          child: Text(
            state.value != null
                ? '${state.value!.day}/${state.value!.month}/${state.value!.year}'
                : 'Select a date',
          ),
        ),
      );
    },
  );
}

// Usage in a Form:
DatePickerFormField(
  decoration: InputDecoration(labelText: 'Birth Date', prefixIcon: Icon(Icons.calendar_today)),
  validator: (value) => value == null ? 'Date is required' : null,
  onSaved: (value) => _birthDate = value!,
)
Enter fullscreen mode Exit fullscreen mode

Q9. How do you format text input (e.g., phone numbers, credit cards)?

Answer:
Use TextInputFormatter on the inputFormatters property of TextField:

Built-in formatters:

  • FilteringTextInputFormatter.digitsOnly -- Only allow digits.
  • FilteringTextInputFormatter.allow(RegExp(...)) -- Allow matching characters.
  • FilteringTextInputFormatter.deny(RegExp(...)) -- Block matching characters.
  • LengthLimitingTextInputFormatter(maxLength) -- Limit character count.

Custom formatter:

class PhoneNumberFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
    final buffer = StringBuffer();
    for (int i = 0; i < digits.length && i < 10; i++) {
      if (i == 0) buffer.write('(');
      if (i == 3) buffer.write(') ');
      if (i == 6) buffer.write('-');
      buffer.write(digits[i]);
    }
    return TextEditingValue(
      text: buffer.toString(),
      selection: TextSelection.collapsed(offset: buffer.length),
    );
  }
}

// Usage
TextField(
  keyboardType: TextInputType.phone,
  inputFormatters: [PhoneNumberFormatter()],
)
Enter fullscreen mode Exit fullscreen mode

Q10. What is AutovalidateMode and what are the differences between its values?

Answer:
AutovalidateMode controls when TextFormField validators run automatically:

  • AutovalidateMode.disabled -- Validators only run when Form.validate() is called manually. Best for forms where you want to validate only on submit.

  • AutovalidateMode.onUserInteraction -- Validators run after the user first interacts with the field (types or focuses/unfocuses). The field does not show errors until the user touches it. This is the most common choice for good UX.

  • AutovalidateMode.always -- Validators run immediately and continuously, even before the user interacts. Fields show errors as soon as they render. Rarely desired because it shows errors on a fresh form.

Form(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: Column(
    children: [
      TextFormField(
        validator: (v) => v!.isEmpty ? 'Required' : null,
      ),
      // Each field validates as the user interacts with it
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

You can set autovalidateMode on either the Form (applies to all fields) or on individual TextFormField widgets (overrides the Form setting).


5. Buttons (ElevatedButton, TextButton, OutlinedButton, IconButton, FloatingActionButton)

Q1. What are the main button types in Flutter and when do you use each?

Answer:
Flutter provides several Material Design button types:

Button Appearance Use Case
ElevatedButton Raised/filled with elevation Primary actions, high emphasis
FilledButton Filled, no elevation (Material 3) Primary actions in Material 3
FilledButton.tonal Filled with tonal color Medium emphasis actions
TextButton No background or border Low emphasis, in dialogs, inline
OutlinedButton Outlined border, no fill Medium emphasis, alternative to elevated
IconButton Just an icon, no text Toolbars, compact actions
FloatingActionButton Circular, floating Primary screen action
ElevatedButton(onPressed: () {}, child: Text('Save'));
TextButton(onPressed: () {}, child: Text('Cancel'));
OutlinedButton(onPressed: () {}, child: Text('Details'));
IconButton(onPressed: () {}, icon: Icon(Icons.delete));
FloatingActionButton(onPressed: () {}, child: Icon(Icons.add));
Enter fullscreen mode Exit fullscreen mode

Disabled state: Pass null to onPressed to disable any button:

ElevatedButton(onPressed: null, child: Text('Disabled'));
Enter fullscreen mode Exit fullscreen mode

Q2. How do you style buttons in Flutter?

Answer:
Buttons are styled using the style parameter with ButtonStyle, or more conveniently with styleFrom():

ElevatedButton(
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.indigo,
    foregroundColor: Colors.white,
    elevation: 4,
    padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    textStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
    minimumSize: Size(200, 50),
  ),
  onPressed: () {},
  child: Text('Styled Button'),
)
Enter fullscreen mode Exit fullscreen mode

Full ButtonStyle with WidgetStateProperty (formerly MaterialStateProperty):

ElevatedButton(
  style: ButtonStyle(
    backgroundColor: WidgetStateProperty.resolveWith((states) {
      if (states.contains(WidgetState.pressed)) return Colors.indigo.shade700;
      if (states.contains(WidgetState.disabled)) return Colors.grey;
      return Colors.indigo;
    }),
    overlayColor: WidgetStateProperty.all(Colors.white24),
  ),
  onPressed: () {},
  child: Text('Dynamic Style'),
)
Enter fullscreen mode Exit fullscreen mode

Global button theme:

ThemeData(
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      backgroundColor: Colors.indigo,
      foregroundColor: Colors.white,
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Q3. What is FloatingActionButton and what are its variants?

Answer:
FloatingActionButton (FAB) is a circular button that floats above the UI, typically used for the primary action on a screen. It is usually placed in Scaffold.floatingActionButton.

Variants:

  • FloatingActionButton -- Standard circular FAB.
  • FloatingActionButton.small -- Smaller circular FAB.
  • FloatingActionButton.large -- Larger circular FAB.
  • FloatingActionButton.extended -- Pill-shaped FAB with icon and label.
Scaffold(
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
)

// Extended FAB
FloatingActionButton.extended(
  onPressed: () {},
  icon: Icon(Icons.add),
  label: Text('New Item'),
)
Enter fullscreen mode Exit fullscreen mode

floatingActionButtonLocation controls where the FAB is placed: endFloat (default), centerFloat, centerDocked, endDocked, startFloat, etc.


Q4. How do you create a button with an icon and text?

Answer:
All button types have an .icon constructor:

ElevatedButton.icon(
  onPressed: () {},
  icon: Icon(Icons.send),
  label: Text('Send'),
)

TextButton.icon(
  onPressed: () {},
  icon: Icon(Icons.download),
  label: Text('Download'),
)

OutlinedButton.icon(
  onPressed: () {},
  icon: Icon(Icons.share),
  label: Text('Share'),
)
Enter fullscreen mode Exit fullscreen mode

Alternatively, manually compose with a Row:

ElevatedButton(
  onPressed: () {},
  child: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Icon(Icons.send),
      SizedBox(width: 8),
      Text('Send'),
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

Q5. What is the difference between InkWell and GestureDetector?

Answer:
Both detect user gestures, but they differ in visual feedback and use cases:

  • InkWell -- A Material widget that shows a ripple/splash effect when tapped. It must have a Material ancestor in the widget tree. It supports onTap, onLongPress, onDoubleTap, onHover, and the splash can be customized.

  • GestureDetector -- A lower-level widget that detects gestures without any visual feedback. It supports a much wider range of gestures: tap, double tap, long press, pan, scale, drag, force press.

// InkWell -- with ripple effect
InkWell(
  onTap: () => print('Tapped'),
  borderRadius: BorderRadius.circular(8),
  splashColor: Colors.blue.withOpacity(0.3),
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Text('Tap me'),
  ),
)

// GestureDetector -- no visual feedback
GestureDetector(
  onTap: () => print('Tapped'),
  onPanUpdate: (details) => print('Dragging: ${details.delta}'),
  child: Container(
    width: 100,
    height: 100,
    color: Colors.red,
  ),
)
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Use InkWell for Material-style tappable items. Use GestureDetector for custom gestures, drag, or when you do not want Material effects.


Q6. How do you add a loading state to a button?

Answer:
A common pattern is to show a CircularProgressIndicator inside the button while an async operation is running:

class _MyState extends State<MyWidget> {
  bool _isLoading = false;

  Future<void> _submit() async {
    setState(() => _isLoading = true);
    try {
      await apiService.submit();
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isLoading ? null : _submit,
      child: _isLoading
          ? SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
            )
          : Text('Submit'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Setting onPressed to null disables the button during loading.
  • Use SizedBox to constrain the progress indicator's size.
  • Check mounted before calling setState in case the widget was disposed during the async operation.

Q7. What are WidgetStateProperty (formerly MaterialStateProperty) and how do buttons use them?

Answer:
WidgetStateProperty allows button styles to vary based on the widget's interactive state -- pressed, hovered, focused, disabled, selected, etc.

Every property in ButtonStyle (backgroundColor, foregroundColor, elevation, padding, etc.) is a WidgetStateProperty, meaning it can return different values depending on the current state.

ElevatedButton(
  style: ButtonStyle(
    backgroundColor: WidgetStateProperty.resolveWith<Color>((states) {
      if (states.contains(WidgetState.disabled)) return Colors.grey.shade300;
      if (states.contains(WidgetState.pressed)) return Colors.blue.shade800;
      if (states.contains(WidgetState.hovered)) return Colors.blue.shade600;
      return Colors.blue; // Default
    }),
    foregroundColor: WidgetStateProperty.all(Colors.white),
    elevation: WidgetStateProperty.resolveWith((states) {
      if (states.contains(WidgetState.pressed)) return 0;
      return 4;
    }),
  ),
  onPressed: () {},
  child: Text('Stateful Button'),
)
Enter fullscreen mode Exit fullscreen mode

Convenience constructors:

  • WidgetStateProperty.all(value) -- Same value for all states.
  • WidgetStateProperty.resolveWith(callback) -- Different values per state.

Q8. How do you create a custom-shaped button (e.g., circular, pill, or with a gradient)?

Answer:

Pill / Rounded button:

ElevatedButton(
  style: ElevatedButton.styleFrom(
    shape: StadiumBorder(), // Pill shape
    padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
  ),
  onPressed: () {},
  child: Text('Pill Button'),
)
Enter fullscreen mode Exit fullscreen mode

Circular button:

ElevatedButton(
  style: ElevatedButton.styleFrom(
    shape: CircleBorder(),
    padding: EdgeInsets.all(20),
  ),
  onPressed: () {},
  child: Icon(Icons.add),
)
Enter fullscreen mode Exit fullscreen mode

Gradient button (custom):

Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
    borderRadius: BorderRadius.circular(30),
  ),
  child: ElevatedButton(
    style: ElevatedButton.styleFrom(
      backgroundColor: Colors.transparent,
      shadowColor: Colors.transparent,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
    ),
    onPressed: () {},
    child: Text('Gradient'),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Q9. What is ToggleButtons and when is it used?

Answer:
ToggleButtons displays a row of toggle buttons where one or more can be selected. It is used for settings, filters, or any mutually exclusive / multi-select option groups.

class _ToggleState extends State<MyWidget> {
  List<bool> _selections = [true, false, false];

  @override
  Widget build(BuildContext context) {
    return ToggleButtons(
      isSelected: _selections,
      onPressed: (index) {
        setState(() {
          // For single select:
          for (int i = 0; i < _selections.length; i++) {
            _selections[i] = i == index;
          }
          // For multi-select: _selections[index] = !_selections[index];
        });
      },
      borderRadius: BorderRadius.circular(8),
      selectedColor: Colors.white,
      fillColor: Colors.blue,
      children: [
        Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: Text('Day')),
        Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: Text('Week')),
        Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: Text('Month')),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In Material 3, consider using SegmentedButton as a more modern alternative.


Q10. What is PopupMenuButton and how do you use it?

Answer:
PopupMenuButton shows a popup menu when pressed, typically used for overflow menus (three-dot menu) or context menus.

PopupMenuButton<String>(
  onSelected: (value) {
    switch (value) {
      case 'edit': _edit(); break;
      case 'delete': _delete(); break;
      case 'share': _share(); break;
    }
  },
  itemBuilder: (context) => [
    PopupMenuItem(value: 'edit', child: ListTile(leading: Icon(Icons.edit), title: Text('Edit'))),
    PopupMenuItem(value: 'delete', child: ListTile(leading: Icon(Icons.delete), title: Text('Delete'))),
    PopupMenuDivider(),
    PopupMenuItem(value: 'share', child: ListTile(leading: Icon(Icons.share), title: Text('Share'))),
  ],
  icon: Icon(Icons.more_vert),
)
Enter fullscreen mode Exit fullscreen mode

Properties:

  • itemBuilder -- Returns the list of PopupMenuEntry items.
  • onSelected -- Called when an item is selected.
  • icon -- Custom icon (default is three dots).
  • offset -- Offset the popup from the button.
  • shape -- Shape of the popup menu.

6. Dialogs, BottomSheets, SnackBars, Drawers

Q1. How do you show a dialog in Flutter? What is the difference between AlertDialog and SimpleDialog?

Answer:
Dialogs are shown using the showDialog() function, which returns a Future that completes when the dialog is dismissed.

  • AlertDialog -- For confirmations and messages. Has a title, content, and actions (buttons). Used for "Are you sure?" prompts, error messages, etc.
  • SimpleDialog -- For choosing from a list of options. Has a title and children (usually SimpleDialogOption widgets).
// AlertDialog
showDialog(
  context: context,
  barrierDismissible: false, // Must tap a button to close
  builder: (context) => AlertDialog(
    title: Text('Delete Item?'),
    content: Text('This action cannot be undone.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context, false),
        child: Text('Cancel'),
      ),
      TextButton(
        onPressed: () => Navigator.pop(context, true),
        child: Text('Delete', style: TextStyle(color: Colors.red)),
      ),
    ],
  ),
);

// SimpleDialog
final result = await showDialog<String>(
  context: context,
  builder: (context) => SimpleDialog(
    title: Text('Select Language'),
    children: [
      SimpleDialogOption(onPressed: () => Navigator.pop(context, 'en'), child: Text('English')),
      SimpleDialogOption(onPressed: () => Navigator.pop(context, 'es'), child: Text('Spanish')),
      SimpleDialogOption(onPressed: () => Navigator.pop(context, 'fr'), child: Text('French')),
    ],
  ),
);
Enter fullscreen mode Exit fullscreen mode

Returning data: Pass a value to Navigator.pop(context, value) and await the showDialog call.


Q2. What is the difference between showModalBottomSheet and showBottomSheet?

Answer:

Feature showModalBottomSheet showBottomSheet
Background Dims the background with a barrier No barrier, background is interactive
Dismissal Tap outside or swipe down to dismiss Must be closed programmatically
Scope Creates a new route (modal overlay) Attached to the current Scaffold
Use case Menus, pickers, confirmations Persistent info panels
// Modal bottom sheet
showModalBottomSheet(
  context: context,
  isScrollControlled: true, // Allows full height
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
  builder: (context) => Container(
    padding: EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(leading: Icon(Icons.camera), title: Text('Camera'), onTap: () {}),
        ListTile(leading: Icon(Icons.photo), title: Text('Gallery'), onTap: () {}),
      ],
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

isScrollControlled: true is critical when you want the bottom sheet to be taller than half the screen, or when it contains a scrollable widget.

To make a full-screen or dynamic-height bottom sheet:

showModalBottomSheet(
  context: context,
  isScrollControlled: true,
  builder: (context) => DraggableScrollableSheet(
    initialChildSize: 0.6,
    minChildSize: 0.3,
    maxChildSize: 0.95,
    expand: false,
    builder: (context, scrollController) => ListView.builder(
      controller: scrollController,
      itemCount: 50,
      itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

Q3. How do you show a SnackBar in Flutter?

Answer:
SnackBar is a lightweight message bar shown at the bottom of the screen. It is displayed using ScaffoldMessenger.

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text('Item deleted'),
    duration: Duration(seconds: 3),
    action: SnackBarAction(
      label: 'Undo',
      onPressed: () {
        // Undo the delete
      },
    ),
    behavior: SnackBarBehavior.floating,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    margin: EdgeInsets.all(16),
  ),
);
Enter fullscreen mode Exit fullscreen mode

Key properties:

  • content -- The main message (usually a Text widget).
  • action -- An optional action button (e.g., "Undo").
  • duration -- How long the SnackBar stays visible.
  • behavior -- SnackBarBehavior.fixed (default, anchored to bottom) or SnackBarBehavior.floating (floats above the bottom).
  • backgroundColor, shape, margin, padding -- Styling.

Important: Use ScaffoldMessenger.of(context) (not the older Scaffold.of(context).showSnackBar). ScaffoldMessenger is more reliable and persists across route changes.

To dismiss programmatically:

ScaffoldMessenger.of(context).hideCurrentSnackBar();
// or
ScaffoldMessenger.of(context).clearSnackBars();
Enter fullscreen mode Exit fullscreen mode

Q4. What is a Drawer and how do you implement it?

Answer:
Drawer is a slide-in panel from the left (or right with endDrawer) of the screen, used for app navigation. It is placed in the Scaffold.drawer property.

Scaffold(
  appBar: AppBar(title: Text('My App')),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              CircleAvatar(radius: 30, backgroundImage: AssetImage('avatar.jpg')),
              SizedBox(height: 8),
              Text('John Doe', style: TextStyle(color: Colors.white, fontSize: 18)),
              Text('john@example.com', style: TextStyle(color: Colors.white70)),
            ],
          ),
        ),
        ListTile(
          leading: Icon(Icons.home),
          title: Text('Home'),
          onTap: () {
            Navigator.pop(context); // Close the drawer
            // Navigate to Home
          },
        ),
        ListTile(
          leading: Icon(Icons.settings),
          title: Text('Settings'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/settings');
          },
        ),
        Divider(),
        ListTile(
          leading: Icon(Icons.logout),
          title: Text('Logout'),
          onTap: () => _logout(),
        ),
      ],
    ),
  ),
  body: Center(child: Text('Content')),
)
Enter fullscreen mode Exit fullscreen mode

Opening/closing programmatically:

Scaffold.of(context).openDrawer();    // Open drawer
Scaffold.of(context).openEndDrawer(); // Open end drawer
Navigator.pop(context);               // Close drawer
Enter fullscreen mode Exit fullscreen mode

The hamburger menu icon in the AppBar is added automatically when a drawer is set.


Q5. How do you create a custom dialog?

Answer:
Use showDialog with a custom widget instead of AlertDialog:

showDialog(
  context: context,
  builder: (context) => Dialog(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
    child: Padding(
      padding: EdgeInsets.all(24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.check_circle, color: Colors.green, size: 64),
          SizedBox(height: 16),
          Text('Success!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          Text('Your order has been placed successfully.', textAlign: TextAlign.center),
          SizedBox(height: 24),
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: Text('OK'),
            ),
          ),
        ],
      ),
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

For a full-screen dialog, use showGeneralDialog or Dialog.fullscreen:

showGeneralDialog(
  context: context,
  pageBuilder: (context, animation, secondaryAnimation) {
    return Scaffold(
      appBar: AppBar(title: Text('Full Screen Dialog'), leading: IconButton(
        icon: Icon(Icons.close),
        onPressed: () => Navigator.pop(context),
      )),
      body: Center(child: Text('Full screen content')),
    );
  },
);
Enter fullscreen mode Exit fullscreen mode

Q6. What is DraggableScrollableSheet and when would you use it?

Answer:
DraggableScrollableSheet is a scrollable container that can be dragged up or down to expand or collapse. It is commonly used inside showModalBottomSheet to create bottom sheets that the user can pull to different heights (like Google Maps or Apple Maps bottom panels).

showModalBottomSheet(
  context: context,
  isScrollControlled: true,
  builder: (context) => DraggableScrollableSheet(
    initialChildSize: 0.4,   // 40% of screen height
    minChildSize: 0.2,       // Minimum 20%
    maxChildSize: 0.9,       // Maximum 90%
    expand: false,
    builder: (context, scrollController) {
      return Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
        ),
        child: ListView.builder(
          controller: scrollController, // MUST pass this controller
          itemCount: 30,
          itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
        ),
      );
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

Important: You must pass the scrollController provided in the builder to the scrollable child. This allows the sheet to coordinate dragging vs. scrolling.


Q7. How do you show a DatePicker or TimePicker dialog?

Answer:
Flutter provides built-in Material date and time picker dialogs:

// Date Picker
final DateTime? selectedDate = await showDatePicker(
  context: context,
  initialDate: DateTime.now(),
  firstDate: DateTime(2000),
  lastDate: DateTime(2100),
  helpText: 'Select birth date',
);

// Time Picker
final TimeOfDay? selectedTime = await showTimePicker(
  context: context,
  initialTime: TimeOfDay.now(),
);

// Date Range Picker
final DateTimeRange? range = await showDateRangePicker(
  context: context,
  firstDate: DateTime(2020),
  lastDate: DateTime(2030),
);
Enter fullscreen mode Exit fullscreen mode

Both return null if the user cancels. The appearance follows your app's ThemeData and Material version (Material 2 or 3).


Q8. What is showCupertinoDialog and showCupertinoModalPopup?

Answer:
These show iOS-style dialogs and action sheets:

// Cupertino Alert Dialog (iOS style)
showCupertinoDialog(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: Text('Delete?'),
    content: Text('This cannot be undone.'),
    actions: [
      CupertinoDialogAction(child: Text('Cancel'), onPressed: () => Navigator.pop(context)),
      CupertinoDialogAction(
        isDestructiveAction: true,
        child: Text('Delete'),
        onPressed: () => Navigator.pop(context, true),
      ),
    ],
  ),
);

// Cupertino Action Sheet (iOS-style bottom sheet)
showCupertinoModalPopup(
  context: context,
  builder: (context) => CupertinoActionSheet(
    title: Text('Select Photo'),
    actions: [
      CupertinoActionSheetAction(child: Text('Camera'), onPressed: () {}),
      CupertinoActionSheetAction(child: Text('Gallery'), onPressed: () {}),
    ],
    cancelButton: CupertinoActionSheetAction(
      child: Text('Cancel'),
      onPressed: () => Navigator.pop(context),
    ),
  ),
);
Enter fullscreen mode Exit fullscreen mode

Use these when you want platform-specific look on iOS, or use showAdaptiveDialog in newer Flutter versions which automatically picks Material or Cupertino based on the platform.


Q9. How do you prevent a dialog from being dismissed by tapping outside?

Answer:
Set barrierDismissible: false in showDialog:

showDialog(
  context: context,
  barrierDismissible: false, // User MUST tap a button to close
  builder: (context) => AlertDialog(
    title: Text('Terms and Conditions'),
    content: Text('You must accept to continue.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: Text('Accept'),
      ),
    ],
  ),
);
Enter fullscreen mode Exit fullscreen mode

For showModalBottomSheet, set isDismissible: false and enableDrag: false:

showModalBottomSheet(
  context: context,
  isDismissible: false,   // Can't tap outside to dismiss
  enableDrag: false,       // Can't swipe down to dismiss
  builder: (context) => ...,
);
Enter fullscreen mode Exit fullscreen mode

Q10. What is the Tooltip widget?

Answer:
Tooltip shows a text label when the user long-presses or hovers over a widget. It is used for accessibility and to describe icon buttons or other widgets that lack visible labels.

Tooltip(
  message: 'Add new item',
  child: IconButton(
    icon: Icon(Icons.add),
    onPressed: () {},
  ),
)
Enter fullscreen mode Exit fullscreen mode

Many Material widgets (like IconButton, FloatingActionButton) have a built-in tooltip property:

IconButton(
  icon: Icon(Icons.delete),
  tooltip: 'Delete this item',
  onPressed: () {},
)
Enter fullscreen mode Exit fullscreen mode

Properties: message, richMessage (for styled text), waitDuration, showDuration, decoration, textStyle, verticalOffset.


7. Themes & Styling (ThemeData, TextTheme, ColorScheme, Material 3)

Q1. What is ThemeData and how do you use it in Flutter?

Answer:
ThemeData defines the overall visual theme for a Flutter app -- colors, typography, shapes, button styles, and more. It is set on MaterialApp.theme and can be accessed anywhere via Theme.of(context).

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
    useMaterial3: true,
    textTheme: TextTheme(
      headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
      bodyLarge: TextStyle(fontSize: 16),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
      ),
    ),
    appBarTheme: AppBarTheme(centerTitle: true, elevation: 0),
    inputDecorationTheme: InputDecorationTheme(
      border: OutlineInputBorder(),
      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    ),
  ),
  home: MyApp(),
)
Enter fullscreen mode Exit fullscreen mode

Access in a widget:

final theme = Theme.of(context);
Text('Hello', style: theme.textTheme.headlineLarge);
Container(color: theme.colorScheme.primary);
Enter fullscreen mode Exit fullscreen mode

Q2. What is ColorScheme and how does ColorScheme.fromSeed work?

Answer:
ColorScheme is a set of 25+ harmonious colors used throughout the Material Design system. Instead of manually choosing every color, you define a ColorScheme and all widgets use its colors automatically.

Key color roles:

  • primary / onPrimary -- Main brand color and text/icons on it.
  • secondary / onSecondary -- Accent color.
  • surface / onSurface -- Card, dialog, sheet backgrounds.
  • error / onError -- Error states.
  • tertiary / onTertiary -- Additional accent.
  • outline -- Border colors.

ColorScheme.fromSeed generates a complete, harmonious color scheme from a single seed color using Material 3's color generation algorithm:

ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.deepPurple,
    brightness: Brightness.light, // or Brightness.dark
  ),
)
Enter fullscreen mode Exit fullscreen mode

This is the recommended approach in Material 3. You provide one color and Flutter derives an entire palette.


Q3. How do you implement dark mode in Flutter?

Answer:
Define both a theme (light) and darkTheme (dark) on MaterialApp:

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.light),
    useMaterial3: true,
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
    useMaterial3: true,
  ),
  themeMode: ThemeMode.system, // Follow system setting
)
Enter fullscreen mode Exit fullscreen mode

ThemeMode options:

  • ThemeMode.system -- Follows the device's system-wide dark mode setting.
  • ThemeMode.light -- Always light.
  • ThemeMode.dark -- Always dark.

To let the user toggle themes, store the preference (e.g., SharedPreferences) and update themeMode via state management. In widgets, always use theme colors (e.g., Theme.of(context).colorScheme.surface) instead of hardcoded colors so they adapt automatically.


Q4. What is TextTheme and what are its standard text styles?

Answer:
TextTheme defines named text styles following the Material Design type scale. In Material 3:

Style Name Typical Use
displayLarge/Medium/Small Hero text, very large headlines
headlineLarge/Medium/Small Screen titles, section headers
titleLarge/Medium/Small AppBar titles, card titles
bodyLarge/Medium/Small Paragraph text, main content
labelLarge/Medium/Small Button labels, captions, chips
Text('Title', style: Theme.of(context).textTheme.headlineMedium);
Enter fullscreen mode Exit fullscreen mode

You can use Google Fonts easily:

ThemeData(textTheme: GoogleFonts.poppinsTextTheme())
Enter fullscreen mode Exit fullscreen mode

Q5. What is Material 3 and how do you enable it in Flutter?

Answer:
Material 3 (Material You) is the latest version of Google's Material Design. Key features:

  • Dynamic color -- Generates themes from a single seed color.
  • Updated components -- Redesigned buttons, cards, FABs, navigation bars, etc.
  • New shape system -- Rounded corners with configurable radii.
  • New typography scale -- Display, Headline, Title, Body, Label sizes.
  • New color roles -- Tertiary color, surface tint, outline variants.

Enabling:

MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
  ),
)
Enter fullscreen mode Exit fullscreen mode

As of Flutter 3.16+, Material 3 is the default. Key visual changes from Material 2: Buttons have rounded shapes, NavigationBar replaces BottomNavigationBar, Cards have surface tint, FABs have rounded-square shape, and AppBars use surface color with scroll-under tint.


Q6. How do you apply a theme to only a portion of the widget tree?

Answer:
Use the Theme widget to override the theme for a subtree:

Theme(
  data: Theme.of(context).copyWith(
    colorScheme: Theme.of(context).colorScheme.copyWith(primary: Colors.red),
  ),
  child: Column(
    children: [
      ElevatedButton(onPressed: () {}, child: Text('Red Button')),
      Text('This area has a different primary color'),
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

copyWith is critical -- it copies the parent theme and overrides only the properties you specify, keeping everything else consistent.


Q7. How do you use custom fonts in Flutter?

Answer:

Step 1: Place font files in assets/fonts/.

Step 2: Declare in pubspec.yaml:

flutter:
  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-Regular.ttf
        - asset: assets/fonts/Poppins-Bold.ttf
          weight: 700
        - asset: assets/fonts/Poppins-Italic.ttf
          style: italic
Enter fullscreen mode Exit fullscreen mode

Step 3: Use in code:

// Global
ThemeData(fontFamily: 'Poppins')

// Per-widget
Text('Hello', style: TextStyle(fontFamily: 'Poppins', fontWeight: FontWeight.bold));

// Using Google Fonts package (no download needed)
Text('Hello', style: GoogleFonts.poppins(fontSize: 18, fontWeight: FontWeight.w600));
Enter fullscreen mode Exit fullscreen mode

Q8. What is ThemeExtension and how do you create custom theme properties?

Answer:
ThemeExtension lets you add custom properties to the theme (e.g., custom spacing, custom colors not covered by Material). This is useful for design system tokens.

class AppSpacing extends ThemeExtension<AppSpacing> {
  final double small;
  final double medium;
  final double large;

  AppSpacing({required this.small, required this.medium, required this.large});

  @override
  AppSpacing copyWith({double? small, double? medium, double? large}) {
    return AppSpacing(
      small: small ?? this.small,
      medium: medium ?? this.medium,
      large: large ?? this.large,
    );
  }

  @override
  AppSpacing lerp(ThemeExtension<AppSpacing>? other, double t) {
    if (other is! AppSpacing) return this;
    return AppSpacing(
      small: lerpDouble(small, other.small, t)!,
      medium: lerpDouble(medium, other.medium, t)!,
      large: lerpDouble(large, other.large, t)!,
    );
  }
}

// Register
ThemeData(extensions: [AppSpacing(small: 8, medium: 16, large: 32)])

// Access
final spacing = Theme.of(context).extension<AppSpacing>()!;
Padding(padding: EdgeInsets.all(spacing.medium));
Enter fullscreen mode Exit fullscreen mode

Q9. What is the difference between primaryColor, colorScheme.primary, and primarySwatch?

Answer:

  • primarySwatch (Material 2, deprecated pattern) -- A MaterialColor with shades (50-900). Was used to generate multiple theme colors. Not recommended in Material 3.
  • primaryColor (legacy) -- A single Color for the app's primary brand color. Many widgets no longer read this in Material 3.
  • colorScheme.primary (recommended) -- The primary color in the ColorScheme. This is what Material 3 widgets actually use.

In Material 3, always use ColorScheme:

ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  useMaterial3: true,
)
// Access: Theme.of(context).colorScheme.primary
Enter fullscreen mode Exit fullscreen mode

Do not rely on primaryColor or primarySwatch in new code.


Q10. How do you style the AppBar globally?

Answer:
Use appBarTheme in your ThemeData:

ThemeData(
  appBarTheme: AppBarTheme(
    backgroundColor: Colors.white,
    foregroundColor: Colors.black,
    elevation: 0,
    centerTitle: true,
    titleTextStyle: TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.w600),
    iconTheme: IconThemeData(color: Colors.black),
    surfaceTintColor: Colors.transparent, // Removes M3 scroll tint
    shadowColor: Colors.transparent,
  ),
)
Enter fullscreen mode Exit fullscreen mode

In Material 3, the AppBar uses surface color by default and adds a tint on scroll. To get a flat white AppBar with no scroll effect, set surfaceTintColor and shadowColor to Colors.transparent.


8. Custom Painting (CustomPainter, Canvas, Paint)

Q1. What is CustomPainter and how do you use it?

Answer:
CustomPainter lets you draw custom graphics on a Canvas. You extend CustomPainter and override two methods:

  • paint(Canvas canvas, Size size) -- Where you draw using the Canvas API.
  • shouldRepaint(covariant CustomPainter oldDelegate) -- Returns true if the painting needs to be updated.
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 3
      ..style = PaintingStyle.stroke;

    canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
    canvas.drawLine(Offset(0, 0), Offset(size.width, size.height), paint);
    canvas.drawRect(Rect.fromLTWH(10, 10, 100, 60), paint..color = Colors.red);
  }

  @override
  bool shouldRepaint(covariant MyPainter oldDelegate) => false;
}

// Usage
CustomPaint(
  size: Size(300, 300),
  painter: MyPainter(),
  child: Center(child: Text('Overlay text')),
)
Enter fullscreen mode Exit fullscreen mode

Q2. What is the Canvas API? What are the key drawing methods?

Answer:
Canvas provides low-level drawing methods:

  • drawLine(p1, p2, paint) -- Draws a line between two points.
  • drawRect(rect, paint) -- Draws a rectangle.
  • drawRRect(rrect, paint) -- Draws a rounded rectangle.
  • drawCircle(center, radius, paint) -- Draws a circle.
  • drawOval(rect, paint) -- Draws an oval.
  • drawArc(rect, startAngle, sweepAngle, useCenter, paint) -- Draws an arc.
  • drawPath(path, paint) -- Draws an arbitrary path.
  • drawPoints(mode, points, paint) -- Draws points.
  • drawImage(image, offset, paint) -- Draws an image.

Canvas transformations:

  • canvas.save() / canvas.restore() -- Save and restore canvas state.
  • canvas.translate(dx, dy) -- Move the origin.
  • canvas.rotate(radians) -- Rotate.
  • canvas.scale(sx, sy) -- Scale.
  • canvas.clipRect(rect) / canvas.clipPath(path) -- Clip to a region.

Q3. What is the Paint object and what are its key properties?

Answer:
Paint describes how to draw on the canvas -- color, style, stroke width, and effects.

Key properties:

  • color -- The draw color.
  • style -- PaintingStyle.fill (solid) or PaintingStyle.stroke (outline).
  • strokeWidth -- Width of the stroke.
  • strokeCap -- End cap: StrokeCap.butt, .round, .square.
  • strokeJoin -- Join style: StrokeJoin.miter, .round, .bevel.
  • isAntiAlias -- Whether to apply anti-aliasing (default: true).
  • shader -- A gradient or image shader.
  • maskFilter -- Blur effects (e.g., MaskFilter.blur(BlurStyle.normal, 5)).
  • blendMode -- How the paint blends with the background.
final paint = Paint()
  ..color = Colors.blue
  ..style = PaintingStyle.stroke
  ..strokeWidth = 4
  ..strokeCap = StrokeCap.round
  ..shader = LinearGradient(colors: [Colors.blue, Colors.purple])
      .createShader(Rect.fromLTWH(0, 0, 200, 200));
Enter fullscreen mode Exit fullscreen mode

Q4. How do you draw a custom Path (complex shapes)?

Answer:
Path lets you define arbitrary shapes:

class TrianglePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final path = Path()
      ..moveTo(size.width / 2, 0)       // Top center
      ..lineTo(size.width, size.height)  // Bottom right
      ..lineTo(0, size.height)           // Bottom left
      ..close();                          // Back to start

    canvas.drawPath(path, Paint()..color = Colors.orange..style = PaintingStyle.fill);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Enter fullscreen mode Exit fullscreen mode

Path methods: moveTo, lineTo, quadraticBezierTo (one control point), cubicTo (two control points), arcToPoint, addRect, addOval, addRRect, close.


Q5. How do you animate a CustomPainter?

Answer:
Combine CustomPainter with an AnimationController. Pass the animation value to the painter and use shouldRepaint to trigger redraws.

class AnimatedCirclePainter extends CustomPainter {
  final double progress;
  AnimatedCirclePainter(this.progress);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.blue..style = PaintingStyle.stroke..strokeWidth = 6..strokeCap = StrokeCap.round;
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2 - 10;
    canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2, 2 * pi * progress, false, paint);
  }

  @override
  bool shouldRepaint(AnimatedCirclePainter old) => old.progress != progress;
}

// Usage with AnimatedBuilder:
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) => CustomPaint(
    size: Size(200, 200),
    painter: AnimatedCirclePainter(_controller.value),
  ),
);
Enter fullscreen mode Exit fullscreen mode

Wrap in RepaintBoundary to isolate repaints from the rest of the widget tree.


Q6. What is the difference between painter and foregroundPainter in CustomPaint?

Answer:

  • painter -- Draws behind the child widget.
  • foregroundPainter -- Draws in front of the child widget.
CustomPaint(
  painter: BackgroundPainter(),         // Drawn first (behind)
  foregroundPainter: OverlayPainter(),  // Drawn last (in front)
  child: Center(child: Text('Hello')),  // Drawn in between
)
Enter fullscreen mode Exit fullscreen mode

Use painter for backgrounds, grids, and decorations. Use foregroundPainter for overlays, selection highlights, and annotations.


Q7. How do you draw text on a Canvas?

Answer:
Use TextPainter to measure and draw text:

@override
void paint(Canvas canvas, Size size) {
  final textPainter = TextPainter(
    text: TextSpan(text: 'Hello Canvas', style: TextStyle(color: Colors.black, fontSize: 24)),
    textDirection: TextDirection.ltr,
  );
  textPainter.layout(maxWidth: size.width);
  final offset = Offset((size.width - textPainter.width) / 2, (size.height - textPainter.height) / 2);
  textPainter.paint(canvas, offset);
}
Enter fullscreen mode Exit fullscreen mode

You must call layout() before accessing width/height or painting.


Q8. What is ClipPath and how does it relate to custom painting?

Answer:
ClipPath clips its child to an arbitrary Path, creating custom-shaped widgets:

ClipPath(
  clipper: WaveClipper(),
  child: Container(height: 200, color: Colors.blue),
)

class WaveClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path()
      ..lineTo(0, size.height - 40)
      ..quadraticBezierTo(size.width / 4, size.height, size.width / 2, size.height - 30)
      ..quadraticBezierTo(3 * size.width / 4, size.height - 60, size.width, size.height - 20)
      ..lineTo(size.width, 0)
      ..close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
Enter fullscreen mode Exit fullscreen mode

Related: ClipRect (rectangle), ClipRRect (rounded rectangle), ClipOval (circle/oval).


Q9. How do you create a gradient using CustomPainter?

Answer:
Use Paint.shader with a gradient:

@override
void paint(Canvas canvas, Size size) {
  final rect = Rect.fromLTWH(0, 0, size.width, size.height);

  // Linear gradient
  final paint = Paint()
    ..shader = LinearGradient(
      begin: Alignment.topLeft, end: Alignment.bottomRight,
      colors: [Colors.blue, Colors.purple, Colors.pink],
    ).createShader(rect);
  canvas.drawRect(rect, paint);

  // Radial gradient
  final radialPaint = Paint()
    ..shader = RadialGradient(colors: [Colors.yellow, Colors.red]).createShader(rect);
  canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, radialPaint);

  // Sweep gradient
  final sweepPaint = Paint()
    ..shader = SweepGradient(colors: [Colors.red, Colors.green, Colors.blue, Colors.red]).createShader(rect);
  canvas.drawCircle(Offset(size.width / 2, size.height / 2), 80, sweepPaint);
}
Enter fullscreen mode Exit fullscreen mode

Q10. When should you use CustomPainter vs. composing existing widgets?

Answer:
Use existing widgets when possible. They handle hit testing, accessibility, and repainting efficiently. Use CustomPainter when:

  1. Complex shapes that cannot be achieved with Container, ClipPath, or DecoratedBox.
  2. Charts and graphs -- Line charts, pie charts, bar charts.
  3. Custom progress indicators -- Circular or arc-based progress.
  4. Drawing tools -- Freehand drawing, signature pads.
  5. Game-like graphics -- Custom sprites, animations.

Performance tips:

  • Use shouldRepaint correctly -- return false when nothing changed.
  • Wrap in RepaintBoundary to isolate repaints.
  • Cache complex Path objects instead of recreating every frame.
  • Use canvas.saveLayer sparingly -- it is expensive.

9. Responsive Design (MediaQuery, LayoutBuilder, OrientationBuilder)

Q1. What is MediaQuery and what information does it provide?

Answer:
MediaQuery provides information about the device's screen and user preferences. Access it via MediaQuery.of(context) or specific methods.

Key properties of MediaQueryData:

  • size -- Screen width and height.
  • devicePixelRatio -- Physical pixels per logical pixel.
  • padding -- System UI padding (status bar, notch, home indicator).
  • viewInsets -- Space occupied by the keyboard.
  • viewPadding -- Like padding but not reduced when keyboard appears.
  • orientation -- Portrait or landscape.
  • textScaler -- User's preferred text size multiplier.
  • platformBrightness -- Light or dark system theme.
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
final keyboardHeight = MediaQuery.viewInsetsOf(context).bottom;
Enter fullscreen mode Exit fullscreen mode

Performance tip: Use MediaQuery.sizeOf(context) instead of MediaQuery.of(context).size. The specific methods only rebuild when that specific property changes.


Q2. What is LayoutBuilder and how does it differ from MediaQuery?

Answer:

Feature MediaQuery LayoutBuilder
Provides Full screen dimensions Parent's constraints for this widget
Scope Global screen info Local layout constraints
When to use Breakpoints based on screen size Adapting to available space
LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth > 600) return _buildWideLayout();
    return _buildNarrowLayout();
  },
)
Enter fullscreen mode Exit fullscreen mode

Key insight: If your widget is in a 300px-wide column on a 1200px screen, MediaQuery reports 1200px but LayoutBuilder reports 300px. LayoutBuilder gives the more useful answer for that widget.


Q3. What is OrientationBuilder and when do you use it?

Answer:
OrientationBuilder rebuilds when the device orientation changes:

OrientationBuilder(
  builder: (context, orientation) {
    return GridView.count(
      crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
      children: items.map((item) => ItemCard(item)).toList(),
    );
  },
)
Enter fullscreen mode Exit fullscreen mode

Note: It determines orientation based on the parent's constraints (width > height = landscape), not device rotation. This works correctly in split-screen mode. For actual device orientation, use MediaQuery.orientationOf(context).


Q4. How do you build a responsive layout that adapts to phones, tablets, and desktops?

Answer:
Use breakpoints to switch between layouts:

class ResponsiveLayout extends StatelessWidget {
  final Widget mobile;
  final Widget? tablet;
  final Widget desktop;

  const ResponsiveLayout({required this.mobile, this.tablet, required this.desktop});

  static bool isMobile(BuildContext context) => MediaQuery.sizeOf(context).width < 600;
  static bool isTablet(BuildContext context) =>
      MediaQuery.sizeOf(context).width >= 600 && MediaQuery.sizeOf(context).width < 1200;
  static bool isDesktop(BuildContext context) => MediaQuery.sizeOf(context).width >= 1200;

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    if (width >= 1200) return desktop;
    if (width >= 600) return tablet ?? mobile;
    return mobile;
  }
}
Enter fullscreen mode Exit fullscreen mode

Common breakpoints (Material Design):

  • Compact (phone): < 600dp
  • Medium (tablet): 600dp - 839dp
  • Expanded (large tablet/desktop): 840dp+
  • Large (desktop): 1200dp+

Q5. What is FractionallySizedBox for responsive sizing?

Answer:
FractionallySizedBox sizes a widget as a fraction of its parent's size, independent of Row/Column:

FractionallySizedBox(
  widthFactor: 0.8,  // 80% of parent width
  heightFactor: 0.5, // 50% of parent height
  child: Container(color: Colors.blue),
)
Enter fullscreen mode Exit fullscreen mode

For responsive spacing:

final width = MediaQuery.sizeOf(context).width;
Padding(
  padding: EdgeInsets.symmetric(horizontal: width * 0.05),
  child: ...,
)
Enter fullscreen mode Exit fullscreen mode

Q6. How do you handle safe areas (notches, status bar, navigation bar)?

Answer:
Use the SafeArea widget:

Scaffold(
  body: SafeArea(
    child: Column(
      children: [Text('This text avoids the notch and status bar')],
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Properties: top, bottom, left, right (all default true), minimum (minimum padding).

For manual control: MediaQuery.paddingOf(context) gives exact inset values. Note: Scaffold already handles some safe area insets for AppBar and BottomNavigationBar.


Q7. How do you handle the keyboard covering input fields?

Answer:
Scaffold with resizeToAvoidBottomInset: true (default) automatically resizes the body to avoid the keyboard.

Scaffold(
  resizeToAvoidBottomInset: true,
  body: SingleChildScrollView(child: Form(...)),
)
Enter fullscreen mode Exit fullscreen mode

For manual handling, use MediaQuery.viewInsetsOf(context).bottom to get keyboard height:

Padding(
  padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
  child: ...,
)
Enter fullscreen mode Exit fullscreen mode

Q8. What is AspectRatio widget and when is it useful?

Answer:
AspectRatio forces its child to maintain a specific width-to-height ratio:

AspectRatio(
  aspectRatio: 16 / 9,
  child: Container(color: Colors.blue),
)
Enter fullscreen mode Exit fullscreen mode

Common use cases: video players (16:9), image placeholders, responsive cards. The widget calculates height automatically based on the available width.


Q9. What is IntrinsicHeight / IntrinsicWidth and when should you use them?

Answer:
IntrinsicHeight sizes its child to the child's intrinsic height (natural height). Use case: making all children in a Row as tall as the tallest child.

IntrinsicHeight(
  child: Row(
    children: [
      Container(width: 100, color: Colors.red, child: Text('Short')),
      VerticalDivider(), // Now knows how tall to be
      Container(width: 100, color: Colors.blue, child: Text('This is\na taller\nwidget')),
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

Caution: These require two layout passes and can be expensive. Avoid in performance-critical or deeply nested layouts.


Q10. How do you use Wrap and Flow for responsive layouts?

Answer:
Wrap flows children to the next line when space runs out:

Wrap(
  spacing: 8, runSpacing: 8,
  children: tags.map((tag) => Chip(label: Text(tag))).toList(),
)
Enter fullscreen mode Exit fullscreen mode

Flow uses a FlowDelegate for full control over child positioning. More efficient than Wrap for animations because it repositions without re-laying out children.

Rule of thumb: Use Wrap for simple responsive flow. Use Flow only for animated or highly custom positioning.


10. Images & Assets (Image widget, AssetImage, NetworkImage, cached_network_image)

Q1. What are the different ways to display images in Flutter?

Answer:
Flutter provides several Image constructors:

  • Image.asset -- From bundled assets.
  • Image.network -- From a URL.
  • Image.file -- From a device file.
  • Image.memory -- From Uint8List bytes.
Image.asset('assets/images/logo.png', width: 100, height: 100)

Image.network(
  'https://example.com/photo.jpg',
  width: 200, height: 200, fit: BoxFit.cover,
  loadingBuilder: (context, child, progress) {
    if (progress == null) return child;
    return CircularProgressIndicator();
  },
  errorBuilder: (context, error, stackTrace) => Icon(Icons.error),
)
Enter fullscreen mode Exit fullscreen mode

Q2. How do you add and configure asset images in Flutter?

Answer:

Step 1: Place images in assets/images/.

Step 2: Declare in pubspec.yaml:

flutter:
  assets:
    - assets/images/
Enter fullscreen mode Exit fullscreen mode

Step 3: Use: Image.asset('assets/images/logo.png')

Resolution-aware images: Place variants in subdirectories:

assets/images/logo.png          # 1x
assets/images/2.0x/logo.png     # 2x
assets/images/3.0x/logo.png     # 3x
Enter fullscreen mode Exit fullscreen mode

Flutter automatically selects the best resolution for the device.


Q3. What is BoxFit and what are its values?

Answer:

Value Behavior
BoxFit.fill Stretches to fill. May distort aspect ratio.
BoxFit.contain Fits entirely within box. May leave empty space.
BoxFit.cover Covers entire box. May crop. Most commonly used.
BoxFit.fitWidth Matches box width. May crop top/bottom.
BoxFit.fitHeight Matches box height. May crop sides.
BoxFit.none No scaling. Centers at original size.
BoxFit.scaleDown Like contain but never scales up.
Image.network(url, width: 200, height: 200, fit: BoxFit.cover)
Enter fullscreen mode Exit fullscreen mode

Q4. How do you cache network images in Flutter?

Answer:
Use the cached_network_image package:

CachedNetworkImage(
  imageUrl: 'https://example.com/photo.jpg',
  width: 200, height: 200, fit: BoxFit.cover,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
  memCacheWidth: 400,
)
Enter fullscreen mode Exit fullscreen mode

How it works: First load downloads and caches to disk. Subsequent loads use the disk/memory cache. Under the hood, it uses flutter_cache_manager.

Flutter's built-in Image.network only caches in memory (cleared when app closes). cached_network_image persists to disk.


Q5. What is ImageProvider and what are the common types?

Answer:
ImageProvider is the abstract class that identifies and loads image data.

Common types:

  • AssetImage('path') -- From assets, resolution-aware.
  • NetworkImage('url') -- From a URL.
  • FileImage(File(...)) -- From local file.
  • MemoryImage(bytes) -- From memory.
  • ResizeImage(provider, width, height) -- Resizes decoded image.
CircleAvatar(backgroundImage: NetworkImage(url))
Container(decoration: BoxDecoration(image: DecorationImage(image: AssetImage('assets/bg.jpg'), fit: BoxFit.cover)))
precacheImage(AssetImage('assets/hero.jpg'), context);
Enter fullscreen mode Exit fullscreen mode

Q6. How do you display a circular image (avatar)?

Answer:
Several approaches:

// CircleAvatar (simplest)
CircleAvatar(radius: 40, backgroundImage: NetworkImage(url))

// ClipOval
ClipOval(child: Image.network(url, width: 80, height: 80, fit: BoxFit.cover))

// Container with BoxDecoration
Container(
  width: 80, height: 80,
  decoration: BoxDecoration(
    shape: BoxShape.circle,
    image: DecorationImage(image: NetworkImage(url), fit: BoxFit.cover),
    border: Border.all(color: Colors.white, width: 3),
  ),
)

// ClipRRect for rounded rectangle
ClipRRect(
  borderRadius: BorderRadius.circular(16),
  child: Image.network(url, width: 100, height: 100, fit: BoxFit.cover),
)
Enter fullscreen mode Exit fullscreen mode

Q7. How do you handle image loading states and errors?

Answer:

// With Image.network
Image.network(
  url,
  loadingBuilder: (context, child, progress) {
    if (progress == null) return child;
    return CircularProgressIndicator(
      value: progress.expectedTotalBytes != null
          ? progress.cumulativeBytesLoaded / progress.expectedTotalBytes!
          : null,
    );
  },
  errorBuilder: (context, error, stack) => Icon(Icons.broken_image),
)

// FadeInImage for smooth transition
FadeInImage(
  placeholder: AssetImage('assets/placeholder.png'),
  image: NetworkImage(url),
  fadeInDuration: Duration(milliseconds: 300),
  fit: BoxFit.cover,
)

// cached_network_image with shimmer
CachedNetworkImage(
  imageUrl: url,
  placeholder: (ctx, url) => Shimmer.fromColors(
    baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100,
    child: Container(color: Colors.white),
  ),
  errorWidget: (ctx, url, err) => Icon(Icons.error),
)
Enter fullscreen mode Exit fullscreen mode

Q8. What is precacheImage and when should you use it?

Answer:
precacheImage pre-loads an image into the cache before it is needed, so it appears instantly when used.

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  precacheImage(AssetImage('assets/images/hero_banner.jpg'), context);
  precacheImage(NetworkImage('https://example.com/header.jpg'), context);
}
Enter fullscreen mode Exit fullscreen mode

Use cases: splash-to-home transitions, next-screen images, gallery pre-fetching. Call in didChangeDependencies() (not initState()) because it needs a valid BuildContext.


Q9. How do you optimize image performance in Flutter?

Answer:

  1. Resize to display size: Image.asset('photo.jpg', cacheWidth: 400) or ResizeImage(provider, width: 400).
  2. Use appropriate formats: WebP for smaller sizes; SVG for icons.
  3. Lazy loading: ListView.builder only builds visible items.
  4. Limit ImageCache: PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20;
  5. Use cached_network_image with memCacheWidth/memCacheHeight.
  6. Evict unused images: imageCache.evict(key) or imageCache.clear().

Q10. How do you display SVG images in Flutter?

Answer:
Use the flutter_svg package (Flutter has no native SVG support):

import 'package:flutter_svg/flutter_svg.dart';

// From asset
SvgPicture.asset(
  'assets/icons/logo.svg',
  width: 100, height: 100,
  colorFilter: ColorFilter.mode(Colors.blue, BlendMode.srcIn),
)

// From network
SvgPicture.network('https://example.com/icon.svg')

// From string
SvgPicture.string('<svg>...</svg>')
Enter fullscreen mode Exit fullscreen mode

When to use SVG vs PNG: SVG for icons, logos, simple illustrations (scales perfectly). PNG/WebP for photos and complex images (better performance). Alternative: convert SVGs to icon fonts for even better performance.


Top comments (0)