device_preview 3.0.0-prerelease3 copy "device_preview: ^3.0.0-prerelease3" to clipboard
device_preview: ^3.0.0-prerelease3 copied to clipboard

A custom WidgetsBinding that simulates device characteristics (screen metrics, safe areas, locale, brightness, text scale, accessibility flags, target platform) at the engine-abstraction level. No in- [...]

example/lib/main.dart

// Example app for package:device_preview.
//
// The app itself is completely ordinary: a [MaterialApp] whose layout adapts
// to [MediaQuery]. The only device_preview-specific line is the call to
// [DevicePreview.enable] below — everything else is a
// regular Flutter app that happens to make the effects of a simulation easy
// to see:
//
//  * The "Characteristics" tab shows the live values the framework observes
//    (screen size, pixel ratio, safe areas, brightness, text scale, locale,
//    platform, accessibility flags). Apply a simulation and watch them change.
//  * The "Playground" tab is realistic scrollable content — cards, a list, a
//    text field (to exercise keyboard viewInsets) — with the safe-area
//    padding visualized as translucent bands.
//
// NOTE: the real control surface for device_preview is the DevTools
// extension ("Device Preview" tab in Dart DevTools). The buttons on the
// first tab exist only to demonstrate the programmatic Dart API
// (DevicePreview.controller); a production app would ship none of
// them.

import 'package:device_preview/device_preview.dart';
import 'package:device_preview/presets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  // The single integration point: install the device-preview binding before
  // runApp. In release builds this is behaviorally identical to
  // WidgetsFlutterBinding.ensureInitialized() (simulation defaults to
  // kDebugMode and the wrappers are never installed when disabled).
  DevicePreview.enable();
  runApp(const DevicePreviewExampleApp());
}

/// Root widget — a stock [MaterialApp] with light and dark themes so that
/// simulated [Brightness] changes are visible immediately.
class DevicePreviewExampleApp extends StatelessWidget {
  /// Creates the example app.
  const DevicePreviewExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Device Preview Example',
      // themeMode defaults to ThemeMode.system, which follows
      // MediaQuery.platformBrightnessOf — i.e. the simulated brightness.
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
      ),
      home: const HomeShell(),
    );
  }
}

/// Adaptive two-tab shell: bottom [NavigationBar] on narrow screens, a
/// [NavigationRail] on wide ones. Simulating a tablet or desktop preset
/// flips the layout live — a quick sanity check that the simulated
/// [MediaQuery] size drives responsive code exactly like a real device.
class HomeShell extends StatefulWidget {
  /// Creates the shell.
  const HomeShell({super.key});

  @override
  State<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
  int _selectedIndex = 0;

  static const List<NavigationDestination> _destinations =
      <NavigationDestination>[
        NavigationDestination(
          icon: Icon(Icons.phone_android_outlined),
          selectedIcon: Icon(Icons.phone_android),
          label: 'Characteristics',
        ),
        NavigationDestination(
          icon: Icon(Icons.widgets_outlined),
          selectedIcon: Icon(Icons.widgets),
          label: 'Playground',
        ),
      ];

  @override
  Widget build(BuildContext context) {
    // MediaQuery-adaptive layout: this is the simulated size when a preset
    // is active.
    final bool wide = MediaQuery.sizeOf(context).width >= 600;

    final Widget page = switch (_selectedIndex) {
      0 => const CharacteristicsPage(),
      _ => const PlaygroundPage(),
    };

    if (wide) {
      return Scaffold(
        body: SafeArea(
          child: Row(
            children: <Widget>[
              NavigationRail(
                selectedIndex: _selectedIndex,
                onDestinationSelected: (int i) =>
                    setState(() => _selectedIndex = i),
                labelType: NavigationRailLabelType.all,
                destinations: <NavigationRailDestination>[
                  for (final NavigationDestination d in _destinations)
                    NavigationRailDestination(
                      icon: d.icon,
                      selectedIcon: d.selectedIcon,
                      label: Text(d.label),
                    ),
                ],
              ),
              const VerticalDivider(thickness: 1, width: 1),
              Expanded(child: page),
            ],
          ),
        ),
      );
    }

    return Scaffold(
      body: page,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (int i) => setState(() => _selectedIndex = i),
        destinations: _destinations,
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Tab 1 — live device characteristics
// ---------------------------------------------------------------------------

/// Shows every device characteristic the framework can observe, read straight
/// from [MediaQuery] (and friends) so the values update live as simulations
/// are applied from DevTools or from the demo buttons below.
class CharacteristicsPage extends StatelessWidget {
  /// Creates the page.
  const CharacteristicsPage({super.key});

  String _edgeInsets(EdgeInsets insets) =>
      'L ${insets.left.toStringAsFixed(1)} · '
      'T ${insets.top.toStringAsFixed(1)} · '
      'R ${insets.right.toStringAsFixed(1)} · '
      'B ${insets.bottom.toStringAsFixed(1)}';

  @override
  Widget build(BuildContext context) {
    // Everything below is plain framework API — no device_preview imports
    // needed to *observe* a simulation. MediaQuery.of registers this widget
    // for rebuilds, which is what makes the readout live.
    final MediaQueryData media = MediaQuery.of(context);
    final Locale locale = Localizations.localeOf(context);
    final List<Locale> platformLocales =
        View.of(context).platformDispatcher.locales;

    // A 16px reference reading makes the effect of non-trivial TextScaler
    // implementations visible as a single number.
    final double effectiveScale = media.textScaler.scale(16) / 16;

    return CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar.large(title: Text('Device characteristics')),
        SliverList(
          delegate: SliverChildListDelegate(<Widget>[
            const _ApiDemoCard(),
            _Section(
              title: 'Screen',
              children: <Widget>[
                _ValueRow(
                  'MediaQuery.size',
                  '${media.size.width.toStringAsFixed(1)} × '
                      '${media.size.height.toStringAsFixed(1)} logical px',
                ),
                _ValueRow(
                  'devicePixelRatio',
                  media.devicePixelRatio.toStringAsFixed(2),
                ),
                _ValueRow('orientation', media.orientation.name),
                _ValueRow(
                  'displayFeatures',
                  media.displayFeatures.isEmpty
                      ? 'none'
                      : media.displayFeatures
                            .map((ui) => ui.type.name)
                            .join(', '),
                ),
              ],
            ),
            _Section(
              title: 'Insets (logical px)',
              children: <Widget>[
                _ValueRow('padding', _edgeInsets(media.padding)),
                _ValueRow('viewPadding', _edgeInsets(media.viewPadding)),
                _ValueRow(
                  'viewInsets (keyboard)',
                  _edgeInsets(media.viewInsets),
                ),
                _ValueRow(
                  'systemGestureInsets',
                  _edgeInsets(media.systemGestureInsets),
                ),
              ],
            ),
            _Section(
              title: 'Appearance & text',
              children: <Widget>[
                _ValueRow('platformBrightness', media.platformBrightness.name),
                _ValueRow(
                  'textScaler',
                  '×${effectiveScale.toStringAsFixed(2)}',
                ),
                _ValueRow(
                  'alwaysUse24HourFormat',
                  '${media.alwaysUse24HourFormat}',
                ),
              ],
            ),
            _Section(
              title: 'Locale & platform',
              children: <Widget>[
                _ValueRow('Localizations.localeOf', locale.toLanguageTag()),
                _ValueRow(
                  'PlatformDispatcher.locales',
                  platformLocales
                      .map((Locale l) => l.toLanguageTag())
                      .join(', '),
                ),
                _ValueRow('defaultTargetPlatform', defaultTargetPlatform.name),
              ],
            ),
            _Section(
              title: 'Accessibility',
              children: <Widget>[
                _FlagRow('accessibleNavigation', media.accessibleNavigation),
                _FlagRow('boldText', media.boldText),
                _FlagRow('disableAnimations', media.disableAnimations),
                _FlagRow('highContrast', media.highContrast),
                _FlagRow('invertColors', media.invertColors),
                _FlagRow('onOffSwitchLabels', media.onOffSwitchLabels),
              ],
            ),
            const SizedBox(height: 24),
          ]),
        ),
      ],
    );
  }
}

/// A handful of buttons exercising the programmatic API.
///
/// This exists purely to demonstrate [DevicePreviewController] from Dart —
/// the intended day-to-day control surface is the DevTools extension, which
/// drives the very same controller over the VM service.
class _ApiDemoCard extends StatelessWidget {
  const _ApiDemoCard();

  /// The controller, or null when simulation is disabled (e.g. release mode).
  DevicePreviewController? get _controller =>
      DevicePreview.maybeController;

  Future<void> _toggleOrientation(BuildContext context) async {
    final DevicePreviewController? c = _controller;
    if (c == null) {
      return;
    }
    final DeviceSimulation? simulation = c.simulation;
    if (simulation == null || !simulation.simulatesMetrics) {
      // Orientation is a property of the *simulated* screen; without a
      // simulated screen size there is nothing to rotate.
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Apply a device preset first, then rotate it.'),
        ),
      );
      return;
    }
    await c.setOrientation(
      simulation.orientation == Orientation.portrait
          ? Orientation.landscape
          : Orientation.portrait,
    );
  }

  Future<void> _toggleBrightness() async {
    final DevicePreviewController? c = _controller;
    if (c == null) {
      return;
    }
    final bool simulatedDark =
        c.simulation?.platformBrightness == Brightness.dark;
    // copyWith uses a sentinel default, so explicitly passing null CLEARS
    // the override and reverts to the real device brightness.
    await c.update(
      (DeviceSimulation s) => s.copyWith(
        platformBrightness: simulatedDark ? null : Brightness.dark,
      ),
    );
  }

  Future<void> _toggleTextScale() async {
    final DevicePreviewController? c = _controller;
    if (c == null) {
      return;
    }
    final bool scaled = c.simulation?.textScaleFactor == 2.0;
    await c.update(
      (DeviceSimulation s) => s.copyWith(textScaleFactor: scaled ? null : 2.0),
    );
  }

  @override
  Widget build(BuildContext context) {
    final DevicePreviewController? controller = _controller;
    return Card(
      margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Row(
              children: <Widget>[
                Icon(
                  Icons.info_outline,
                  size: 18,
                  color: Theme.of(context).colorScheme.primary,
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    controller == null
                        ? 'Simulation is disabled in this build mode.'
                        : 'The real control surface is the Device Preview '
                              'tab in DevTools. These buttons just demo the '
                              'Dart API.',
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                ),
              ],
            ),
            if (controller != null) ...<Widget>[
              const SizedBox(height: 12),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: <Widget>[
                  FilledButton.tonal(
                    onPressed: () =>
                        controller.applyPreset(DevicePresets.iPhone16),
                    child: const Text('iPhone SE preset'),
                  ),
                  FilledButton.tonal(
                    onPressed: () => _toggleOrientation(context),
                    child: const Text('Toggle orientation'),
                  ),
                  FilledButton.tonal(
                    onPressed: _toggleBrightness,
                    child: const Text('Toggle dark brightness'),
                  ),
                  FilledButton.tonal(
                    onPressed: _toggleTextScale,
                    child: const Text('Toggle text scale 2.0'),
                  ),
                  OutlinedButton(
                    onPressed: controller.reset,
                    child: const Text('Reset'),
                  ),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}

/// A titled group of value rows.
class _Section extends StatelessWidget {
  const _Section({required this.title, required this.children});

  final String title;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            title,
            style: Theme.of(context).textTheme.titleSmall?.copyWith(
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const SizedBox(height: 4),
          ...children,
        ],
      ),
    );
  }
}

/// A single "label: value" line, tabular-styled.
class _ValueRow extends StatelessWidget {
  const _ValueRow(this.label, this.value);

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    final TextTheme text = Theme.of(context).textTheme;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 3),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Expanded(child: Text(label, style: text.bodyMedium)),
          Expanded(
            child: Text(
              value,
              textAlign: TextAlign.end,
              style: text.bodyMedium?.copyWith(fontFamily: 'monospace'),
            ),
          ),
        ],
      ),
    );
  }
}

/// A boolean accessibility flag rendered as a small on/off chip.
class _FlagRow extends StatelessWidget {
  const _FlagRow(this.label, this.value);

  final String label;
  final bool value;

  @override
  Widget build(BuildContext context) {
    final ColorScheme colors = Theme.of(context).colorScheme;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 3),
      child: Row(
        children: <Widget>[
          Expanded(child: Text(label)),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
            decoration: BoxDecoration(
              color: value ? colors.primaryContainer : colors.surfaceContainer,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              value ? 'on' : 'off',
              style: Theme.of(context).textTheme.labelSmall?.copyWith(
                color: value
                    ? colors.onPrimaryContainer
                    : colors.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Tab 2 — realistic content playground
// ---------------------------------------------------------------------------

/// Realistic scrollable content to eyeball a simulation with: cards, a list,
/// a text field (tap it to see keyboard `viewInsets` flow into the layout),
/// and translucent bands visualizing the safe-area `padding`.
class PlaygroundPage extends StatelessWidget {
  /// Creates the page.
  const PlaygroundPage({super.key});

  @override
  Widget build(BuildContext context) {
    final EdgeInsets padding = MediaQuery.paddingOf(context);
    final ColorScheme colors = Theme.of(context).colorScheme;

    return Stack(
      children: <Widget>[
        CustomScrollView(
          slivers: <Widget>[
            const SliverAppBar.medium(title: Text('Playground')),
            SliverPadding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverList(
                delegate: SliverChildListDelegate(<Widget>[
                  // A text field: focusing it opens the real keyboard, whose
                  // inset is mapped into simulated space by device_preview so
                  // scroll-into-view keeps working while simulating.
                  TextField(
                    decoration: InputDecoration(
                      border: const OutlineInputBorder(),
                      labelText: 'Try the keyboard',
                      helperText:
                          'viewInsets update live on the first tab while '
                          'this is focused',
                      prefixIcon: const Icon(Icons.keyboard_alt_outlined),
                      suffixIcon: IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () =>
                            FocusManager.instance.primaryFocus?.unfocus(),
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  // A few cards with mixed content, like a real app screen.
                  for (int i = 0; i < 3; i++)
                    Card(
                      margin: const EdgeInsets.only(bottom: 12),
                      child: Padding(
                        padding: const EdgeInsets.all(16),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              'Card ${i + 1}',
                              style: Theme.of(context).textTheme.titleMedium,
                            ),
                            const SizedBox(height: 8),
                            const Text(
                              'Body text reflows with the simulated screen '
                              'width and scales with the simulated text '
                              'scale factor. Toggle presets in DevTools and '
                              'watch this paragraph adapt.',
                            ),
                          ],
                        ),
                      ),
                    ),
                  const SizedBox(height: 4),
                ]),
              ),
            ),
            // A longer list to make scrolling (and fling physics under the
            // remapped pointer coordinates) easy to test.
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) => ListTile(
                  leading: CircleAvatar(child: Text('${index + 1}')),
                  title: Text('List item ${index + 1}'),
                  subtitle: const Text('Scroll and fling to test input'),
                ),
                childCount: 30,
              ),
            ),
            // Keep the last items reachable above the bottom safe area.
            SliverToBoxAdapter(child: SizedBox(height: padding.bottom + 16)),
          ],
        ),
        // Safe-area visualization: translucent bands over the padding
        // regions. With an iPhone preset active you should see the notch
        // band at the top and the home-indicator band at the bottom.
        IgnorePointer(
          child: Stack(
            children: <Widget>[
              _SafeAreaBand.top(padding.top, colors),
              _SafeAreaBand.bottom(padding.bottom, colors),
              _SafeAreaBand.left(padding.left, colors),
              _SafeAreaBand.right(padding.right, colors),
            ],
          ),
        ),
      ],
    );
  }
}

/// One translucent edge band visualizing a safe-area inset.
class _SafeAreaBand extends StatelessWidget {
  const _SafeAreaBand.top(this.extent, this.colors)
    : left = 0,
      top = 0,
      right = 0,
      bottom = null,
      horizontal = true;

  const _SafeAreaBand.bottom(this.extent, this.colors)
    : left = 0,
      top = null,
      right = 0,
      bottom = 0,
      horizontal = true;

  const _SafeAreaBand.left(this.extent, this.colors)
    : left = 0,
      top = 0,
      right = null,
      bottom = 0,
      horizontal = false;

  const _SafeAreaBand.right(this.extent, this.colors)
    : left = null,
      top = 0,
      right = 0,
      bottom = 0,
      horizontal = false;

  final double extent;
  final ColorScheme colors;
  final double? left;
  final double? top;
  final double? right;
  final double? bottom;
  final bool horizontal;

  @override
  Widget build(BuildContext context) {
    if (extent <= 0) {
      return const SizedBox.shrink();
    }
    return Positioned(
      left: left,
      top: top,
      right: right,
      bottom: bottom,
      child: Container(
        height: horizontal ? extent : null,
        width: horizontal ? null : extent,
        color: colors.tertiary.withValues(alpha: 0.15),
      ),
    );
  }
}
4.25k
likes
150
points
182k
downloads

Documentation

API reference

Publisher

verified publisheraloisdeniel.com

Weekly Downloads

A custom WidgetsBinding that simulates device characteristics (screen metrics, safe areas, locale, brightness, text scale, accessibility flags, target platform) at the engine-abstraction level. No in-app UI — driven programmatically and by a DevTools extension.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on device_preview