简体   繁体   中英

Flutter - Using GetIt with BuildContext

I'm using Localizations in my app based on the flutter documentation.
See here: https://flutter.dev/docs/development/accessibility-and-localization/internationalization

I use get_it package (version 4.0.4) to retrieve singleton objects like the Localization delegate. Unfortunately it needs a BuildContext property. Sometimes in my app I don't have the context reference so it would be nice if it would work like this: GetIt.I<AppLocalizations>() instead of this: AppLocalizations.of(context) . It still can be achieved without a problem if you setup get_it like this: GetIt.I.registerLazySingleton(() => AppLocalizations.of(context)); The problem is that you need the context at least once to make it work. Moreover if you would like to display a localized text instantly in your initial route it's more difficult to get a properly initialized BuildContext at a time when you need it.

It's a little hard for me to explain it properly so I recreated the issue in a minimal example.

I commented out some code that would cause compile time errors, but it shows how I imagined it to be done.

main.dart

GetIt getIt = GetIt.instance;

void setupGetIt() {
  // How to get BuildContext properly if no context is available yet?
  // Compile time error.
  // getIt.registerLazySingleton(() => AppLocalizations.of(context));
}

void main() {
  setupGetIt();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    // The above line also won't work. It has BuildContext but Applocalizations.of(context) won't work
    // because it's above in the Widget tree and not yet setted up.
    getIt.registerLazySingleton(() => AppLocalizations.of(context));
    return MaterialApp(
      supportedLocales: const [
        Locale('en', 'US'),
        Locale('hu', 'HU'),
      ],
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      localeResolutionCallback: (locale, supportedLocales) {
        // check if locale is supported
        for (final supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale?.languageCode &&
              supportedLocale.countryCode == locale?.countryCode) {
            return supportedLocale;
          }
        }
        // if locale is not supported then return the first (default) one
        return supportedLocales.first;
      },
      // You may pass the BuildContext here for Page1 in it's constructor 
      // but in a more advanced routing case it's not a maintanable solution.
      home: Page1(),
    );
  }
}

Initial route

class PageBase extends StatelessWidget {
  final String title;
  final Widget content;

  PageBase(this.title, this.content);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: content,
    );
  }
}

class Page1 extends PageBase {
  // It won't run because I need the context but clearly I don't have it.
  // And in a real app you also don't want to pass the context all over the place 
     if you have many routes to manage.
  Page1(String title)
      : super(AppLocalizations.of(context).title, Center(child: Text('Hello')));

  // Intended solution
  // I don't know how to properly initialize getIt AppLocalizations singleton by the time
  // it tries to retrieve it
  Page1.withGetIt(String title)
      : super(getIt<AppLocalizations>().title, Center(child: Text('Hello')));
}

locales.dart

String globalLocaleName;

class AppLocalizations {
  //AppLocalizations(this.localeName);

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

  static const LocalizationsDelegate<AppLocalizations> delegate =
      _AppLocalizationsDelegate();

  static Future<AppLocalizations> load(Locale locale) async {
    final String name =
        locale.countryCode.isEmpty ? locale.languageCode : locale.toString();

    final String localeName = Intl.canonicalizedLocale(name);

    return initializeMessages(localeName).then((_) {
      globalLocaleName = localeName;
      return AppLocalizations();
    });
  }

  String get title => Intl.message(
        'This is the title.',
        name: 'title',
      );
}

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) {
    return ['en', 'hu'].contains(locale.languageCode);
  }

  @override
  Future<AppLocalizations> load(Locale locale) => AppLocalizations.load(locale);

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

And some intl generated dart code and.arb files that is not so important to illustrate the problem.


So all in all, how can I achive to use my AppLocalizations class as a singleton without using a context for example in a situation like this? Maybe my initial approach is bad and it can be done in other ways that I represented. Please let me know if you have a solution.

Thank you.

To achieve what you have described you need to first make the navigation service using get_it . Follow these steps to achieve the result:

1. Create a navigation service

import 'package:flutter/material.dart';

class NavigationService {
  final GlobalKey<NavigatorState> navigatorKey =
      new GlobalKey<NavigatorState>();

  Future<dynamic> navigateTo(String routeName) {
    return navigatorKey.currentState!
        .push(routeName);
  }

  goBack() {
    return navigatorKey.currentState!.pop();
  }
}

This allows you to navigate anywhere from any point throughout the app without build context. This navigator key is what you can use to achieve the AppLocalization instance for the current context. Refer to the FilledStacks tutorials for this method of navigating without build context.

https://www.filledstacks.com/post/navigate-without-build-context-in-flutter-using-a-navigation-service/

2. Register

GetIt locator = GetIt.instance;

void setupLocator() {
  ...
  locator.registerLazySingleton(() => NavigationService());
  ...
}

3. Assign the navigator key in the material app

return MaterialApp(
    ...
    navigatorKey: navigationService.navigatorKey,
    ...
  ),

3. Create an instance for the AppLocalizations and import it wherever you want to use

localeInstance() => AppLocalizations.of(locator<NavigationService>().navigatorKey.currentContext!)!;

3. The actual use case

import 'package:{your_app_name}/{location_to_this_instace}/{file_name}.dart';

localeInstance().your_localization_variable

You can add a builder to your MaterialApp and setup the service locator inside it with the context available. Example:

  Widget build(BuildContext context) {
    return MaterialApp(
          builder: (context, widget) {
            setUpServiceLocator(context);
            return FutureBuilder(
                future: getIt.allReady(),
                builder: (BuildContext context, AsyncSnapshot snapshot) {
                  if (snapshot.hasData) {
                    return widget;
                  } else {
                    return Container(color: Colors.white);
                  }
                });
          },
        );
}

Service Locator Setup:

void setUpServiceLocator(BuildContext context) {
  getIt.registerSingleton<AppLocalizations>(AppLocalizations.of(context));
}

You could use some non-localizable splash screen with FutureBuilder and getIt.allReady() .

Something like:

  class SplashScreen extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return FutureBuilder<void>(
        future: getIt.allReady(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            // Navigate to main page (with replace)
          } else if (snapshot.hasError) {
            // Error handling
          } else {
            // Some pretty loading indicator
          }
        },
      );
    }

I'd like to recommend the injectable package for dealing with get_it also.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM