Record of metronome development

Testing Flutter’s multilingual UI revealed “300 BPM” split across two lines

Display tests for SANGE Metronome combined 12 languages, 6 screen sizes and 3 text scales. This article records the UI fixes and more specific automated assertions prompted by a line break in “300 BPM” and long error messages.

public update
  • Flutter
  • personal development
  • Multilingual support
  • UI testing

I checked how my independently developed app, “SANGE Metronome – Simple Rhythm Practice”, displays on small screens and with enlarged text.

One issue was that the digits in “300 BPM” on a favorite card split into “30” and “0”. The text was not overflowing off the screen, yet the tempo value had become difficult to read.

We needed to verify both the absence of layout exceptions and the readability of essential information.

This article describes the display issues found through widget tests in 12 languages and screen reviews in an Android emulator, and how those findings informed the fixes and tests.

An overview of the lower home screens in French, Brazilian Portuguese, Indonesian, Thai, Vietnamese and Japanese at 130% text scale
An overview of the corrected screens in six languages. Top row, left to right: French, Brazilian Portuguese and Indonesian. Bottom row: Thai, Vietnamese and Japanese. The German favorite names are shared test data used in every language to test long names.

A layout that fits Japanese may not fit other languages

The metronome screen shows not only tempo digits, but also beats, rhythm names, volume and favorite names. The Save screen also needs an input field, error explanations, and Save and Cancel buttons.

Even when a layout fits in Japanese, translating changes label lengths. For example, Japanese “12拍” and German “12 Schläge” require different button widths.

Larger text also means that elements arranged side by side no longer fit. Instead of changing only the language, I decided to combine screen sizes, text scales and the screen being displayed in the checks.

Automated checks across 12 languages, 6 sizes and 3 text scales

The widget tests combined the following conditions. Widget testing renders Flutter UI components in a test environment so that their behavior and layout can be checked.

Test dimensionconditions
LanguageJapanese, English, Spanish, Brazilian Portuguese, German, French, Korean, Indonesian, Vietnamese, Thai, Traditional Chinese and Italian
Logical size, portrait320×568、360×640、360×800、393×873、412×915、432×960
Text scale100%, 130% and 200%
ScreensHome, beats, rhythm subdivision, settings, language selection, save, favorite management and favorites list
12 languages × 6 sizes × 3 text scales × 8 screens = 1,728 conditions

The test data used 300 BPM, 12 beats, sixteenth notes, 100% volume and three favorites with long names. The Save screen also displayed the duplicate-name error.

Text scaling was specified with TextScaler.linear. The 100%, 130% and 200% settings in these tests scale text linearly; they do not reproduce every aspect of text enlargement through a physical device’s OS settings.

Images were also collected in an Android emulator for seven languages: German, French, Brazilian Portuguese, Indonesian, Thai, Vietnamese and Japanese. This was not an exhaustive combination of every size, scale and language. Screen sizes and text scales were checked in German, while the other six languages were checked at a 360×640 setting and 130%.

System bars reduce the area available to the app in the emulator. Along with the configured size, I recorded the logical size at rendering time and the size within SafeArea.

Adjust beat-button columns to fit the text

The first issues found were tempo descriptions and beat units overflowing on the right. The Android emulator also reported RenderFlex overflow, which required a layout revision.

Beat selection also used a fixed four-column layout. However, longer translated labels or larger text scales leave insufficient width for that many columns.

I therefore measured translated labels with TextPainter and determined the number of columns from the required width, including text scaling. In narrower conditions, fewer columns let the buttons extend vertically.

The corrected beat selection screen at a 320×568 display setting, in German at 100%, with buttons for beats 1–12 arranged in two columns
The corrected display at 100%. Under these conditions, the 12 buttons fit in two columns.
The beat selection screen scrolled down at a 320×568 display setting, in German at 200%. Buttons for beats 7–12 are arranged in one column
At 200% with the same display size, the buttons use one column. I scrolled to check the 12-beat button and the explanation below it. The text scale and scroll position differ from the image above; both images show the corrected layout.

The home screen’s narrow two-column layout for long rhythm names was also revised so that beats and rhythm stack vertically when necessary. To address split digits for beats 10–12, the overall beat display was allowed to wrap onto multiple rows.

Splitting “300” does not always raise an exception

While reviewing the images, I noticed an issue with favorite cards at a 320×568 display setting and 200% text scale.

The cards still used three columns, which caused a line break within “300”. Even if the text remains inside its display area, splitting the digits into “30” and “0” makes the tempo difficult to read.

Checks for layout exceptions alone did not detect this issue.

The fix adjusts the number of favorite-card columns to the text scale. At 200% on the small screen tested here, the cards use one column so that tempo digits stay together.

The corrected lower home screen at a 320×568 display setting, in German at 200%, with favorites in one column and the digits in 300 BPM kept together
Favorites after the fix. Long names are truncated at the end, while tempo digits stay together. No image from before the fix was saved for this condition, so this is not a side-by-side before-and-after comparison under identical conditions.

Alongside the fix, I added an automated assertion that a three-digit BPM value fits on one line. The following is an excerpt of that check.

for (var card = 0; card < 3; card++) {
  final paragraph = tester.renderObject<RenderParagraph>(
    find.text('300  BPM').at(card),
  );
  expect(
    paragraph.getBoxesForSelection(
      const TextSelection(baseOffset: 0, extentOffset: 3),
    ),
    hasLength(1),
    reason:
        '$locale/$size/$scale favorite BPM must stay on one line',
  );
}

The check confirms that the character range corresponding to “300” has a single rendering box. The actual tests checked the BPM in each of three favorite cards, as well as beat numbers 1–12 on the home screen.

This method targets the digits rendered in the same typeface in these tests. It is not a general-purpose way to determine line breaks in text mixing different languages or typefaces.

Make assertions specific to the information the UI needs to convey. A useful outcome was turning an issue found in screenshots into an automated check that can detect it after future changes.

Can the full error message and Save button be reached?

The Save screen’s error-message line limit was also revised.

The error message was originally limited to three lines, so a long translation might not fit when text was enlarged on a narrow screen. I removed the line limit and made the full explanation accessible by scrolling.

Save and Cancel buttons now stack vertically on narrow screens or with larger text. The modal heading and close button, as well as setting names and selected values, were also rearranged to give text enough width.

The Save screen scrolled down at a 320×568 display setting, in German at 200%, showing the complete duplicate-name error and vertically arranged Cancel and Save buttons
The duplicate-name error explanation and the Cancel and Save buttons, shown after scrolling.

Checks while entering text were also added. The widget tests reserved a bottom inset of 280 logical pixels to simulate a keyboard and verified that scrolling brought the Save button above that area. They also checked that error messages were not truncated by a line limit.

In the Android emulator, I actually focused the input field and checked the layout with the keyboard open. However, these PNGs capture only the Flutter surface and do not include the OS keyboard itself. Normal and text-entry states were recorded separately.

Results after the fixes and the scope of verification

These are the results of the checks performed on October 30, 2026.

ItemResult
Display widget testsNo layout exceptions across 1,728 conditions. Checks for split numbers, truncated error messages and access to the Save button also passed on the relevant screens
All Flutter tests121 tests passed
Static analysisNo issues reported
Android emulator screen conditions280 conditions. In every run, layoutErrors was 0
PNGs collected478 images, including scrolled and text-entry states

1,728 is the number of display-condition combinations; 121 is the total number of Flutter tests run. They are separate figures because each test repeatedly checks multiple display conditions.

The figure 478 is the number of PNGs collected. Rather than assigning an individual visual pass or fail to every image, I selected key screens to check for missing or overlapping text, split numbers and the view after scrolling. The inspected Thai and Vietnamese screens also showed no missing-glyph boxes or overlapping text.

In addition to six synthetic sizes, the emulator used six device display profiles: Galaxy A16 5G, A06 4G, S24+, S23, Redmi 14C 4G and Pixel 9. I prepared 30 device profiles, but ran six of them in this verification.

These settings applied resolutions and approximate test densities to the same Android 14 emulator. The checks did not reproduce manufacturers’ custom operating systems, fonts or camera cutouts, and did not use 30 physical device models. Flutter tests ran on Windows, and Linux pixel-exact golden comparisons were not run this time.

This record concerns display verification. Rendering screens, focusing input fields and checking scrolling do not establish that audio playback or saving data succeeded.

What to check with the next UI change

These checks clarified the assertions worth retaining in the display tests.

  • Keep numbers such as tempo values and beat numbers together.
  • Make the full error explanation readable.
  • Keep required action buttons reachable with enlarged text and during text entry.

Changing column counts to suit text sizes and translations, and using vertical layouts and scrolling, provided room for essential information and controls.

Repeatedly check the conditions with automated tests, then add assertions for issues noticed in actual rendered images. Keeping this workflow makes it possible to check that the problems found here have not returned after the next UI change.

← Go to list of development blog articles