Flutter Layouts & Responsive Design

Building a Fully Responsive App

60 min Lesson 16 of 16

Capstone: Responsive Dashboard App

In this capstone lesson, we bring together every layout concept from this tutorial to build a fully responsive dashboard application. This dashboard adapts seamlessly across phones, tablets, and desktops with adaptive navigation, responsive grids, a collapsing app bar, a master-detail pattern, and proper safe area handling.

What We’ll Build: A complete analytics dashboard with: (1) adaptive navigation that switches between bottom nav, navigation rail, and full drawer, (2) a responsive grid that adjusts columns based on screen width, (3) a collapsing app bar with search, (4) a master-detail pattern for data exploration, and (5) safe area handling throughout.

Step 1: Responsive Utilities

First, let’s create the foundational utilities that the entire app will use — breakpoints, device type detection, and a responsive builder widget.

responsive_utils.dart

import 'package:flutter/material.dart';

enum DeviceType { phone, tablet, desktop }

class Breakpoints {
  static const double phone = 0;
  static const double tablet = 600;
  static const double desktop = 1024;
  static const double desktopLarge = 1440;

  static DeviceType getDeviceType(double width) {
    if (width >= desktop) return DeviceType.desktop;
    if (width >= tablet) return DeviceType.tablet;
    return DeviceType.phone;
  }

  static int getGridColumns(double width) {
    if (width >= desktopLarge) return 4;
    if (width >= desktop) return 3;
    if (width >= tablet) return 2;
    return 1;
  }

  static double getContentMaxWidth(double width) {
    if (width >= desktopLarge) return 1200;
    if (width >= desktop) return 960;
    return double.infinity;
  }
}

class ResponsiveBuilder extends StatelessWidget {
  final Widget Function(BuildContext, DeviceType, double) builder;

  const ResponsiveBuilder({super.key, required this.builder});

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final deviceType = Breakpoints.getDeviceType(width);
    return builder(context, deviceType, width);
  }
}

class ResponsiveConstrainedBox extends StatelessWidget {
  final Widget child;

  const ResponsiveConstrainedBox({
    super.key,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final maxWidth = Breakpoints.getContentMaxWidth(width);

    return Center(
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: maxWidth),
        child: child,
      ),
    );
  }
}

Step 2: App Theme and Constants

Define a consistent theme and data models used throughout the dashboard.

dashboard_theme.dart

import 'package:flutter/material.dart';

class DashboardTheme {
  static ThemeData get lightTheme => ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.indigo,
    brightness: Brightness.light,
    cardTheme: CardTheme(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
        side: BorderSide(color: Colors.grey.shade200),
      ),
    ),
    appBarTheme: const AppBarTheme(
      centerTitle: false,
      elevation: 0,
    ),
  );
}

class DashboardItem {
  final String title;
  final String value;
  final IconData icon;
  final Color color;
  final double change;

  const DashboardItem({
    required this.title,
    required this.value,
    required this.icon,
    required this.color,
    required this.change,
  });
}

class ActivityItem {
  final String title;
  final String description;
  final String time;
  final IconData icon;

  const ActivityItem({
    required this.title,
    required this.description,
    required this.time,
    required this.icon,
  });
}

// Sample data
const List<DashboardItem> kDashboardStats = [
  DashboardItem(
    title: 'Total Users',
    value: '24,563',
    icon: Icons.people,
    color: Colors.blue,
    change: 12.5,
  ),
  DashboardItem(
    title: 'Revenue',
    value: '\$48,290',
    icon: Icons.attach_money,
    color: Colors.green,
    change: 8.3,
  ),
  DashboardItem(
    title: 'Orders',
    value: '1,847',
    icon: Icons.shopping_cart,
    color: Colors.orange,
    change: -2.1,
  ),
  DashboardItem(
    title: 'Conversion',
    value: '3.24%',
    icon: Icons.trending_up,
    color: Colors.purple,
    change: 5.7,
  ),
];

const List<ActivityItem> kRecentActivity = [
  ActivityItem(
    title: 'New user registered',
    description: 'John Doe created an account',
    time: '2 min ago',
    icon: Icons.person_add,
  ),
  ActivityItem(
    title: 'Order completed',
    description: 'Order #1234 was delivered',
    time: '15 min ago',
    icon: Icons.check_circle,
  ),
  ActivityItem(
    title: 'Payment received',
    description: '\$250.00 from Jane Smith',
    time: '1 hour ago',
    icon: Icons.payment,
  ),
  ActivityItem(
    title: 'New review',
    description: '5-star review on Product A',
    time: '2 hours ago',
    icon: Icons.star,
  ),
  ActivityItem(
    title: 'Inventory alert',
    description: 'Product B is running low',
    time: '3 hours ago',
    icon: Icons.warning,
  ),
  ActivityItem(
    title: 'Campaign launched',
    description: 'Summer Sale campaign is live',
    time: '5 hours ago',
    icon: Icons.campaign,
  ),
];

Step 3: Adaptive Navigation Shell

The navigation shell is the heart of our responsive app. It switches between a bottom navigation bar on phones, a compact navigation rail on tablets, and a full extended navigation rail (drawer-like) on desktops.

adaptive_shell.dart

import 'package:flutter/material.dart';

class AdaptiveShell extends StatefulWidget {
  const AdaptiveShell({super.key});

  @override
  State<AdaptiveShell> createState() => _AdaptiveShellState();
}

class _AdaptiveShellState extends State<AdaptiveShell> {
  int _selectedIndex = 0;

  static const _navItems = [
    (icon: Icons.dashboard, label: 'Dashboard'),
    (icon: Icons.analytics, label: 'Analytics'),
    (icon: Icons.people, label: 'Users'),
    (icon: Icons.settings, label: 'Settings'),
  ];

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        return Scaffold(
          body: Row(
            children: [
              // Desktop: Extended navigation rail (like a drawer)
              if (deviceType == DeviceType.desktop)
                NavigationRail(
                  extended: width >= Breakpoints.desktopLarge,
                  minExtendedWidth: 220,
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  leading: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 16),
                    child: width >= Breakpoints.desktopLarge
                        ? const Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Icon(Icons.dashboard_customize,
                                  color: Colors.indigo),
                              SizedBox(width: 8),
                              Text(
                                'Dashboard',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ],
                          )
                        : const Icon(Icons.dashboard_customize,
                            color: Colors.indigo, size: 32),
                  ),
                  destinations: _navItems
                      .map((item) => NavigationRailDestination(
                            icon: Icon(item.icon),
                            label: Text(item.label),
                          ))
                      .toList(),
                )

              // Tablet: Compact navigation rail
              else if (deviceType == DeviceType.tablet)
                NavigationRail(
                  extended: false,
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  leading: const Padding(
                    padding: EdgeInsets.symmetric(vertical: 8),
                    child: Icon(Icons.dashboard_customize,
                        color: Colors.indigo, size: 28),
                  ),
                  destinations: _navItems
                      .map((item) => NavigationRailDestination(
                            icon: Icon(item.icon),
                            label: Text(item.label),
                          ))
                      .toList(),
                ),

              // Page content
              Expanded(
                child: _buildPage(_selectedIndex),
              ),
            ],
          ),

          // Phone: Bottom navigation bar
          bottomNavigationBar: deviceType == DeviceType.phone
              ? NavigationBar(
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  destinations: _navItems
                      .map((item) => NavigationDestination(
                            icon: Icon(item.icon),
                            label: item.label,
                          ))
                      .toList(),
                )
              : null,
        );
      },
    );
  }

  void _onItemSelected(int index) {
    setState(() => _selectedIndex = index);
  }

  Widget _buildPage(int index) {
    return switch (index) {
      0 => const DashboardPage(),
      1 => const AnalyticsPage(),
      2 => const UsersPage(),
      3 => const SettingsPage(),
      _ => const DashboardPage(),
    };
  }
}
Tip: On very large desktops (1440px+), the navigation rail extends to show labels. On standard desktops, it stays compact with just icons. This progressive enhancement ensures the navigation never wastes space.

Step 4: Dashboard Page with Collapsing App Bar

The main dashboard page features a SliverAppBar with search functionality that collapses as the user scrolls, followed by responsive stat cards and an activity feed.

dashboard_page.dart

class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        return CustomScrollView(
          slivers: [
            // Collapsing app bar with search
            SliverAppBar(
              expandedHeight: deviceType == DeviceType.phone
                  ? 140
                  : 160,
              floating: true,
              pinned: true,
              snap: true,
              flexibleSpace: FlexibleSpaceBar(
                titlePadding: const EdgeInsets.only(
                  left: 16,
                  bottom: 16,
                  right: 16,
                ),
                title: deviceType == DeviceType.phone
                    ? null
                    : const Text(
                        'Dashboard Overview',
                        style: TextStyle(fontSize: 16),
                      ),
                background: SafeArea(
                  bottom: false,
                  child: Padding(
                    padding: const EdgeInsets.fromLTRB(16, 16, 16, 48),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        if (deviceType == DeviceType.phone)
                          const Text(
                            'Dashboard',
                            style: TextStyle(
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        const Spacer(),
                        // Search bar
                        TextField(
                          decoration: InputDecoration(
                            hintText: 'Search dashboard...',
                            prefixIcon: const Icon(Icons.search),
                            filled: true,
                            fillColor: Theme.of(context)
                                .colorScheme
                                .surfaceContainerHighest
                                .withValues(alpha: 0.5),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide.none,
                            ),
                            contentPadding:
                                const EdgeInsets.symmetric(vertical: 12),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),

            // Stats grid
            SliverPadding(
              padding: const EdgeInsets.all(16),
              sliver: SliverGrid(
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: Breakpoints.getGridColumns(width),
                  mainAxisSpacing: 12,
                  crossAxisSpacing: 12,
                  childAspectRatio: deviceType == DeviceType.phone
                      ? 2.5
                      : 2.0,
                ),
                delegate: SliverChildBuilderDelegate(
                  (context, index) =>
                      StatCard(item: kDashboardStats[index]),
                  childCount: kDashboardStats.length,
                ),
              ),
            ),

            // Section title
            const SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
                child: Text(
                  'Recent Activity',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),

            // Activity feed
            SliverPadding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverList.separated(
                itemCount: kRecentActivity.length,
                itemBuilder: (context, index) =>
                    ActivityCard(item: kRecentActivity[index]),
                separatorBuilder: (_, __) => const SizedBox(height: 8),
              ),
            ),

            // Bottom safe area spacing
            SliverToBoxAdapter(
              child: SizedBox(
                height: MediaQuery.paddingOf(context).bottom + 16,
              ),
            ),
          ],
        );
      },
    );
  }
}

Step 5: Stat Card and Activity Card Widgets

These reusable card widgets adapt their layout based on the available space.

stat_card.dart

class StatCard extends StatelessWidget {
  final DashboardItem item;

  const StatCard({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    final isPositive = item.change >= 0;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: item.color.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(item.icon, color: item.color, size: 24),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    item.title,
                    style: TextStyle(
                      color: Colors.grey[600],
                      fontSize: 13,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    item.value,
                    style: const TextStyle(
                      fontSize: 22,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 8,
                vertical: 4,
              ),
              decoration: BoxDecoration(
                color: (isPositive ? Colors.green : Colors.red)
                    .withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    isPositive
                        ? Icons.arrow_upward
                        : Icons.arrow_downward,
                    size: 14,
                    color: isPositive ? Colors.green : Colors.red,
                  ),
                  Text(
                    '\${item.change.abs()}%',
                    style: TextStyle(
                      color: isPositive ? Colors.green : Colors.red,
                      fontWeight: FontWeight.bold,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ActivityCard extends StatelessWidget {
  final ActivityItem item;

  const ActivityCard({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor:
              Theme.of(context).colorScheme.primaryContainer,
          child: Icon(
            item.icon,
            color: Theme.of(context).colorScheme.primary,
            size: 20,
          ),
        ),
        title: Text(
          item.title,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        subtitle: Text(item.description),
        trailing: Text(
          item.time,
          style: TextStyle(
            color: Colors.grey[500],
            fontSize: 12,
          ),
        ),
      ),
    );
  }
}

Step 6: Users Page with Master-Detail Pattern

The Users page demonstrates the master-detail pattern. On phones, selecting a user navigates to a detail page. On tablets and desktops, the detail panel appears alongside the list.

users_page.dart

class UsersPage extends StatefulWidget {
  const UsersPage({super.key});

  @override
  State<UsersPage> createState() => _UsersPageState();
}

class _UsersPageState extends State<UsersPage> {
  int? _selectedUserIndex;

  final List<Map<String, dynamic>> _users = List.generate(
    15,
    (i) => {
      'name': 'User \${i + 1}',
      'email': 'user\${i + 1}@example.com',
      'role': i % 3 == 0 ? 'Admin' : (i % 3 == 1 ? 'Editor' : 'Viewer'),
      'status': i % 4 != 0 ? 'Active' : 'Inactive',
      'joined': '\${12 - (i % 12)} months ago',
      'orders': (i + 1) * 7,
      'revenue': (i + 1) * 142.50,
    },
  );

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        final showDetail = deviceType != DeviceType.phone;

        return SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Header
              Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    const Expanded(
                      child: Text(
                        'Users',
                        style: TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    FilledButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.add, size: 18),
                      label: Text(
                        deviceType == DeviceType.phone
                            ? 'Add'
                            : 'Add User',
                      ),
                    ),
                  ],
                ),
              ),

              // Content
              Expanded(
                child: showDetail
                    ? Row(
                        children: [
                          SizedBox(
                            width: deviceType == DeviceType.desktop
                                ? 400
                                : 320,
                            child: _buildUserList(),
                          ),
                          const VerticalDivider(width: 1),
                          Expanded(
                            child: _selectedUserIndex != null
                                ? _buildUserDetail(
                                    _users[_selectedUserIndex!])
                                : const Center(
                                    child: Column(
                                      mainAxisSize: MainAxisSize.min,
                                      children: [
                                        Icon(
                                          Icons.person_search,
                                          size: 64,
                                          color: Colors.grey,
                                        ),
                                        SizedBox(height: 16),
                                        Text(
                                          'Select a user to view details',
                                          style: TextStyle(
                                            color: Colors.grey,
                                            fontSize: 16,
                                          ),
                                        ),
                                      ],
                                    ),
                                  ),
                          ),
                        ],
                      )
                    : _buildUserList(),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildUserList() {
    return ListView.builder(
      itemCount: _users.length,
      itemBuilder: (context, index) {
        final user = _users[index];
        final isSelected = _selectedUserIndex == index;

        return ListTile(
          selected: isSelected,
          selectedTileColor: Colors.indigo.withValues(alpha: 0.08),
          leading: CircleAvatar(
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              user['name'][0],
              style: const TextStyle(color: Colors.white),
            ),
          ),
          title: Text(
            user['name'],
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
          subtitle: Text(user['email']),
          trailing: Container(
            padding: const EdgeInsets.symmetric(
              horizontal: 8,
              vertical: 2,
            ),
            decoration: BoxDecoration(
              color: user['status'] == 'Active'
                  ? Colors.green.withValues(alpha: 0.1)
                  : Colors.grey.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              user['status'],
              style: TextStyle(
                fontSize: 12,
                color: user['status'] == 'Active'
                    ? Colors.green
                    : Colors.grey,
              ),
            ),
          ),
          onTap: () {
            setState(() => _selectedUserIndex = index);
            final width = MediaQuery.sizeOf(context).width;
            if (width < Breakpoints.tablet) {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => Scaffold(
                    appBar: AppBar(title: Text(user['name'])),
                    body: _buildUserDetail(user),
                  ),
                ),
              );
            }
          },
        );
      },
    );
  }

  Widget _buildUserDetail(Map<String, dynamic> user) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // User header
          Center(
            child: Column(
              children: [
                CircleAvatar(
                  radius: 40,
                  backgroundColor: Colors.indigo,
                  child: Text(
                    user['name'][0],
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 32,
                    ),
                  ),
                ),
                const SizedBox(height: 12),
                Text(
                  user['name'],
                  style: const TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  user['email'],
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          // Info cards
          _buildInfoRow('Role', user['role']),
          _buildInfoRow('Status', user['status']),
          _buildInfoRow('Joined', user['joined']),
          _buildInfoRow('Total Orders', '\${user['orders']}'),
          _buildInfoRow(
            'Total Revenue',
            '\$\${user['revenue'].toStringAsFixed(2)}',
          ),

          const SizedBox(height: 24),

          // Action buttons
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.edit),
                  label: const Text('Edit'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.email),
                  label: const Text('Contact'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: TextStyle(
              color: Colors.grey[600],
              fontSize: 14,
            ),
          ),
          Text(
            value,
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              fontSize: 14,
            ),
          ),
        ],
      ),
    );
  }
}

Step 7: Analytics and Settings Pages

Placeholder pages that also demonstrate responsive layout principles.

analytics_page.dart and settings_page.dart

class AnalyticsPage extends StatelessWidget {
  const AnalyticsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        final columns = Breakpoints.getGridColumns(width);
        return SafeArea(
          child: CustomScrollView(
            slivers: [
              const SliverToBoxAdapter(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Text(
                    'Analytics',
                    style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                sliver: SliverGrid(
                  gridDelegate:
                      SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: columns,
                    mainAxisSpacing: 12,
                    crossAxisSpacing: 12,
                  ),
                  delegate: SliverChildBuilderDelegate(
                    (context, index) {
                      return Card(
                        child: Padding(
                          padding: const EdgeInsets.all(16),
                          child: Column(
                            crossAxisAlignment:
                                CrossAxisAlignment.start,
                            children: [
                              Row(
                                children: [
                                  Icon(
                                    Icons.bar_chart,
                                    color: Colors.primaries[
                                        index %
                                            Colors.primaries.length],
                                  ),
                                  const SizedBox(width: 8),
                                  Text(
                                    'Metric \${index + 1}',
                                    style: const TextStyle(
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ],
                              ),
                              const Spacer(),
                              // Chart placeholder
                              Expanded(
                                flex: 2,
                                child: Container(
                                  decoration: BoxDecoration(
                                    color: Colors.grey.shade100,
                                    borderRadius:
                                        BorderRadius.circular(8),
                                  ),
                                  child: const Center(
                                    child: Icon(
                                      Icons.show_chart,
                                      size: 32,
                                      color: Colors.grey,
                                    ),
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                    childCount: 8,
                  ),
                ),
              ),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: MediaQuery.paddingOf(context).bottom + 16,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: ResponsiveConstrainedBox(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            const Text(
              'Settings',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildSection('Account', [
              _buildSettingsTile(
                Icons.person,
                'Profile',
                'Update your personal information',
              ),
              _buildSettingsTile(
                Icons.lock,
                'Security',
                'Password, 2FA, and sessions',
              ),
              _buildSettingsTile(
                Icons.notifications,
                'Notifications',
                'Configure alert preferences',
              ),
            ]),
            const SizedBox(height: 16),
            _buildSection('Preferences', [
              _buildSettingsTile(
                Icons.palette,
                'Appearance',
                'Theme, language, and display',
              ),
              _buildSettingsTile(
                Icons.storage,
                'Data & Storage',
                'Manage cached data',
              ),
            ]),
          ],
        ),
      ),
    );
  }

  Widget _buildSection(String title, List<Widget> children) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Text(
              title,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
          ),
          ...children,
        ],
      ),
    );
  }

  Widget _buildSettingsTile(
    IconData icon,
    String title,
    String subtitle,
  ) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      subtitle: Text(subtitle),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    );
  }
}

Step 8: Putting It All Together

Finally, the main entry point wires up the theme and the adaptive shell.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // Edge-to-edge mode for modern feel
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      systemNavigationBarColor: Colors.transparent,
      systemNavigationBarDividerColor: Colors.transparent,
    ),
  );

  runApp(const DashboardApp());
}

class DashboardApp extends StatelessWidget {
  const DashboardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Responsive Dashboard',
      theme: DashboardTheme.lightTheme,
      debugShowCheckedModeBanner: false,
      home: const AdaptiveShell(),
    );
  }
}
Important: In production, consider using a state management solution (Provider, Riverpod, Bloc) instead of setState for sharing the selected user index across the master-detail views. The examples here use setState for clarity, but larger apps benefit from centralized state management.

How the Concepts Connect

Let’s review how every layout concept from this tutorial appears in our dashboard app:

  • Breakpoint System (Lessons 1-2)Breakpoints class drives all responsive decisions: navigation type, grid columns, content width.
  • Flex, Row, Column (Lesson 3) — The navigation rail + content area use Row. User detail layout uses Column.
  • MediaQuery (Lesson 5)MediaQuery.sizeOf(context) provides screen dimensions for breakpoint calculations.
  • LayoutBuilder (Lesson 6)ResponsiveBuilder encapsulates the pattern of reading size and computing device type.
  • Slivers (Lesson 13)SliverAppBar with search, SliverGrid for stats, SliverList.separated for activity.
  • OrientationBuilder (Lesson 14) — Grid columns adapt to orientation within the breakpoint system.
  • SafeArea (Lesson 15) — Applied to every page for proper notch and home indicator handling.
  • SystemChrome (Lesson 15) — Edge-to-edge mode with transparent system bars in main().
Best Practices Recap:
1. Always start with a breakpoint system and use it consistently.
2. Use adaptive navigation that matches the platform convention.
3. Implement master-detail for data-heavy pages on larger screens.
4. Use SliverAppBar for rich scrolling headers.
5. Wrap page content in SafeArea when there is no AppBar.
6. Set edge-to-edge mode for a modern look.
7. Constrain content width on very large screens.
8. Test on phone, tablet, and desktop form factors.
Summary: This capstone demonstrated how to build a fully responsive Flutter application from scratch. We created a breakpoint utility, adaptive navigation shell, responsive dashboard grid, collapsing app bar with search, master-detail user management, and consistent safe area handling. Every concept from the Flutter Layouts & Responsive Design tutorial was applied in a real-world context. The key takeaway is that responsive Flutter apps are built from composable, reusable utilities — define your breakpoints once, create adaptive wrapper widgets, and let them drive layout decisions throughout the app.

Tutorial Complete!

Congratulations! You have completed all lessons in this tutorial.