
Even the best Flutter developers run into frustrating UI challenges. From tricky layouts to performance bottlenecks and platform-specific quirks, building polished apps in Flutter isn’t always smooth sailing. After years of building production Flutter apps, I’ve encountered these issues repeatedly—and developed reliable patterns to solve them. This post covers 10 real-world Flutter UI challenges with comprehensive code solutions you can copy directly into your projects.
1. Pixel-Perfect Alignment Across Devices
The Problem: Your UI looks great on one screen but breaks on another. Padding, spacing, and alignment feel off across device sizes and densities. What looks perfect on an iPhone 14 Pro becomes cramped on an SE or stretched on an iPad.
The Solution: Build a responsive design system that scales proportionally across all devices.
// Create a responsive utility class
class Responsive {
static late double _screenWidth;
static late double _screenHeight;
static late double _blockWidth;
static late double _blockHeight;
static void init(BuildContext context) {
final size = MediaQuery.sizeOf(context);
_screenWidth = size.width;
_screenHeight = size.height;
_blockWidth = _screenWidth / 100;
_blockHeight = _screenHeight / 100;
}
// Percentage-based sizing
static double width(double percentage) => _blockWidth * percentage;
static double height(double percentage) => _blockHeight * percentage;
// Scaled font sizes based on screen width
static double fontSize(double size) {
final scaleFactor = _screenWidth / 375; // iPhone 14 baseline
return size * scaleFactor.clamp(0.8, 1.3);
}
// Breakpoint detection
static bool get isMobile => _screenWidth < 600;
static bool get isTablet => _screenWidth >= 600 && _screenWidth < 1024;
static bool get isDesktop => _screenWidth >= 1024;
// Adaptive value based on screen size
static T adaptive({
required T mobile,
T? tablet,
T? desktop,
}) {
if (isDesktop) return desktop ?? tablet ?? mobile;
if (isTablet) return tablet ?? mobile;
return mobile;
}
}
// Usage in widgets
class ResponsiveCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
Responsive.init(context);
return Container(
width: Responsive.adaptive(
mobile: Responsive.width(90),
tablet: Responsive.width(45),
desktop: Responsive.width(30),
),
padding: EdgeInsets.all(Responsive.width(4)),
child: Column(
children: [
Text(
'Responsive Title',
style: TextStyle(
fontSize: Responsive.fontSize(18),
fontWeight: FontWeight.bold,
),
),
SizedBox(height: Responsive.height(2)),
Text(
'This card adapts to any screen size',
style: TextStyle(fontSize: Responsive.fontSize(14)),
),
],
),
);
}
}
// LayoutBuilder for constraint-aware layouts
class AdaptiveGrid extends StatelessWidget {
final List children;
const AdaptiveGrid({required this.children});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final crossAxisCount = width < 600 ? 2 : width < 1024 ? 3 : 4;
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.2,
),
itemCount: children.length,
itemBuilder: (context, index) => children[index],
);
},
);
}
}
2. Overflow and Clipping Issues
The Problem: UI elements overflow their containers, especially on smaller devices or with long text. The dreaded yellow-black overflow stripes appear, or content gets cut off unexpectedly.
The Solution: Use proper flex widgets and text overflow handling.
// Comprehensive overflow prevention
class SafeListTile extends StatelessWidget {
final String title;
final String subtitle;
final String? trailing;
final VoidCallback? onTap;
const SafeListTile({
required this.title,
required this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Expanded ensures text takes available space
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
if (trailing != null) ...[
const SizedBox(width: 12),
// Flexible with bounded constraints
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 100),
child: Text(
trailing!,
style: Theme.of(context).textTheme.labelMedium,
textAlign: TextAlign.end,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
);
}
}
// Scrollable form that handles keyboard appearance
class SafeForm extends StatelessWidget {
final List children;
final GlobalKey formKey;
const SafeForm({
required this.children,
required this.formKey,
});
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: SingleChildScrollView(
// Automatically scrolls to focused field
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.only(
bottom: MediaQuery.viewInsetsOf(context).bottom + 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
),
),
);
}
}
// FittedBox for scaling content to fit
class ScalableText extends StatelessWidget {
final String text;
final TextStyle? style;
const ScalableText(this.text, {this.style});
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(text, style: style),
);
}
}
3. Poor Scroll Performance
The Problem: Your list views lag, stutter, or load slowly. Scrolling feels janky, especially with images or complex item layouts.
The Solution: Use proper lazy loading, caching, and widget optimization.
// Optimized list with pagination and caching
class OptimizedProductList extends StatefulWidget {
@override
State createState() => _OptimizedProductListState();
}
class _OptimizedProductListState extends State {
final ScrollController _scrollController = ScrollController();
final List _products = [];
bool _isLoading = false;
bool _hasMore = true;
int _page = 1;
@override
void initState() {
super.initState();
_loadProducts();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadProducts();
}
}
Future _loadProducts() async {
if (_isLoading || !_hasMore) return;
setState(() => _isLoading = true);
final newProducts = await ProductApi.fetch(page: _page);
setState(() {
_products.addAll(newProducts);
_page++;
_isLoading = false;
_hasMore = newProducts.length >= 20;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
// Add cacheExtent for smoother scrolling
cacheExtent: 500,
itemCount: _products.length + (_hasMore ? 1 : 0),
// Use itemExtent if items have fixed height
// itemExtent: 120,
itemBuilder: (context, index) {
if (index >= _products.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
}
return ProductListItem(
key: ValueKey(_products[index].id),
product: _products[index],
);
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
// Optimized list item with RepaintBoundary
class ProductListItem extends StatelessWidget {
final Product product;
const ProductListItem({super.key, required this.product});
@override
Widget build(BuildContext context) {
// RepaintBoundary prevents unnecessary repaints
return RepaintBoundary(
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
// Cached network image with placeholder
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(12),
),
child: CachedNetworkImage(
imageUrl: product.imageUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[200],
child: const Icon(Icons.error),
),
// Memory cache settings
memCacheWidth: 200, // Cache at 2x for retina
memCacheHeight: 200,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'\$${product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
);
}
}
4. Janky Animations
The Problem: Your animations aren’t smooth or feel choppy. Frame drops make the app feel unpolished.
The Solution: Use efficient animation builders and avoid rebuilding unchanged widgets.
// Optimized fade-in animation
class FadeInWidget extends StatefulWidget {
final Widget child;
final Duration duration;
final Duration delay;
const FadeInWidget({
required this.child,
this.duration = const Duration(milliseconds: 300),
this.delay = Duration.zero,
});
@override
State createState() => _FadeInWidgetState();
}
class _FadeInWidgetState extends State
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation _fadeAnimation;
late final Animation _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
_slideAnimation = Tween(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
));
Future.delayed(widget.delay, () {
if (mounted) _controller.forward();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// AnimatedBuilder only rebuilds this subtree
return AnimatedBuilder(
animation: _controller,
// Child is cached and not rebuilt
child: widget.child,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: child,
),
);
},
);
}
}
// Staggered list animation
class StaggeredAnimatedList extends StatelessWidget {
final List children;
const StaggeredAnimatedList({required this.children});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: children.length,
itemBuilder: (context, index) {
return FadeInWidget(
delay: Duration(milliseconds: index * 50),
child: children[index],
);
},
);
}
}
// Implicit animation for simple state changes
class AnimatedCard extends StatelessWidget {
final bool isSelected;
final Widget child;
final VoidCallback onTap;
const AnimatedCard({
required this.isSelected,
required this.child,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
transform: Matrix4.identity()
..scale(isSelected ? 1.02 : 1.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isSelected ? 0.2 : 0.1),
blurRadius: isSelected ? 12 : 6,
offset: Offset(0, isSelected ? 6 : 3),
),
],
),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: TextStyle(
color: isSelected ? Colors.blue : Colors.black,
),
child: child,
),
),
);
}
}
You can debug animation performance using Flutter DevTools.
5. Inconsistent Theming
The Problem: Some widgets ignore your app’s theme or appear differently on iOS vs Android. Custom widgets don’t pick up theme changes.
The Solution: Create a comprehensive theme system with proper extensions.
// Custom theme extension for app-specific colors
@immutable
class AppColors extends ThemeExtension {
final Color? success;
final Color? warning;
final Color? info;
final Color? cardBackground;
final Color? divider;
const AppColors({
this.success,
this.warning,
this.info,
this.cardBackground,
this.divider,
});
@override
AppColors copyWith({
Color? success,
Color? warning,
Color? info,
Color? cardBackground,
Color? divider,
}) {
return AppColors(
success: success ?? this.success,
warning: warning ?? this.warning,
info: info ?? this.info,
cardBackground: cardBackground ?? this.cardBackground,
divider: divider ?? this.divider,
);
}
@override
AppColors lerp(AppColors? other, double t) {
if (other is! AppColors) return this;
return AppColors(
success: Color.lerp(success, other.success, t),
warning: Color.lerp(warning, other.warning, t),
info: Color.lerp(info, other.info, t),
cardBackground: Color.lerp(cardBackground, other.cardBackground, t),
divider: Color.lerp(divider, other.divider, t),
);
}
}
// Complete theme configuration
class AppTheme {
static ThemeData light() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
// Consistent button styling
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// Consistent card styling
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
// Input decoration
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
extensions: const [
AppColors(
success: Color(0xFF4CAF50),
warning: Color(0xFFFF9800),
info: Color(0xFF2196F3),
cardBackground: Colors.white,
divider: Color(0xFFE0E0E0),
),
],
);
}
static ThemeData dark() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
extensions: const [
AppColors(
success: Color(0xFF81C784),
warning: Color(0xFFFFB74D),
info: Color(0xFF64B5F6),
cardBackground: Color(0xFF1E1E1E),
divider: Color(0xFF424242),
),
],
);
}
}
// Easy access extension
extension AppThemeExtension on BuildContext {
AppColors get appColors => Theme.of(this).extension()!;
}
// Usage
class ThemedWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: context.appColors.cardBackground,
child: Column(
children: [
Icon(Icons.check, color: context.appColors.success),
Divider(color: context.appColors.divider),
],
),
);
}
}
6. Bottom Overflow When Keyboard Opens
The Problem: The keyboard hides inputs or overflows the view. Form fields become inaccessible when the user types.
The Solution: Properly configure scaffold and scroll behavior.
// Complete keyboard-aware form screen
class LoginScreen extends StatefulWidget {
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
final _formKey = GlobalKey();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_emailFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// This ensures the scaffold resizes when keyboard appears
resizeToAvoidBottomInset: true,
appBar: AppBar(title: const Text('Login')),
body: SafeArea(
child: GestureDetector(
// Dismiss keyboard on tap outside
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
// Reverse keeps submit button visible
// reverse: true,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.fromLTRB(
24,
24,
24,
// Extra padding when keyboard is visible
MediaQuery.viewInsetsOf(context).bottom + 24,
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_passwordFocus.requestFocus();
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
},
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _submit,
child: const Text('Login'),
),
],
),
),
),
),
),
);
}
void _submit() {
if (_formKey.currentState?.validate() ?? false) {
// Handle login
}
}
}
7. Stack Widgets Not Layering Correctly
The Problem: Widgets appear under others even when declared later. Elevation doesn't work as expected.
The Solution: Understand Stack ordering and use proper Material widgets.
// Properly layered card with floating action
class LayeredProductCard extends StatelessWidget {
final Product product;
const LayeredProductCard({required this.product});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: Stack(
clipBehavior: Clip.none, // Allow overflow for badges
children: [
// Base layer - the card
Positioned.fill(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
Text(
'\$${product.price}',
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
),
),
// Middle layer - discount badge (positioned outside)
if (product.discount > 0)
Positioned(
top: -10,
right: 10,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(20),
color: Colors.red,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Text(
'-${product.discount}%',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Top layer - floating action button
Positioned(
bottom: -20,
right: 20,
child: Material(
type: MaterialType.transparency,
child: FloatingActionButton.small(
heroTag: 'add_${product.id}',
onPressed: () {},
child: const Icon(Icons.add),
),
),
),
],
),
);
}
}
8. Misaligned List Items in RTL (Arabic, Hebrew)
The Problem: Right-to-left languages cause layout issues. Icons and text appear in wrong positions.
The Solution: Use directional widgets and avoid hardcoded left/right values.
// RTL-aware list item
class DirectionalListItem extends StatelessWidget {
final IconData leadingIcon;
final String title;
final String subtitle;
final VoidCallback? onTap;
const DirectionalListItem({
required this.leadingIcon,
required this.title,
required this.subtitle,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
// Use EdgeInsetsDirectional instead of EdgeInsets
padding: const EdgeInsetsDirectional.only(
start: 16, // Becomes left in LTR, right in RTL
end: 16,
top: 12,
bottom: 12,
),
child: Row(
children: [
Icon(leadingIcon),
const SizedBox(width: 16),
Expanded(
child: Column(
// Use CrossAxisAlignment.start, not .left
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
// TextAlign.start respects direction
textAlign: TextAlign.start,
),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
// Use directional icons
Icon(
// This icon flips automatically in RTL
Directionality.of(context) == TextDirection.rtl
? Icons.chevron_left
: Icons.chevron_right,
),
],
),
),
);
}
}
// Positioned with directional awareness
class DirectionalPositioned extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Use PositionedDirectional instead of Positioned
PositionedDirectional(
start: 16, // Respects text direction
top: 16,
child: const Text('Badge'),
),
],
);
}
}
9. Hard to Debug Layout Shifts
The Problem: You can't figure out why widgets suddenly shift or grow. Layout behaves unexpectedly.
The Solution: Use debugging tools and understand constraints.
// Debug wrapper to visualize constraints
class DebugConstraints extends StatelessWidget {
final Widget child;
final bool enabled;
const DebugConstraints({
required this.child,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
if (!enabled || !kDebugMode) return child;
return LayoutBuilder(
builder: (context, constraints) {
debugPrint('Constraints: $constraints');
return Stack(
children: [
child,
Positioned(
top: 0,
left: 0,
child: Container(
color: Colors.red.withOpacity(0.7),
padding: const EdgeInsets.all(4),
child: Text(
'min: ${constraints.minWidth.toInt()}x${constraints.minHeight.toInt()}\n'
'max: ${constraints.maxWidth.toInt()}x${constraints.maxHeight.toInt()}',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
),
),
],
);
},
);
}
}
// Common constraint issues and fixes
class ConstraintFixes extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// PROBLEM: Unbounded height in Column
// Expanded(
// child: ListView(), // Error: unbounded height
// ),
// FIX: Give ListView explicit bounds
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
),
// PROBLEM: Row with multiple Expanded children
// Row(children: [Expanded(child: Text('Long...')), Expanded(child: Text('Long...'))]),
// FIX: Use Flexible with flex ratios
Row(
children: [
Flexible(
flex: 2,
child: Text('Takes 2/3', overflow: TextOverflow.ellipsis),
),
Flexible(
flex: 1,
child: Text('Takes 1/3', overflow: TextOverflow.ellipsis),
),
],
),
],
);
}
}
10. Web-Specific Bugs
The Problem: Your UI behaves differently in Flutter Web. Hover states don't work, scrolling feels different, and some widgets render incorrectly.
The Solution: Add web-specific handling and use appropriate renderers.
import 'package:flutter/foundation.dart' show kIsWeb;
// Platform-aware button with hover effects
class AdaptiveButton extends StatefulWidget {
final Widget child;
final VoidCallback onPressed;
const AdaptiveButton({
required this.child,
required this.onPressed,
});
@override
State createState() => _AdaptiveButtonState();
}
class _AdaptiveButtonState extends State {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
Widget button = ElevatedButton(
onPressed: widget.onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: _isHovered
? Theme.of(context).primaryColor.withOpacity(0.8)
: null,
),
child: widget.child,
);
// Only add MouseRegion for web/desktop
if (kIsWeb) {
button = MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: button,
);
}
return button;
}
}
// Web-optimized scrolling
class WebOptimizedList extends StatelessWidget {
final List children;
const WebOptimizedList({required this.children});
@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: kIsWeb
? ScrollConfiguration.of(context).copyWith(
dragDevices: {
// Enable mouse drag on web
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
scrollbars: true,
)
: ScrollConfiguration.of(context),
child: ListView(
children: children,
),
);
}
}
// Platform-specific widget selection
class PlatformWidget extends StatelessWidget {
final Widget mobile;
final Widget? web;
final Widget? desktop;
const PlatformWidget({
required this.mobile,
this.web,
this.desktop,
});
@override
Widget build(BuildContext context) {
if (kIsWeb) return web ?? desktop ?? mobile;
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.macOS ||
platform == TargetPlatform.windows ||
platform == TargetPlatform.linux) {
return desktop ?? mobile;
}
return mobile;
}
}
Common Mistakes to Avoid
Using setState Excessively
Every setState call rebuilds the entire widget. For complex UIs, use proper state management solutions like Riverpod or BLoC to minimize rebuilds.
Ignoring const Constructors
Widgets without const can't be cached by Flutter. Always use const where possible to enable Flutter's optimization.
Not Testing on Multiple Screen Sizes
Use Flutter's device preview tools and test on actual devices. What works in the simulator often breaks on real hardware.
Hardcoding Dimensions
Avoid fixed pixel values. Use MediaQuery, LayoutBuilder, or responsive utilities to adapt to any screen.
Final Thoughts
UI challenges in Flutter are part of the journey. Whether you're building for mobile, web, or desktop, keeping your code adaptable, responsive, and well-structured is key. The patterns in this article address the most common issues I've encountered across dozens of production apps.
For debugging these issues effectively, learn to use Flutter DevTools shortcuts. And for more Flutter best practices, explore the official Flutter documentation.