Como ler strings localizadas fora dos widgets usando Riverpod
Se quiser implantar seu aplicativo Flutter para usuários que falam outro idioma, você precisará localizá-lo. E a documentação do Flutter já possui um guia detalhado de internacionalização que aborda esse tópico em profundidade.
Mas se tivermos alguma lógica de negócios fora de nossos widgets, como podemos ler as strings localizadas?
Este artigo mostra como abordo a localização usando o pacote Riverpod em meus próprios aplicativos.
Este artigo é baseado no pacote oficial
flutter_localization
, que suporta mais de 78 localidades. O site Flutter Gems lista muitos outros pacotes de localização aqui.
Conteudo
Requisitos de localização de aplicativos
Antes de mergulharmos no código, vamos descobrir o que queremos fazer:
- acessar strings localizadas fora de nossos widgets (sem usar
BuildContext
) - garantir que todos os ouvintes (provedores e widgets) sejam reconstruídos quando a localidade for alterada
Para satisfazer esses requisitos, precisamos combinar duas coisas:
- um
Provider<AppLocalizations>
que podemos usar em qualquer lugar do aplicativo - uma classe
LocaleObserver
que podemos usar para rastrear alterações de localidade
Veja como implementá-los. 👇
1. Criando o provedor AppLocalizations
Para obter o objeto AppLocalizations
correto para a localidade atual, podemos usar o método lookupAppLocalizations
, que é gerado quando executamos o comando flutter gen-l10n
.
Portanto, o primeiro passo é criar um provedor que o utilize:
import 'package:flutter_riverpod/flutter_riverpod.dart'; // lookupAppLocalizations é definido aqui 👇 import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:ui' as ui; /// provedor usado para acessar o objeto AppLocalizations para a localidade atual final appLocalizationsProvider = Provider<AppLocalizations>((ref) { return lookupAppLocalizations(ui.window.locale); });
Mas isso não é suficiente porque nosso provedor não reconstruirá e notificará seus ouvintes quando a localidade for alterada.
2. Adicionando um Locale Observer
Para acompanhar as alterações de localidade, podemos criar uma subclasse de WidgetsBindingObserver
e substituir o método didChangeLocales
:
/// observador usado para notificar o chamador quando a localidade muda class _LocaleObserver extends WidgetsBindingObserver { _LocaleObserver(this._didChangeLocales); final void Function(List<Locale>? locales) _didChangeLocales; @override void didChangeLocales(List<Locale>? locales) { _didChangeLocales(locales); } }
E agora que temos isso, podemos atualizar nosso código de provedor:
/// provedor usado para acessar o objeto AppLocalizations para a localidade atual final appLocalizationsProvider = Provider<AppLocalizations>((ref) { // 1. inicializar a partir do local inicial ref.state = lookupAppLocalizations(ui.window.locale); // 2. crie um observador para atualizar o estado final observer = _LocaleObserver((locales) { ref.state = lookupAppLocalizations(ui.window.locale); }); // 3. registrar o observador e descartá-lo quando não for mais necessário final binding = WidgetsBinding.instance; binding.addObserver(observer); ref.onDispose(() => binding.removeObserver(observer)); // 4. devolver o estado return ref.state; });
Isso garante que quaisquer outros provedores ou widgets que dependam de appLocalizationsProvider
serão reconstruídos quando a localidade for alterada.
A seguir, vamos ver como usar nosso provedor.
3. Usando o provedor AppLocalizations
A ideia principal é que possamos ler o appLocalizationsProvider
usando um objeto ref
:
// obtenha o objeto AppLocalizations (leia uma vez) final loc = ref.read(appLocalizationsProvider); // leia uma propriedade definida no arquivo *.arb final error = loc.addToCartFailed;
Por exemplo, considere esta classe CartService
usada para atualizar um carrinho de compras:
class CartService { CartService(this.ref); // declarar ref como uma propriedade final Ref ref; Future<void> addItem(Item item) async { try { // buscar o carrinho final cart = ref.read(cartRepositoryProvider).fetchCart(); // retornar uma cópia com os dados atualizados final updated = cart.addItem(item); // configure o carrinho com os dados atualizados await ref.read(cartRepositoryProvider).setCart(updated); } catch (e) { // receba a mensagem de erro localizada final errorMessage = ref.read(appLocalizationsProvider).addToCartFailed; // jogue isso como uma exceção throw Exception(errorMessage); } } } // o fornecedor correspondente final cartServiceProvider = Provider<CartService>((ref) { return CartService(ref); });
Esta classe CartService
recebe um argumento Ref
, que é usado para chamar ref.read(appLocalizationsProvider)
dentro do bloco catch – tornando assim AppLocalizations
uma dependência implícita.
Nesse caso, devemos ler (não observar) o
appLocalizationsProvider
, pois precisamos recuperar a mensagem de erro uma vez quando o métodoaddItem
for chamado.
Mas se quisermos tornar a dependência de AppLocalizations
mais explícita, podemos fazer isso:
class CartService { CartService({required this.cartRepository, required this.loc}); // declarar dependências como propriedades explícitas final CartRepository cartRepository; final AppLocalizations loc; Future<void> addItem(Item item) async { try { // buscar o carrinho final cart = cartRepository.fetchCart(); // retornar uma cópia com os dados atualizados final updated = cart.addItem(item); // configure o carrinho com os dados atualizados await cartRepository.setCart(updated); } catch (e) { // receba a mensagem de erro localizada final errorMessage = loc.addToCartFailed; // jogue isso como uma exceção throw Exception(errorMessage); } } } // o fornecedor correspondente final cartServiceProvider = Provider<CartService>((ref) { return CartService( // passe as dependências explicitamente (usando watch) cartRepository: ref.watch(cartRepositoryProvider), loc: ref.watch(appLocalizationsProvider), ); });
Nesse caso, devemos observar (não ler) o
appLocalizationsProvider
para reconstruir o CartService retornado pelocartServiceProvider
quando a localidade for alterada.
De qualquer forma, agora podemos acessar AppLocalizations
em qualquer lugar dentro de nossos provedores sem usar BuildContext
.
Bônus: usando AppLocalizations para tratamento de erros
Se usarmos freezed
, podemos definir nossa própria classe de exceção específica de domínio:
@freezed class AppException with _$AppException { const factory AppException.permissionDenied() = PermissionDenied; const factory AppException.paymentFailed() = PaymentFailed; // adicione outros tipos de erro aqui }
E se quisermos mapear cada erro para uma mensagem de erro localizada (que mostraremos na UI), podemos criar esta extensão simples:
extension AppExceptionMessage on AppException { String message(AppLocalizations loc) { return when( permissionDenied: () => loc.permissionDeniedMessage, paymentFailed: () => loc.paymentFailedMessage, // e assim por diante... ); } }
Então, podemos definir um estado de erro com uma string localizada dentro de uma subclasse StateNotifier<AsyncValue>
que nossos widgets podem ouvir:
// dentro de uma subclasse StateNotifier final exception = AppException.permissionDenied(); final loc = ref.read(appLocalizationsProvider); state = AsyncError(exception.message(loc));
Mais uma vez, nenhum BuildContext
é necessário para ler strings localizadas fora de nossos widgets.
Conclusão
Agora descobrimos como acessar strings localizadas em nosso aplicativo:
- Dentro dos widgets → obtenha o objeto
AppLocalizations
doBuildContext
- Fora dos widgets → obtenha o objeto
AppLocalizations
doappLocalizationsProvider
De qualquer forma, nossos widgets e provedores serão reconstruídos se a localidade mudar.
Para obter instruções sobre como testar a alteração de localidade no iOS e Android, consulte este projeto de exemplo no GitHub:
E fique à vontade para reutilizar esse código em seus aplicativos, se desejar:
import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:ui' as ui; /// provedor usado para acessar o objeto AppLocalizations para a localidade atual final appLocalizationsProvider = Provider<AppLocalizations>((ref) { // 1. inicializar a partir do local inicial ref.state = lookupAppLocalizations(ui.window.locale); // 2. crie um observador para atualizar o estado final observer = _LocaleObserver((locales) { ref.state = lookupAppLocalizations(ui.window.locale); }); // 3. registrar o observador e descartá-lo quando não for mais necessário final binding = WidgetsBinding.instance; binding.addObserver(observer); ref.onDispose(() => binding.removeObserver(observer)); // 4. devolver o estado return ref.state; }); /// observador usado para notificar o chamador quando a localidade for alteradaes class _LocaleObserver extends WidgetsBindingObserver { _LocaleObserver(this._didChangeLocales); final void Function(List<Locale>? locales) _didChangeLocales; @override void didChangeLocales(List<Locale>? locales) { _didChangeLocales(locales); } }