Flutter - Nullability problem with Locale

Viewed 4356

Below code has no compiler warnings or errors. Running it results in:

lib/main.dart:26:51: Error: The argument type 'Locale?' can't be assigned to the parameter type 'Locale' because 'Locale?' is nullable and 'Locale' isn't.
 - 'Locale' is from 'dart:ui'.
    thisAppsLocaleNotifier = ValueNotifier(window.locale);
                                                  ^
lib/main.dart:27:43: Error: Property 'languageCode' cannot be accessed on 'Locale?' because it is potentially null.
 - 'Locale' is from 'dart:ui'.
Try accessing using ?. instead.
    Localization.langCode = window.locale.languageCode;
                                          ^^^^^^^^^^^^
Failed to compile application.

Neither window nor locale could be null. No parameters here can be null. The error message is quite annoying because trying

thisAppsLocaleNotifier = ValueNotifier(window.locale ?? Locale('en'));

Results in a compiler warning:

The left operand can't be null, so the right operand is never executed.  Try removing the operator and the right operand.

This is the code:

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

import 'main.i18n.dart';

/// Supported locales. First entry `en` is default.
const List<Locale> supportedLocales = [
  Locale('en', 'US'),
  Locale('de', 'DE'),
  Locale('es', 'ES'),
];

late ValueNotifier<Locale> thisAppsLocaleNotifier;

/// Entry point for example application
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp() {
    thisAppsLocaleNotifier = ValueNotifier(window.locale);
    Localization.langCode = window.locale.languageCode;
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: thisAppsLocaleNotifier,
      builder: (BuildContext context, Locale thisAppsLocale, Widget? child) =>
          MaterialApp(
        home: MyHomePage(),
        locale: thisAppsLocale,
        localizationsDelegates: [
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
        ],
        supportedLocales: supportedLocales,
        theme: ThemeData(
          primarySwatch: Colors.orange,
        ),
        title: 'input_country',
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('_MyHomePageState build');
    return Scaffold(
      appBar: AppBar(
        title: Text('App Title'.i18n),
      ),
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            Text('thisAppsLocale = ${thisAppsLocaleNotifier.value} / langCode ='
                ' ${Localization.langCode}'),
            ButtonBar(
              alignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => setLocale(Locale('de')),
                  child: Text('de'),
                ),
                ElevatedButton(
                  onPressed: () => setLocale(Locale('en')),
                  child: Text('en'),
                ),
                ElevatedButton(
                  onPressed: () => setLocale(Locale('es')),
                  child: Text('es'),
                ),
              ],
            )
          ],
        ),
      ),
    );
  }

  void setLocale(Locale newLocale) {
    Localization.langCode = newLocale.languageCode;
    thisAppsLocaleNotifier.value = newLocale;
  }
}

And this is pubspec.yaml

name: qq
description: A new Flutter application.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

What is required to run the code in null-safety mode?

This is the localization class main.i18n.dart:

extension Localization on String {
  static String langCode = 'en';

  String get i18n => localize(this, _t);

  static final Map<String, Map<String, String>> _t = {
    'All': {
      'de': 'Alle',
      'es': 'Todas',
    },
    'Example': {
      'de': 'Beispiel',
      'es': 'Ejemplo',
    },
    'Languages': {
      'de': 'Sprachen',
      'es': 'Idiomas',
    },
    'Selectables': {
      'de': 'Einträge',
      'es': 'Entradas',
    },
  };

  String localize(String english, Map<String, Map<String, String>> t10ns) {
    if (langCode != 'en') {
      Map<String, String>? _t10ns = t10ns[english];
      if (_t10ns == null) {
        print('No translations found for "$english"');
      } else {
        String? translated = _t10ns[langCode];
        if (translated == null) {
          print('Translation to language "$langCode" missing for "$english"');
        } else {
          return translated;
        }
      }
    }
    return english;
  }
}

Output of Flutter doctor:

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.0.3, on Microsoft Windows [Version 10.0.19042.867], locale de-DE)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[√] Chrome - develop for the web
[√] Android Studio (version 4.1.0)
[√] Connected device (3 available)

• No issues found!
2 Answers

It looks like it was a bug with flutter web. It should be fixed in one of the next releases

https://github.com/flutter/engine/pull/24922


What you can do for now to make it work on web and avoid warnings on desktop or mobiles:

Create 3 files:

  • shared.dart
  • mobile.dart
  • web.dart
// shared.dart
export 'web.dart' if (dart.library.io) 'mobile.dart';
// mobile.dart
import 'dart:ui';

Locale getLocale() => window.locale;
// web.dart
import 'dart:ui';

// ignore: unnecessary_non_null_assertion
Locale getLocale() => window.locale!;

Then in your code, only import shared.dart and get the Locale from there:

main.dart
import 'shared.dart';

final Locale locale = getLocale();

There is a comprehensive explanation of null safety here: Null safety in Flutter. It includes a link to the migration guide that will help you migrate your code.

Note that in the pubspec:

environment:
  sdk: ">=2.12.0 <3.0.0"

specifying the sdk version >=2.12.0 enables null safety, with the consequent checks. Specifying any earlier version will disable null safety.

In order to fix your code for null safety, you need to check for non-null values where they are not permitted - the error messages generally point this out. So the errors:

lib/main.dart:26:51: Error: The argument type 'Locale?' can't be assigned to the parameter type 'Locale' because 'Locale?' is nullable and 'Locale' isn't.
 - 'Locale' is from 'dart:ui'.
    thisAppsLocaleNotifier = ValueNotifier(window.locale);

lib/main.dart:27:43: Error: Property 'languageCode' cannot be accessed on 'Locale?' because it is potentially null.
 - 'Locale' is from 'dart:ui'.
Try accessing using ?. instead.
    Localization.langCode = window.locale.languageCode;

can be fixed by asserting that the parameter is not null using window.locale!.

Your frustration with window.locale ?? ... was caused because this does not check for window being null.

Related