Better Flutter golden testing - how mobile logical sizing is different than the default golden size

We wanted to verify our layout behavior across various screen widths, especially across layout change boundaries. Ex: Change field sizes, expose menus, or make other items at higher widths. 

Mobile devices have high-resolution screens that are logically down-scaled for sizing display widgets essentially using a logical resolution. I ended up writing this simple MediaQuery program that tells me the effective size of the screen of a device. This lets us build a library of test device resolutions and aspect ratios. We can run the same test across as many devices as we wish because computing is cheaper than manual testers.

Flutter golden image tests are a simple way to watch for layout regression.  They compare a stored image against a new image captured during test execution.  The base behavior is that they open an 800x600 canvas and use that.   We can change the size of the test canvas where the golden test images are rendered.

What if we want to work in the opposite direction starting with various phones and their geometries? I thought we'd just set the resolution to the phone screen size but it turned out to be very wrong wrong. You have to set the test canvas to the Flutter logical screen resolution.  

Related stuff by Joe

BlogBetter Flutter golden testing - how mobile logical sizing is different than the default golden size
BlogFlutter golden testing - generating tester views sized to your mobile devices
VideoFlutter Golden testing with views sized to your mobile devices
VideoFlutter golden testing - taking into account the Flutter logical resolution on mobile devices
Demonstration CodeFlutter media query example with tests

Example Physical and Media Query

In general, the web is just 1:1 physical to logical.  We can set the test screen size to the target canvas size.  Mobile device test resolution should be sized to the logical MediaQuery width and height.  We don't care how big the test canvas is physically.  We care about the test canvas size as drawn by Flutter.

DevicePixels (w)Pixels (h)DiagonalPPIMQ widthMQ heightDevice Pixel RatioEff PPI
web500430n/a96 @100%500430196
Pixel 3a108022205.6"441392.7783.32.75160?

 Here we can see that the 1080x2220 pixel display is treated as a 392/783 sized canvas.  This means we should size the golden test to 392x783 if we want to detect out-of-bounds drawing or layout issues.

Google Pixel 3a 

Here we see the sample program output for the Pixel 3a.  The native resolution is 1080x2220.  The Device Pixel Ratio is 2.75.  This means we just divide the native resolution by the DPR to get something close to the 392w x 783h reported by the Media Query.  We size the corresponding golden tests to 392x783.

Chrome / Windows

The windows situation is a little simpler. We can treat the physical resolution as the logical because the Device Pixel Ratio is 1:1 so they are both the same.  Want a 480w x 600h screen.  Just size the test that way.

Demonstration Program

This is the program used to generate the sample screens above. Just run it on your device or emulator to get a feel for the scaling involved. Run the program on your device

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());

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

  // This widget is the root of your application.
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor:,
        useMaterial3: true,
      home: const MyHomePage(title: 'Media Query Home Page'),

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  Widget build(BuildContext context) {
    MediaQueryData currentMedia = MediaQuery.of(context);

    List<String> mediaState = [
      'accessibleNavigation: ${currentMedia.accessibleNavigation}',
      'alwaysUse24hour: ${currentMedia.alwaysUse24HourFormat}',
      'devicePixelRatio: ${currentMedia.devicePixelRatio}',
      'displayFeatures: ${currentMedia.displayFeatures}',
      'gestureSettings: ${currentMedia.gestureSettings}',
      'navigationMode: ${currentMedia.navigationMode}',
      'orientation: ${currentMedia.orientation}',
      'padding: ${currentMedia.padding}',
      'runtimeType: ${currentMedia.runtimeType}',
      'size: ${currentMedia.size}',
      'systemGestureInserts: ${currentMedia.systemGestureInsets}',
      'textScaler: ${currentMedia.textScaler}',
      'viewInserts: ${currentMedia.viewInsets}',
      'viewPadding: ${currentMedia.viewPadding}',

    // This method is rerun every time setState is called,
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(title),
        body: Column(children: [
          const Text(
            "MediaQuery returned",
            style: TextStyle(fontWeight: FontWeight.bold),
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: mediaState.length,
              itemBuilder: (BuildContext contest, int index) {
                return ListTile(
                    title: Text(mediaState[index]),
                    shape: const RoundedRectangleBorder(
                        side: BorderSide(
                            style: BorderStyle.solid,
                            color: Color(0x40000000))),
                    tileColor: Colors.white70,
                    dense: true,
                    visualDensity: VisualDensity.compact);

The program could be simplified.  It has some styling changes that could be removed.


Change Log

Created 2023 08 27


Popular posts from this blog

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

DNS for Azure Point to Site (P2S) VPN - getting the internal IPs