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')],
)
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.
-
Flexibleallows its child to be at most as large as the available space but can be smaller. It usesFlexFit.looseby default. -
Expandedis a shorthand forFlexible(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)
],
)
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.loosegives them the constraints of the Stack;StackFit.expandforces 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)),
),
],
)
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) orAxis.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')),
],
)
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: ABoxDecorationfor background color, border, border radius, gradient, box shadow, image, and shape. -
color: Background color (shorthand; cannot be used withdecoration). -
width/height: Fixed dimensions. -
constraints: Minimum and maximum width/height viaBoxConstraints. -
alignment: How to align the child within the container. -
transform: AMatrix4to 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)),
)
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:
-
Fixed-size box:
SizedBox(width: 100, height: 50, child: ...). -
Spacing in Row/Column:
SizedBox(width: 16)orSizedBox(height: 16)as a gap between children. -
Expand to fill:
SizedBox.expand()makes the child fill all available space. -
Shrink to nothing:
SizedBox.shrink()creates a zero-size box.
Column(
children: [
Text('First item'),
SizedBox(height: 20), // 20px vertical gap
Text('Second item'),
],
)
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:
- 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.
- Sizes go up: The child chooses its own size within those constraints and reports it back to the parent.
- 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
)
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.
-
Rowis essentiallyFlex(direction: Axis.horizontal, ...). -
Columnis essentiallyFlex(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()),
],
)
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'),
],
)
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:
-
Use
ExpandedorFlexibleto make children share available space proportionally. -
Wrap with
SingleChildScrollView(withscrollDirectionmatching the axis) to allow scrolling. -
Use
Wrapinstead to automatically flow children to the next line. -
Use
Overflow/clipBehaviorto clip overflowing content visually (though the content is still lost). -
Use
FittedBoxto scale down content to fit. -
Use
ExpandedwithText+overflow: TextOverflow.ellipsisfor text truncation.
Row(
children: [
Expanded(
child: Text(
'Very long text that might overflow the row',
overflow: TextOverflow.ellipsis,
),
),
Icon(Icons.chevron_right),
],
)
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:
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.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.ListView.separated(itemBuilder, separatorBuilder, itemCount)-- Likebuilderbut also takes aseparatorBuilderto insert dividers or spacing between items. Perfect for lists with dividers.ListView.custom(childrenDelegate)-- Takes aSliverChildDelegatefor 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(),
)
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')),
),
),
],
)
Q3. Explain GridView and its different constructors.
Answer:
GridView displays items in a 2D scrollable grid. It has several constructors:
GridView.count-- Creates a grid with a fixed number of columns (crossAxisCount). Simple and straightforward.GridView.extent-- Creates a grid where each item has a maximum cross-axis extent. Flutter calculates how many columns fit.GridView.builder-- Lazily builds items on demand. Takes agridDelegate(eitherSliverGridDelegateWithFixedCrossAxisCountorSliverGridDelegateWithMaxCrossAxisExtent) and anitemBuilder. Best for large datasets.GridView.custom-- Full control with aSliverChildDelegateandSliverGridDelegate.
// 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'))),
)
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,
),
),
],
)
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 adelegate(usuallySliverChildBuilderDelegateorSliverChildListDelegate).SliverGrid-- A sliver that displays a 2D grid. Takes both adelegateand agridDelegate.-
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 withfloatingto fully animate in/out. -
flexibleSpace: Usually aFlexibleSpaceBarwith a background image or title.
-
SliverToBoxAdapter-- Wraps a regular (non-sliver) widget so it can be placed inside aCustomScrollView. 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)),
],
)
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
ListViewis nested inside another scrollable or inside aColumnand 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
Expandedinside aColumn. - Use
CustomScrollViewwith slivers to combine multiple scrollable areas. - Use
NeverScrollableScrollPhysics()withshrinkWrap: trueif 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(...)),
],
)
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)orcontroller.jumpTo(offset). -
Scroll to top/bottom:
controller.animateTo(0)orcontroller.animateTo(controller.position.maxScrollExtent). -
Listen to scroll position:
controller.addListener(callback)orcontroller.offsetto 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])),
);
}
}
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();
}
});
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(...),
)
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 aListViewinside another scrollable. -
AlwaysScrollableScrollPhysics-- Always allows scrolling even when content fits on screen. Useful for pull-to-refresh. -
PageScrollPhysics-- Snaps to page boundaries. Used internally byPageView.
ListView(
physics: BouncingScrollPhysics(), // Always bounce like iOS
children: [...],
)
ListView(
physics: NeverScrollableScrollPhysics(), // Disable scrolling
shrinkWrap: true,
children: [...],
)
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 byitemExtent). 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 instantjumpTofor 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,
),
)
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
);
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');
Passing arguments:
Navigator.pushNamed(context, '/details', arguments: {'id': 42});
// In DetailsScreen:
final args = ModalRoute.of(context)!.settings.arguments as Map;
Limitations of named routes:
-
No type safety -- Arguments are passed as
Object?, requiring casting. - No compile-time checking -- Typos in route names cause runtime errors.
- Difficult deep linking -- Cannot easily parse URL parameters.
- Cannot dynamically control the route stack -- Limited declarative control.
- 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());
}
},
)
Advantages over the routes map:
- Argument parsing -- You can extract and validate arguments in one place.
-
404 handling -- The
defaultcase handles unknown routes. -
Dynamic routes -- You can parse path patterns (e.g.,
/user/:id). -
Transition customization -- Return different route types (e.g.,
CupertinoPageRoute). - 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:
-
Routerwidget -- Configures routing. -
RouteInformationParser-- Parses route information (URL) into app-specific state. -
RouterDelegate-- Builds theNavigatorwidget based on app state. -
RouteInformationProvider-- Provides route information from the platform.
MaterialApp.router(
routerDelegate: myRouterDelegate,
routeInformationParser: myRouteInformationParser,
)
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();
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),
));
2. Named route arguments:
Navigator.pushNamed(context, '/detail', arguments: myItem);
// Receive: ModalRoute.of(context)!.settings.arguments as MyItem
3. Return data with pop:
// Push and await result
final result = await Navigator.push(context, MaterialPageRoute(
builder: (_) => SelectionScreen(),
));
// In SelectionScreen:
Navigator.pop(context, selectedValue);
4. Path/query parameters (go_router):
context.go('/detail/42?tab=reviews');
// Read: state.pathParameters['id'], state.uri.queryParameters['tab']
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(...),
)
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: [...],
),
)
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()),
],
)
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:
-
Configure platform (Android/iOS):
- Android: Add intent filters in
AndroidManifest.xmlfor your URL schemes. - iOS: Add URL types in
Info.plistand Associated Domains for universal links.
- Android: Add intent filters in
-
Handle in Flutter:
- With
go_router, deep links are handled automatically -- URLs map to your route definitions. - With Navigator 1.0, use
onGenerateRouteto parse incoming paths.
- With
Test: Use
adb(Android) orxcrun(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);
},
),
],
);
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),
),
);
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);
},
),
)
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:
TextFieldis a standalone text input widget. It uses aTextEditingControllerto read/set its value. It has anonChangedcallback for reacting to changes. It does not integrate withForm.TextFormFieldis aTextFieldwrapped in aFormField. It adds avalidatorproperty and integrates with theFormwidget. WhenForm.validate()is called, allTextFormFieldvalidators run automatically. It also supportsonSavedcallback.
// 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'),
)
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:
- Create a
GlobalKey<FormState>. - Wrap
TextFormFieldwidgets in aFormwidget. - Call
formKey.currentState!.validate()to trigger all validators. - Call
formKey.currentState!.save()to trigger allonSavedcallbacks. - 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'),
),
],
),
);
}
}
autovalidateMode options:
-
AutovalidateMode.disabled-- Only validate whenvalidate()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'),
),
],
);
}
}
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()orFocusScope.of(context).unfocus(). -
Listen to focus changes:
focusNode.addListener(callback)or usehasFocus. -
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(),
),
],
);
}
}
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();
2. Request focus on an empty FocusNode (older approach):
FocusScope.of(context).requestFocus(FocusNode()); // Not recommended anymore
3. Wrap the Scaffold body in a GestureDetector:
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
body: Form(...),
),
)
4. Use FocusManager:
FocusManager.instance.primaryFocus?.unfocus();
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])),
),
),
],
);
}
}
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, orInputBorder.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,
),
)
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!,
)
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()],
)
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 whenForm.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
],
),
)
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));
Disabled state: Pass null to onPressed to disable any button:
ElevatedButton(onPressed: null, child: Text('Disabled'));
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'),
)
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'),
)
Global button theme:
ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
)
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'),
)
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'),
)
Alternatively, manually compose with a Row:
ElevatedButton(
onPressed: () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.send),
SizedBox(width: 8),
Text('Send'),
],
),
)
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 aMaterialancestor in the widget tree. It supportsonTap,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,
),
)
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'),
);
}
}
Key points:
- Setting
onPressedtonulldisables the button during loading. - Use
SizedBoxto constrain the progress indicator's size. - Check
mountedbefore callingsetStatein 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'),
)
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'),
)
Circular button:
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(20),
),
onPressed: () {},
child: Icon(Icons.add),
)
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'),
),
)
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')),
],
);
}
}
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),
)
Properties:
-
itemBuilder-- Returns the list ofPopupMenuEntryitems. -
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 atitle,content, andactions(buttons). Used for "Are you sure?" prompts, error messages, etc. -
SimpleDialog-- For choosing from a list of options. Has atitleandchildren(usuallySimpleDialogOptionwidgets).
// 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')),
],
),
);
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: () {}),
],
),
),
);
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')),
),
),
);
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),
),
);
Key properties:
-
content-- The main message (usually aTextwidget). -
action-- An optional action button (e.g., "Undo"). -
duration-- How long the SnackBar stays visible. -
behavior--SnackBarBehavior.fixed(default, anchored to bottom) orSnackBarBehavior.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();
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')),
)
Opening/closing programmatically:
Scaffold.of(context).openDrawer(); // Open drawer
Scaffold.of(context).openEndDrawer(); // Open end drawer
Navigator.pop(context); // Close drawer
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'),
),
),
],
),
),
),
);
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')),
);
},
);
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')),
),
);
},
),
);
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),
);
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),
),
),
);
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'),
),
],
),
);
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) => ...,
);
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: () {},
),
)
Many Material widgets (like IconButton, FloatingActionButton) have a built-in tooltip property:
IconButton(
icon: Icon(Icons.delete),
tooltip: 'Delete this item',
onPressed: () {},
)
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(),
)
Access in a widget:
final theme = Theme.of(context);
Text('Hello', style: theme.textTheme.headlineLarge);
Container(color: theme.colorScheme.primary);
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
),
)
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
)
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);
You can use Google Fonts easily:
ThemeData(textTheme: GoogleFonts.poppinsTextTheme())
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),
),
)
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'),
],
),
)
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
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));
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));
Q9. What is the difference between primaryColor, colorScheme.primary, and primarySwatch?
Answer:
-
primarySwatch(Material 2, deprecated pattern) -- AMaterialColorwith shades (50-900). Was used to generate multiple theme colors. Not recommended in Material 3. -
primaryColor(legacy) -- A singleColorfor the app's primary brand color. Many widgets no longer read this in Material 3. -
colorScheme.primary(recommended) -- The primary color in theColorScheme. 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
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,
),
)
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)-- Returnstrueif 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')),
)
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) orPaintingStyle.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));
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;
}
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),
),
);
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
)
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);
}
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;
}
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);
}
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:
-
Complex shapes that cannot be achieved with
Container,ClipPath, orDecoratedBox. - Charts and graphs -- Line charts, pie charts, bar charts.
- Custom progress indicators -- Circular or arc-based progress.
- Drawing tools -- Freehand drawing, signature pads.
- Game-like graphics -- Custom sprites, animations.
Performance tips:
- Use
shouldRepaintcorrectly -- returnfalsewhen nothing changed. - Wrap in
RepaintBoundaryto isolate repaints. - Cache complex
Pathobjects instead of recreating every frame. - Use
canvas.saveLayersparingly -- 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-- Likepaddingbut 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;
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();
},
)
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(),
);
},
)
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;
}
}
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),
)
For responsive spacing:
final width = MediaQuery.sizeOf(context).width;
Padding(
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
child: ...,
)
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')],
),
),
)
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(...)),
)
For manual handling, use MediaQuery.viewInsetsOf(context).bottom to get keyboard height:
Padding(
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
child: ...,
)
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),
)
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')),
],
),
)
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(),
)
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-- FromUint8Listbytes.
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),
)
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/
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
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)
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,
)
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);
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),
)
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),
)
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);
}
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:
-
Resize to display size:
Image.asset('photo.jpg', cacheWidth: 400)orResizeImage(provider, width: 400). - Use appropriate formats: WebP for smaller sizes; SVG for icons.
-
Lazy loading:
ListView.builderonly builds visible items. -
Limit ImageCache:
PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; - Use
cached_network_imagewithmemCacheWidth/memCacheHeight. -
Evict unused images:
imageCache.evict(key)orimageCache.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>')
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)