yx_virtual_device 1.0.1 copy "yx_virtual_device: ^1.0.1" to clipboard
yx_virtual_device: ^1.0.1 copied to clipboard

A Flutter package that lets you simulate various device screens directly inside your running app. Easily test your responsive UI on the fly without the need for physical devices or constantly switchin [...]

example/lib/main.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:yx_virtual_device/yx_virtual_device.dart';
import 'package:yx_virtual_device/virtual_device/devtools/bindings.dart';

void main() {
  // Called before [run App] to replace bindings.
  // It is good not to use it in the product assembly.
  if (kProfileMode || kDebugMode) {
    VirtualDeviceDevtools.setup();
  }
  runApp(const App());
}

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

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  DeviceInfo? _currentDevice;

  List<DeviceInfo?> get _allDevices => [
        null,
        ...Devices.ios.all,
        ...Devices.android.all,
        ...Devices.standard.all,
      ];

  void _setDevice(DeviceInfo? device) {
    setState(() {
      _currentDevice = device;
      VirtualDeviceDevtools.setDevice(device);
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.blue,
        brightness: Brightness.dark,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text(_currentDevice?.name ?? 'Real Device'),
          centerTitle: true,
          actions: [
            if (_currentDevice != null)
              IconButton(
                icon: const Icon(Icons.close),
                onPressed: () => _setDevice(null),
                tooltip: 'Reset to real device',
              ),
          ],
        ),
        drawer: _DeviceDrawer(
          devices: _allDevices,
          currentDevice: _currentDevice,
          onDeviceSelected: _setDevice,
        ),
        body: SafeArea(
          child: _DeviceInfoScreen(device: _currentDevice),
        ),
        floatingActionButton: _QuickSwitchFAB(
          devices: _allDevices,
          currentDevice: _currentDevice,
          onDeviceSelected: _setDevice,
        ),
      ),
    );
  }
}

class _DeviceDrawer extends StatelessWidget {
  const _DeviceDrawer({
    required this.devices,
    required this.currentDevice,
    required this.onDeviceSelected,
  });

  final List<DeviceInfo?> devices;
  final DeviceInfo? currentDevice;
  final ValueChanged<DeviceInfo?> onDeviceSelected;

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: CustomScrollView(
        slivers: [
          SliverToBoxAdapter(
            child: DrawerHeader(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [
                    Theme.of(context).colorScheme.primary,
                    Theme.of(context).colorScheme.secondary,
                  ],
                ),
              ),
              child: const Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  Icon(Icons.devices, size: 48, color: Colors.white),
                  SizedBox(height: 8),
                  Text(
                    'Virtual Devices',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Text(
                    'Select a device to emulate',
                    style: TextStyle(color: Colors.white70),
                  ),
                ],
              ),
            ),
          ),

          // Real Device
          SliverToBoxAdapter(
            child: _DeviceListTile(
              device: null,
              isSelected: currentDevice == null,
              onTap: () {
                onDeviceSelected(null);
                Navigator.pop(context);
              },
            ),
          ),

          const SliverToBoxAdapter(child: Divider()),

          // iOS Devices
          _DeviceSection(
            title: 'iOS',
            devices: Devices.ios.all,
            currentDevice: currentDevice,
            onDeviceSelected: (device) {
              onDeviceSelected(device);
              Navigator.pop(context);
            },
          ),

          // Android Devices
          _DeviceSection(
            title: 'Android',
            devices: Devices.android.all,
            currentDevice: currentDevice,
            onDeviceSelected: (device) {
              onDeviceSelected(device);
              Navigator.pop(context);
            },
          ),

          // Standard Devices
          _DeviceSection(
            title: 'Standard',
            devices: Devices.standard.all,
            currentDevice: currentDevice,
            onDeviceSelected: (device) {
              onDeviceSelected(device);
              Navigator.pop(context);
            },
          ),
        ],
      ),
    );
  }
}

class _DeviceSection extends StatelessWidget {
  const _DeviceSection({
    required this.title,
    required this.devices,
    required this.currentDevice,
    required this.onDeviceSelected,
  });

  final String title;
  final List<DeviceInfo> devices;
  final DeviceInfo? currentDevice;
  final ValueChanged<DeviceInfo> onDeviceSelected;

  @override
  Widget build(BuildContext context) {
    return SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          if (index == 0) {
            return Padding(
              padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
              child: Text(
                title,
                style: Theme.of(context).textTheme.titleSmall?.copyWith(
                      color: Theme.of(context).colorScheme.primary,
                      fontWeight: FontWeight.bold,
                    ),
              ),
            );
          }

          final device = devices[index - 1];
          return _DeviceListTile(
            device: device,
            isSelected: currentDevice?.name == device.name,
            onTap: () => onDeviceSelected(device),
          );
        },
        childCount: devices.length + 1,
      ),
    );
  }
}

class _DeviceListTile extends StatelessWidget {
  const _DeviceListTile({
    required this.device,
    required this.isSelected,
    required this.onTap,
  });

  final DeviceInfo? device;
  final bool isSelected;
  final VoidCallback onTap;

  IconData get _icon {
    if (device == null) return Icons.phone_android;

    final name = device!.name.toLowerCase();
    if (name.contains('iphone')) return Icons.phone_iphone;
    if (name.contains('ipad') || name.contains('tablet')) {
      return Icons.tablet_mac;
    }
    if (name.contains('desktop')) return Icons.desktop_windows;
    return Icons.phone_android;
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return ListTile(
      leading: Icon(
        _icon,
        color: isSelected ? colorScheme.primary : null,
      ),
      title: Text(device?.name ?? 'Real Device'),
      subtitle: device != null
          ? Text(
              '${device!.screenSize.width.toInt()}×${device!.screenSize.height.toInt()} @${device!.pixelRatio}x',
              style: TextStyle(
                color: isSelected ? colorScheme.primary : null,
              ),
            )
          : const Text('Use actual device dimensions'),
      selected: isSelected,
      selectedTileColor: colorScheme.primaryContainer.withValues(alpha: 0.3),
      onTap: onTap,
      trailing: isSelected
          ? Icon(Icons.check_circle, color: colorScheme.primary)
          : null,
    );
  }
}

class _DeviceInfoScreen extends StatelessWidget {
  const _DeviceInfoScreen({required this.device});

  final DeviceInfo? device;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final mediaQuery = MediaQuery.of(context);
        final view = View.of(context);
        final binding = VirtualWidgetsFlutterBinding.virtualBinding;

        return SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Device Card
              _InfoCard(
                title: 'Current Device',
                icon: Icons.devices,
                color: Colors.blue,
                child: Column(
                  children: [
                    _InfoRow('Name', device?.name ?? 'Real Device'),
                    if (device != null) ...[
                      _InfoRow(
                        'Screen Size',
                        '${device!.screenSize.width.toInt()} × ${device!.screenSize.height.toInt()}',
                      ),
                      _InfoRow('Pixel Ratio', '${device!.pixelRatio}x'),
                      _InfoRow('Platform', device!.identifier.platform.name),
                      _InfoRow('Type', device!.identifier.type.name),
                      _InfoRow(
                        'Safe Areas',
                        _formatEdgeInsets(device!.safeAreas),
                      ),
                    ],
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // Layout Constraints
              _InfoCard(
                title: 'Layout Constraints',
                icon: Icons.aspect_ratio,
                color: Colors.green,
                child: Column(
                  children: [
                    _InfoRow(
                      'Max Width',
                      constraints.maxWidth.toStringAsFixed(1),
                    ),
                    _InfoRow(
                      'Max Height',
                      constraints.maxHeight.toStringAsFixed(1),
                    ),
                    _InfoRow(
                      'Aspect Ratio',
                      (constraints.maxWidth / constraints.maxHeight)
                          .toStringAsFixed(2),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // MediaQuery
              _InfoCard(
                title: 'MediaQuery',
                icon: Icons.screen_search_desktop_outlined,
                color: Colors.orange,
                child: Column(
                  children: [
                    _InfoRow(
                      'Size',
                      '${mediaQuery.size.width.toStringAsFixed(1)} × ${mediaQuery.size.height.toStringAsFixed(1)}',
                    ),
                    _InfoRow('Pixel Ratio', '${mediaQuery.devicePixelRatio}x'),
                    _InfoRow('Padding', _formatEdgeInsets(mediaQuery.padding)),
                    _InfoRow(
                      'View Insets',
                      _formatEdgeInsets(mediaQuery.viewInsets),
                    ),
                    _InfoRow(
                      'Text Scale',
                      mediaQuery.textScaler.scale(1).toStringAsFixed(2),
                    ),
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // View Info
              _InfoCard(
                title: 'View.of(context)',
                icon: Icons.visibility,
                color: Colors.purple,
                child: Column(
                  children: [
                    _InfoRow(
                      'Physical Size',
                      '${view.physicalSize.width.toStringAsFixed(0)} × ${view.physicalSize.height.toStringAsFixed(0)}',
                    ),
                    _InfoRow(
                      'Logical Size',
                      '${(view.physicalSize.width / view.devicePixelRatio).toStringAsFixed(1)} × ${(view.physicalSize.height / view.devicePixelRatio).toStringAsFixed(1)}',
                    ),
                    _InfoRow('Device Pixel Ratio', '${view.devicePixelRatio}x'),
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // Binding Info
              _InfoCard(
                title: 'Virtual Binding',
                icon: Icons.settings_applications,
                color: Colors.teal,
                child: Column(
                  children: [
                    _InfoRow(
                      'Virtual Device Active',
                      binding.device != null ? 'Yes' : 'No',
                    ),
                    _InfoRow('Binding Device', binding.device?.name ?? 'None'),
                  ],
                ),
              ),

              const SizedBox(height: 16),

              // Demo Grid
              _InfoCard(
                title: 'Responsive Demo',
                icon: Icons.grid_view,
                color: Colors.pink,
                child: _ResponsiveGrid(),
              ),

              const SizedBox(height: 80), // Space for FAB
            ],
          ),
        );
      },
    );
  }

  String _formatEdgeInsets(EdgeInsets insets) {
    if (insets == EdgeInsets.zero) return 'none';
    return 'T:${insets.top.toInt()} B:${insets.bottom.toInt()} L:${insets.left.toInt()} R:${insets.right.toInt()}';
  }
}

class _InfoCard extends StatelessWidget {
  const _InfoCard({
    required this.title,
    required this.icon,
    required this.color,
    required this.child,
  });

  final String title;
  final IconData icon;
  final Color color;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(icon, color: color),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: color,
                  ),
                ),
              ],
            ),
            const Divider(),
            child,
          ],
        ),
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow(this.label, this.value);

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: const TextStyle(
              color: Colors.grey,
            ),
          ),
          Flexible(
            child: Text(
              value,
              style: const TextStyle(
                fontWeight: FontWeight.w500,
              ),
              textAlign: TextAlign.end,
            ),
          ),
        ],
      ),
    );
  }
}

class _ResponsiveGrid extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final width = constraints.maxWidth;
        final crossAxisCount = width > 500 ? 4 : (width > 300 ? 3 : 2);

        return Column(
          children: [
            Text(
              'Grid adapts to $crossAxisCount columns',
              style: const TextStyle(color: Colors.grey),
            ),
            const SizedBox(height: 12),
            GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: crossAxisCount,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              itemCount: 8,
              itemBuilder: (context, index) {
                return Container(
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [
                        Colors.primaries[index % Colors.primaries.length],
                        Colors.primaries[(index + 5) % Colors.primaries.length],
                      ],
                    ),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Center(
                    child: Text(
                      '${index + 1}',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                );
              },
            ),
            const SizedBox(height: 16),
            const Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
                SizedBox(width: 12),
                Text(
                  'App is running...',
                  style: TextStyle(
                    color: Colors.grey,
                  ),
                ),
              ],
            ),
          ],
        );
      },
    );
  }
}

class _QuickSwitchFAB extends StatelessWidget {
  const _QuickSwitchFAB({
    required this.devices,
    required this.currentDevice,
    required this.onDeviceSelected,
  });

  final List<DeviceInfo?> devices;
  final DeviceInfo? currentDevice;
  final ValueChanged<DeviceInfo?> onDeviceSelected;

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton.extended(
      onPressed: () => _showQuickPicker(context),
      icon: const Icon(Icons.swap_horiz),
      label: const Text('Switch'),
    );
  }

  void _showQuickPicker(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (context) {
        return SafeArea(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Padding(
                padding: EdgeInsets.all(16),
                child: Text(
                  'Quick Device Switch',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              const Divider(height: 1),
              Flexible(
                child: ListView.builder(
                  shrinkWrap: true,
                  itemCount: devices.length.clamp(0, 10),
                  itemBuilder: (context, index) {
                    final device = devices[index];
                    final isSelected = device?.name == currentDevice?.name;

                    return ListTile(
                      leading: Icon(
                        device == null ? Icons.phone_android : Icons.devices,
                        color: isSelected
                            ? Theme.of(context).colorScheme.primary
                            : null,
                      ),
                      title: Text(device?.name ?? 'Real Device'),
                      trailing: isSelected
                          ? Icon(
                              Icons.check,
                              color: Theme.of(context).colorScheme.primary,
                            )
                          : null,
                      onTap: () {
                        onDeviceSelected(device);
                        Navigator.pop(context);
                      },
                    );
                  },
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}
8
likes
140
points
104
downloads
screenshot

Documentation

API reference

Publisher

verified publisherdev.go.yandex

Weekly Downloads

A Flutter package that lets you simulate various device screens directly inside your running app. Easily test your responsive UI on the fly without the need for physical devices or constantly switching emulators.

Repository (GitHub)
View/report issues
Contributing

Topics

#simplify-testing #testing-utility #virtual-screens #different-devices

License

MIT (license)

Dependencies

flutter

More

Packages that depend on yx_virtual_device