skip to content

In a Flutter currency converter supporting en, ar and he, how is the app locale resolved, and when should you write localeListResolutionCallback?

level: seniorimportance: nice to knowfreq 22%

answer

  1. list callback, single callback, default
  2. null means fall through
  3. single callback sees only the first
  4. language-only match loses the country
  5. iw is already he

basics

~20 s

WidgetsApp tries localeListResolutionCallback, then localeResolutionCallback, then basicLocaleListResolution, moving on whenever a callback returns null. Write the list callback only for a product rule the default cannot express; the single callback sees just the first preferred locale.

solid answer

~40 s

At startup and whenever the device's locale list changes, `WidgetsApp` resolves one locale. It first calls `localeListResolutionCallback(preferredLocales, supportedLocales)`; if that returns `null` it calls `localeResolutionCallback` with only the **first** preferred locale; if that also returns `null` it runs `basicLocaleListResolution`. The default walks the user's list and prefers a perfect match, then language plus script, then language plus country, then a language-only match, then a country-only match, and finally `supportedLocales.first`. Write `localeListResolutionCallback` for a rule the default cannot express, such as mapping one unsupported language to a chosen fallback, and prefer it over `localeResolutionCallback`, whose single-locale view breaks users with several languages. Two traps: a language-only match returns your supported `Locale('ar')`, so the user's country is gone, and `Locale('iw')` is already canonicalised to `he`.

code

dart · 18 lines
dart
import 'package:flutter/material.dart';

Locale? resolveConverterLocale(
  List<Locale>? preferred,
  Iterable<Locale> supported,
) {
  for (final locale in preferred ?? const <Locale>[]) {
    for (final candidate in supported) {
      if (candidate.languageCode == locale.languageCode) {
        return candidate;
      }
    }
  }
  // No shared language at all: English, whatever the list order says.
  return const Locale('en');
}

// MaterialApp(localeListResolutionCallback: resolveConverterLocale, ...)

go deeper

for a junior

Know that Flutter picks the app locale by matching the device's preferred languages against supportedLocales, falling back to the first entry.

for a middle

Explain the order list callback, single callback, default, what returning null means, and the default algorithm's priorities.

for a senior

Choose the list callback over the single one, keep region data from the device list, test resolution with simulated locale lists, and remove needless special cases like iw.

for a principal

Define the product's fallback-language policy per market and decide whether it lives in supportedLocales order, a callback, or an in-app picker.

## The resolution pipeline `WidgetsApp` (and so `MaterialApp`) resolves **one** app locale from the device's ordered list of preferred locales and your `supportedLocales`. It runs at startup and again whenever the platform reports a new locale list. The steps: 1. **`localeListResolutionCallback(List<Locale>? locales, Iterable<Locale> supportedLocales)`**, if set. A non-null result wins. 2. **`localeResolutionCallback(Locale? locale, Iterable<Locale> supportedLocales)`**, if set, called with only the **first** preferred locale. A non-null result wins. 3. **`basicLocaleListResolution`**, the default algorithm. Returning `null` from a callback is the documented way to say "no opinion, continue". When `MaterialApp.locale` is set (an in-app language picker), the same pipeline runs with that single locale as the preferred list. ## What the default algorithm does For each preferred locale in order, `basicLocaleListResolution` looks for: 1. a **perfect match** of language, script and country; 2. a **language plus script** match; 3. a **language plus country** match; 4. a **language-only** match, returned at once for the first preferred locale, or kept while the next preferred locale is checked for a better match; 5. after the list is exhausted, a **country-only** match; 6. otherwise **`supportedLocales.first`**. It does not know about language distance: a Persian speaker is not steered towards Arabic, and a French speaker falls to the first supported locale. ## When a custom callback earns its place - **A product fallback rule.** For example, users whose only languages are unsupported should see English, even if the generated `supportedLocales` list starts with `ar`. Returning `const Locale('en')` when nothing matches expresses that (reordering `supportedLocales` is the lighter alternative). - **Region-aware mapping.** Mapping several regional variants onto the one you ship, when the default's priorities would pick another. - **A remote or saved preference** that must be checked against what the device offers. Prefer **`localeListResolutionCallback`**. `localeResolutionCallback` only sees the first preferred locale, so a user whose list is `[fr, he]` gets French evaluated alone; returning a fallback there skips the Hebrew the user also reads. ## Traps specific to this app | Trap | What happens | Fix | |---|---|---| | Country lost on a language-only match | A device set to `ar_EG` resolves to your `Locale('ar')`, so `Localizations.localeOf` has no `EG` | Read the device list from `PlatformDispatcher.instance.locales` when you need the country, for example to default the source currency | | Writing a callback for `iw` | Unnecessary: `Locale('iw')` has `languageCode` `he`, and the two compare equal | Delete the special case | | Returning a locale not in `supportedLocales` | The delegates are asked to load a locale they may not support, and localized resources go missing | Only return members of `supportedLocales` | | Slow or async logic | Callbacks are synchronous and cannot await a stored preference | Resolve saved preferences before `runApp`, then use `MaterialApp.locale` | ## An in-app language picker Many currency apps let users override the phone's language. The robust shape does not need a resolution callback at all: 1. Store the user's choice (for example `he`) with your persistence layer and read it before `runApp`. 2. Pass it as `MaterialApp.locale`; leave it `null` to follow the device. 3. Rebuild `MaterialApp` with the new value when the user changes the setting; the pipeline resolves that single locale against `supportedLocales`, and `Localizations` reloads the delegates. A callback that reads the stored choice works too, but it mixes a user setting into device matching and cannot wait for storage, so the explicit `locale` property is easier to reason about and to test. ## Keeping formatting in step Whatever the pipeline returns is what `Localizations.localeOf(context)` reports, so pass that value to `NumberFormat` and `DateFormat`. If you chose to show English to French users, amounts should also be formatted in English, not in the device's French conventions. ## Testing Resolution is a pure function of its inputs, so it tests well without a device: - call your callback directly with lists such as `[Locale('fr'), Locale('he')]` and assert the result; - pump `MaterialApp` in a widget test with `locale:` set and assert `Localizations.localeOf`; - use `tester.platformDispatcher.localesTestValue` to simulate a device language list end to end.

  • A Hebrew user's device list is [fr_FR, he_IL]; what does a localeResolutionCallback that returns supportedLocales.first on no match do to them?
    It receives only `fr_FR`, finds no match and returns the first supported locale, so the Hebrew in second place is never considered. With no callback, or with `localeListResolutionCallback`, the default algorithm would continue to `he_IL` and pick Hebrew.
  • How do you know the user is in Egypt when the app resolved to plain ar?
    The resolved locale is your supported `Locale('ar')`, which has no country. Read `PlatformDispatcher.instance.locales` (or `WidgetsBinding.instance.platformDispatcher.locales`) for the device's full list, including `ar_EG`, and use that only for region defaults such as the home currency.
  • Do you need a special case for devices that report Hebrew as iw?
    No. `Locale` canonicalises the deprecated `iw` subtag to `he`, so `Locale('iw')` and `Locale('he')` are equal and matching works without extra code.

saying these in an interview costs you the question

  • localeResolutionCallback receives the full list of preferred locales.
  • The default algorithm picks the closest related language, such as Arabic for Persian.
  • Returning null from a resolution callback leaves the app without a locale.
  • Hebrew devices reporting iw need a custom mapping callback.
  • The resolved locale always keeps the device's country code.