← Back to Blog

Fix Null Check Operator on AppLocalizations.of(context)!

flutterlocalizationgen-l10napplocalizationsnull safetyl10n.yaml

Fix Null Check Operator on AppLocalizations.of(context)!

The stack trace says Null check operator used on a null value and points at a line like this:

Text(AppLocalizations.of(context)!.helloWorld)

AppLocalizations.of(context) returns null for one reason only: there is no loaded AppLocalizations instance above the BuildContext you passed in. The ! then turns that null into a crash. Everything below is about finding out why the instance is missing, and then making the mistake harder to repeat.

Why AppLocalizations.of(context) returns null

MaterialApp (and CupertinoApp, WidgetsApp) builds a Localizations widget from its localizationsDelegates and supportedLocales. The generated of method walks up from your context looking for that widget:

static AppLocalizations? of(BuildContext context) {
  return Localizations.of<AppLocalizations>(context, AppLocalizations);
}

If the context sits above the Localizations widget, or the delegate was never registered, or the delegate refused to load for the current locale, the lookup finds nothing and returns null.

Diagnosis table: match the crash to its cause

Where it crashes Real cause Fix
title: of MaterialApp, or anything in the same build as MaterialApp The context belongs to a widget above MaterialApp, so there is no Localizations ancestor yet Use onGenerateTitle, or move the UI into its own widget (or a Builder) below MaterialApp
Every screen, from the first frame AppLocalizations.delegate is not in localizationsDelegates Pass AppLocalizations.localizationsDelegates and AppLocalizations.supportedLocales
Only on some devices or languages supportedLocales was hand-written and lists a locale that has no ARB file, so the delegate does not load for it Use the generated AppLocalizations.supportedLocales
initState Inherited widgets cannot be read there Move the lookup to didChangeDependencies or build
One feature, a package screen, an embedded flow A second MaterialApp or WidgetsApp without delegates Remove the inner app, or give it the same delegates and locales
Widget tests only The widget is pumped without an app shell Wrap it in a MaterialApp that has the delegates

1. The context is above MaterialApp

This is the classic main.dart case. The context in the build method that returns MaterialApp is a parent of the app, so it cannot see the localizations that the app creates.

// Crashes: this context is above MaterialApp.
return MaterialApp(
  title: AppLocalizations.of(context)!.appTitle,
  home: const HomePage(),
);

// Works: onGenerateTitle receives a context below Localizations.
return MaterialApp(
  onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomePage(),
);

The same applies to a Scaffold written inline as home: that reads the outer context. Extract it into its own widget, or wrap it in a Builder to get a context that lives under the app.

2. Missing localizationsDelegates or supportedLocales

If Flutter localizationsDelegates is missing your app delegate, nothing ever loads your strings. Listing only the three Global* delegates is a common slip after copying an older tutorial. The generated class already exposes complete lists, so use them:

import 'package:my_app/l10n/app_localizations.dart';

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomePage(),
);

If the import itself fails after an upgrade, that is a different problem. See Flutter 3.38: Fix AppLocalizations Not Found After Upgrade.

3. A locale in supportedLocales that has no ARB file

Flutter does not return null just because the device language is unsupported. Per the official docs, if there is no exact match it uses the first supported locale with the same language code, and otherwise the first entry in supportedLocales.

The null appears when the list is hand-written and out of sync with your ARB files. Say you declare Locale('fr') but only ship app_en.arb. On a French device Flutter resolves to fr, the generated delegate reports that it does not support fr, nothing is loaded, and AppLocalizations.of(context) returns null. It works on your English test phone and crashes for French users.

AppLocalizations.supportedLocales is generated from the ARB files, so it cannot drift. To pick the fallback language, set preferred-supported-locales in l10n.yaml rather than rewriting the list by hand.

4. Lookups in initState

Localizations.of depends on an inherited widget, and that is not allowed in initState. In debug builds you usually get an assertion saying dependOnInheritedWidgetOfExactType was called before initState() completed. Either way the fix is the same:

late String _greeting;

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  _greeting = AppLocalizations.of(context)!.helloWorld;
}

didChangeDependencies also runs again when the locale changes, so the string stays correct.

5. A second MaterialApp without delegates

A nested Navigator on its own is fine, because it still sits under the root Localizations widget. Dialogs and bottom sheets are fine too, as long as you use the context given to their builder.

The real culprit is a second MaterialApp or WidgetsApp further down the tree, often added to give a feature its own navigation stack or theme. It creates a new Localizations scope with only the default delegates, which hides yours for everything below it. Replace it with a plain Navigator, or pass the same delegates and supported locales to the inner app.

The permanent fix: flutter nullable-getter false

Once the tree is correct, AppLocalizations.of(context) is never null at runtime, so the ! on every call is just noise. Flutter's l10n.yaml nullable-getter option removes it. The docs describe the default as true for backwards compatibility. Set it to false:

# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false

Run flutter gen-l10n (or flutter run). The generated method now returns a non-nullable type:

static AppLocalizations of(BuildContext context) {
  return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
}

Be clear about what this does. The null check has moved into generated code, so a misconfigured tree still throws. What you gain is cleaner call sites and one place where the assumption lives. Delete the ! everywhere (the analyzer flags each leftover as an unnecessary non-null assertion) and fix the tree using the table above.

Add a context.l10n extension

// lib/l10n/l10n.dart
import 'package:flutter/widgets.dart';
import 'package:my_app/l10n/app_localizations.dart';

extension AppLocalizationsX on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this);
}

Call sites shrink to Text(context.l10n.helloWorld). It also gives you one line to change if the lookup ever needs to.

Wrap tests with the delegates so it cannot come back

A widget pumped on its own has no MaterialApp, so it hits cause 2 every time. Put the wrapper in one helper and use it in every test:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/l10n/app_localizations.dart';

extension PumpLocalized on WidgetTester {
  Future<void> pumpLocalized(
    Widget child, {
    Locale locale = const Locale('en'),
  }) async {
    await pumpWidget(
      MaterialApp(
        locale: locale,
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        home: Scaffold(body: child),
      ),
    );
    await pumpAndSettle();
  }
}

Pump your real app widget in at least one test as well. That is the test that catches a missing delegate or a stray inner MaterialApp before users do. The full reusable harness, including per-locale runs, is in Fix AppLocalizations.of(context) Null in Widget Tests.

Quick checklist

  1. AppLocalizations.localizationsDelegates and AppLocalizations.supportedLocales on the root app.
  2. No lookups with a context above MaterialApp. Use onGenerateTitle for the title.
  3. No lookups in initState.
  4. No second MaterialApp without delegates.
  5. nullable-getter: false in l10n.yaml, plus a context.l10n extension.
  6. One shared test wrapper with the delegates.

Keep the ARB files in step too

A null crash is the loud failure. The quiet ones are a key that exists in app_en.arb but not in app_fr.arb, or a plural that is missing a category the language needs. The FlutterLocalisation ARB editor lets you edit app_<locale>.arb files in a UI instead of raw JSON and manage translations across many locales, and its ICU plural validation flags locales missing a plural category such as few or many for Arabic, Polish or Russian. More guides are on the blog.

Try FlutterLocalisation free and keep every locale complete before it ships.