Search code examples
androidiosflutterlocalizationrelease

App crash in release mode due to localizations after null-safety migration


After null-safety migration and update Flutter sdk from 2.8.0 to 3.3.4, my app crashes on launch in release mode. It shows a black screen during 2 secs and then it close. Before this migration, works fine. I know the problem is on localizations because if I comment supportedLocales, localizationsDelegates and localeResolutionCallback parameter from MaterialApp widget, the Splash screen is shown.

This problem is the same for Android and iOS platforms.

  • Flutter doctor:
[✓] Flutter (Channel stable, 3.3.4, on macOS 12.6 21G115 darwin-x64, locale en-GB)
    • Flutter version 3.3.4 on channel stable at /Users/sissa/fvm/versions/3.3.4
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision eb6d86ee27 (9 days ago), 2022-10-04 22:31:45 -0700
    • Engine revision c08d7d5efc
    • Dart version 2.18.2
    • DevTools version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Users/sissa/Library/Android/sdk
    • Platform android-33, build-tools 30.0.3
    • ANDROID_HOME = /Users/sissa/Library/Android/sdk
    • Java binary at: /Applications/Android Studio 3.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.0.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14A400
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.3)
    • Android Studio at /Applications/Android Studio 3.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] VS Code (version 1.40.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (3 available)
    • PRA LX1 (mobile) • 9DC7N17707001642 • android-arm64  • Android 7.0 (API 24)
    • macOS (desktop)  • macos            • darwin-x64     • macOS 12.6 21G115 darwin-x64
    • Chrome (web)     • chrome           • web-javascript • Google Chrome 106.0.5249.119

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

  • app.dart -> build method
@override
  Widget build(BuildContext context) {
    return KeyedSubtree(
      key: key,
      child: provider.ChangeNotifierProvider<BluetoothConnectionProvider>.value(
        value: bluetoothConnectionProvider as BluetoothConnectionProvider,
        child: LiveCycleManager(
          child: MaterialApp(
              navigatorKey: NavigationService.navigatorKey,
              supportedLocales: [
                Locale('en', 'GB'),
                Locale('es', 'ES'),
                Locale('fr', 'FR'),
                Locale('ca', 'CA'),
                Locale('it', 'IT'),
                Locale('de', 'DE'),
                Locale('en', 'ES'),
                Locale('fr', 'ES'),
                Locale('ca', 'ES'),
                Locale('it', 'ES'),
                Locale('de', 'ES'),
              ],
              localizationsDelegates: [
                AppLocalizations.delegate,
                GlobalCupertinoLocalizations.delegate,
                GlobalMaterialLocalizations.delegate,
                GlobalWidgetsLocalizations.delegate,
              ],
              localeResolutionCallback: (deviceLocale, supportedLocales) {
                for (Locale locale in supportedLocales) {
                  // if device language is supported by the app,
                  // just return it to set it as current app language
                  if (supportedLocales.contains(deviceLocale)) {
                    locator<Resources>().prefs!.preferences!.setString(
                        kCurrentLocaleLanguage, deviceLocale!.languageCode);
                    return deviceLocale;
                  }
                }

                // if device language is not supported by the app,
                // the app will set it to english but return this to set to Bahasa instead
                locator<Resources>()
                    .prefs!
                    .preferences!
                    .setString(kCurrentLocaleLanguage, 'en');
                return Locale('en', 'EN');
              },
              theme: getAppTheme(),
              routes: {
                SplashScreen.routeName: (context) => SplashScreen(),
                LoginScreen.routeName: (context) => LoginScreen(),
                ResetPasswordScreen.routeName: (context) =>
                    ResetPasswordScreen(),
                RegisterScreen.routeName: (context) => RegisterScreen(),
                // .....
              },
              home: FlavorBanner(
                child: SplashScreen(),
                flavor: widget.appConfig!.flavor,
              ),
              debugShowCheckedModeBanner: false),
        ),
      ),
    );
  }
  • AppLocalizations.delegate
class AppLocalizations {
  final Locale locale;

  AppLocalizations(this.locale);

  // Helper method to keep the code in the widgets concise
  // Localizations are accessed using an InheritedWidget "of" syntax
  static AppLocalizations? of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }

  // Static member to have a simple access to the delegate from the MaterialApp
  static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();

  Map<String, String>? _localizedStrings;

  Future<bool> load() async {
    // Load the language JSON file from the "lang" folder
    printLog('LOCALE_SAMCLA:${locale.languageCode}');

    String jsonString =
        await rootBundle.loadString('i18n/${locale.languageCode}.json');
    Map<String, dynamic> jsonMap = json.decode(jsonString);

    _localizedStrings = jsonMap.map((key, value) {
      return MapEntry(key, value.toString());
    });

    return true;
  }

  // This method will be called from every widget which needs a localized text
  String? translate(String? key) {
    if (null == key)
      return _localizedStrings?['LOC_ACTION_COMPLETED_NOT_SUCCESSFULLY'];
    String? text = _localizedStrings?[key];
    return text ?? _localizedStrings?['LOC_ACTION_COMPLETED_NOT_SUCCESSFULLY'];
  }
}

// LocalizationsDelegate is a factory for a set of localized resources
// In this case, the localized strings will be gotten in an AppLocalizations object
class _AppLocalizationsDelegate
    extends LocalizationsDelegate<AppLocalizations> {
  // This delegate instance will never change (it doesn't even have fields!)
  // It can provide a constant constructor.
  const _AppLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) {
    // Include all of your supported language codes here
    return ['en', 'ca', 'fr', 'es', 'it', 'de'].contains(locale.languageCode);
  }

  @override
  Future<AppLocalizations> load(Locale locale) async {
    // AppLocalizations class is where the JSON loading actually runs
    AppLocalizations localizations = new AppLocalizations(locale);
    await localizations.load();
    return localizations;
  }

  @override
  bool shouldReload(_AppLocalizationsDelegate old) => false;
}
  • Logcat result:
Rejecting re-init on previously-failed class java.lang.Class<io.flutter.embedding.engine.FlutterJNI$$ExternalSyntheticLambda0>: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/graphics/ImageDecoder$OnHeaderDecodedListener;
    at io.flutter.embedding.engine.FlutterJNI io.flutter.embedding.engine.FlutterJNI$Factory.provideFlutterJNI() (FlutterJNI.java:123)
    at void io.flutter.FlutterInjector$Builder.fillDefaults() (FlutterInjector.java:169)
    at io.flutter.FlutterInjector io.flutter.FlutterInjector$Builder.build() (FlutterInjector.java:179)
    at io.flutter.FlutterInjector io.flutter.FlutterInjector.instance() (FlutterInjector.java:57)
    at void io.flutter.embedding.engine.FlutterEngine.<init>(android.content.Context, io.flutter.embedding.engine.loader.FlutterLoader, io.flutter.embedding.engine.FlutterJNI, io.flutter.plugin.platform.PlatformViewsController, java.lang.String[], boolean, boolean) (FlutterEngine.java:289)
    at void io.flutter.embedding.engine.FlutterEngine.<init>(android.content.Context, java.lang.String[], boolean, boolean) (FlutterEngine.java:207)
    at void io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.setupFlutterEngine() (FlutterActivityAndFragmentDelegate.java:271)
    at void io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onAttach(android.content.Context) (FlutterActivityAndFragmentDelegate.java:180)
    at void io.flutter.embedding.android.FlutterActivity.onCreate(android.os.Bundle) (FlutterActivity.java:498)
    at void android.app.Activity.performCreate(android.os.Bundle) (Activity.java:6915)
    at void android.app.Instrumentation.callActivityOnCreate(android.app.Activity, android.os.Bundle) (Instrumentation.java:1123)
    at android.app.Activity android.app.ActivityThread.performLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent) (ActivityThread.java:2746)
    at void android.app.ActivityThread.handleLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:2864)
    at void android.app.ActivityThread.-wrap12(android.app.ActivityThread, android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:-1)
    at void android.app.ActivityThread$H.handleMessage(android.os.Message) (ActivityThread.java:1567)
    at void android.os.Handler.dispatchMessage(android.os.Message) (Handler.java:105)
    at void android.os.Looper.loop() (Looper.java:156)
    at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6523)
    at java.lang.Object java.lang.reflect.Method.invoke!(java.lang.Object, java.lang.Object[]) (Method.java:-2)
    at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run() (ZygoteInit.java:942)
    at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:832)
Caused by: java.lang.ClassNotFoundException: Didn't find class "android.graphics.ImageDecoder$OnHeaderDecodedListener" on path: DexPathList[[zip file "/data/app/com.xxxxx.xxxxx.dev-2/base.apk"],nativeLibraryDirectories=[/data/app/com.xxxxx.xxxxx.dev-2/lib/arm64, /data/app/com.xxxxx.xxxxx.dev-2/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64, /system/vendor/lib64, /product/lib64]]
    at java.lang.Class dalvik.system.BaseDexClassLoader.findClass(java.lang.String) (BaseDexClassLoader.java:56)
    at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String, boolean) (ClassLoader.java:380)
    at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String) (ClassLoader.java:312)
    at io.flutter.embedding.engine.FlutterJNI io.flutter.embedding.engine.FlutterJNI$Factory.provideFlutterJNI() (FlutterJNI.java:123)
    at void io.flutter.FlutterInjector$Builder.fillDefaults() (FlutterInjector.java:169)
    at io.flutter.FlutterInjector io.flutter.FlutterInjector$Builder.build() (FlutterInjector.java:179)
    at io.flutter.FlutterInjector io.flutter.FlutterInjector.instance() (FlutterInjector.java:57)
    at void io.flutter.embedding.engine.FlutterEngine.<init>(android.content.Context, io.flutter.embedding.engine.loader.FlutterLoader, io.flutter.embedding.engine.FlutterJNI, io.flutter.plugin.platform.PlatformViewsController, java.lang.String[], boolean, boolean) (FlutterEngine.java:289)
    at void io.flutter.embedding.engine.FlutterEngine.<init>(android.content.Context, java.lang.String[], boolean, boolean) (FlutterEngine.java:207)
    at void io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.setupFlutterEngine() (FlutterActivityAndFragmentDelegate.java:271)
    at void io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onAttach(android.content.Context) (FlutterActivityAndFragmentDelegate.java:180)
    at void io.flutter.embedding.android.FlutterActivity.onCreate(android.os.Bundle) (FlutterActivity.java:498)
    at void android.app.Activity.performCreate(android.os.Bundle) (Activity.java:6915)
    at void android.app.Instrumentation.callActivityOnCreate(android.app.Activity, android.os.Bundle) (Instrumentation.java:1123)
    at android.app.Activity android.app.ActivityThread.performLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent) (ActivityThread.java:2746)
    at void android.app.ActivityThread.handleLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:2864)
    at void android.app.ActivityThread.-wrap12(android.app.ActivityThread, android.app.ActivityThread$ActivityClientRecord, android.content.Intent, java.lang.String) (ActivityThread.java:-1)
    at void android.app.ActivityThread$H.handleMessage(android.os.Message) (ActivityThread.java:1567)
    at void android.os.Handler.dispatchMessage(android.os.Message) (Handler.java:105)
    at void android.os.Looper.loop() (Looper.java:156)
    at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6523)
    at java.lang.Object java.lang.reflect.Method.invoke!(java.lang.Object, java.lang.Object[]) (Method.java:-2)
    at void com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run() (ZygoteInit.java:942)
    at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:832)

Solution

  • Finally I found the solution but I don't understand it. The problem was on routes definition in app.dart. All classes have to be constants.

    Before

    routes: {
                    SplashScreen.routeName: (context) => SplashScreen(),
                    LoginScreen.routeName: (context) => LoginScreen(),
    }
    

    After

    routes: {
                    SplashScreen.routeName: (context) => const SplashScreen(),
                    LoginScreen.routeName: (context) => const LoginScreen(),
    }
    

    If somebody knows why it worked, feel free to explain it to us ;)