DartFlutter

10 Real-World Flutter UI Challenges and How to Solve Them

10 Real-World Flutter UI Challenges and How to Solve Them

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.

Leave a Comment