flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.1k stars 27.43k forks source link

Loading screen when loading asynchronous localization delegates #93865

Open calii23 opened 2 years ago

calii23 commented 2 years ago

Use case

When having a localization delegate which loads asynchronously, the app renders a black screen as long as it is loading. Example:

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

void main() {
  runApp(const MainApp());
}

class DemoLocalization {
  String get welcomeText => 'Hello world';
}

class AsyncLocalizationDelegate
    extends LocalizationsDelegate<DemoLocalization> {
  const AsyncLocalizationDelegate();

  @override
  bool isSupported(Locale locale) => true;

  @override
  Future<DemoLocalization> load(Locale locale) async {
    await Future.delayed(const Duration(seconds: 10));
    return DemoLocalization();
  }

  @override
  bool shouldReload(covariant LocalizationsDelegate old) => false;
}

class MainApp extends StatelessWidget {
  const MainApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Localizations(
      delegates: const [
        AsyncLocalizationDelegate(),
        GlobalWidgetsLocalizations.delegate,
      ],
      locale: const Locale('en', 'US'),
      child: Builder(
        builder: (context) {
          final localization =
              Localizations.of<DemoLocalization>(context, DemoLocalization);
          return Center(child: Text(localization!.welcomeText));
        },
      ),
    );
  }
}

In this case, the app will show a black screen for 10 seconds, after that it shows a text "Hello World". But it is currently not possible to render a custom loading screen easily.

We would like to have that option, because we are loading the locale bundles from a remote server. This might take a bit time depending on the user's internet connection. But in the moment, this causes the app to show a black screen on every startup.

Proposal

The Localizations widget, currently renders an empty container during loading:

@override
Widget build(BuildContext context) {
  if (_locale == null)
    return Container(); // here
  return Semantics(
    textDirection: _textDirection,
    child: _LocalizationsScope(
      key: _localizedResourcesScopeKey,
      locale: _locale!,
      localizationsState: this,
      typeToResources: _typeToResources,
      child: Directionality(
        textDirection: _textDirection,
        child: widget.child!,
      ),
    ),
  );
}

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/localizations.dart#L574

It would be sufficient for us, when there is a property, in which a widget can be passed for the loading, like that:

@override
Widget build(BuildContext context) {
  if (_locale == null)
    return widget.loadingBuilder(context); // here
  return Semantics(
    textDirection: _textDirection,
    child: _LocalizationsScope(
      key: _localizedResourcesScopeKey,
      locale: _locale!,
      localizationsState: this,
      typeToResources: _typeToResources,
      child: Directionality(
        textDirection: _textDirection,
        child: widget.child!,
      ),
    ),
  );
}

The only workaround I found currently is something like that:

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

void main() {
  runApp(const MainApp());
}

class DemoLocalization {
  String get welcomeText => 'Hello world';
}

class AsyncLocalizationDelegate
    extends LocalizationsDelegate<DemoLocalization> {
  bool _loaded = false;

  @override
  bool isSupported(Locale locale) => true;

  @override
  Future<DemoLocalization> load(Locale locale) {
    assert(_loaded, 'preload() must be invoked before load(locale)');
    return SynchronousFuture(DemoLocalization());
  }

  Future<void> preload() async {
    await Future.delayed(const Duration(seconds: 3));
    _loaded = true;
  }

  @override
  bool shouldReload(covariant LocalizationsDelegate old) => false;
}

class MainApp extends StatefulWidget {
  const MainApp({Key? key}) : super(key: key);

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  late bool loading;
  late AsyncLocalizationDelegate delegate;

  @override
  void initState() {
    super.initState();

    loading = true;
    delegate = AsyncLocalizationDelegate()
      ..preload().then((value) => setState(() => loading = false));
  }

  @override
  Widget build(BuildContext context) {
    if (loading) {
      return const Directionality(
          textDirection: TextDirection.ltr,
          child: Center(child: Text('Loading...'))
      );
    }

    return Localizations(
      delegates: [
        delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      locale: const Locale('en', 'US'),
      child: Builder(
        builder: (context) {
          final localization = Localizations.of<DemoLocalization>(
              context, DemoLocalization);
          return Center(child: Text(localization!.welcomeText));
        },
      ),
    );
  }
}
goderbauer commented 2 years ago

My expectation here would be that we just keep up the app's splash screen until the localization delegate is loaded. Is your splash screen by any chance just black?

calii23 commented 2 years ago

@goderbauer Which kind of splash screen do you mean? If you mean the one from iOS which are given as LaunchScreen (I think there is an equivalent in Android too), that screen is only rendered until the flutter engine has started. Meaning that as soon as the flutter engine is there, the new storyboard is displayed. When the root widget now isn't rendering any visible widget (as it is when there is only an empty Container) the screen stays black.

fhuonder commented 2 years ago

When having a localization delegate which loads asynchronously, the app renders a black screen as long as it is loading

Hi, We hit the same issue in our app. We debugged it down and found the following: As soon as the load method in the LocalizationDelegate does not return a SynchronousFuture the issue occurrs. The reason is that when there is a "normal" Future, e.g. with the following code:

Future<MyLocalization> load(Locale locale) async {
    await Future.delayed(const Duration(seconds: 10));
    return MyLocalization();
  }

the build method in the localization.dart returns an empty Container. When this empty container is visible, the described issue occurs. When the load method returns a Synchronous future it is processed differently and therefore it works.

Hope this helps.

Regards, Florian