如何使用Riverpod读取Widgets外的本地化字符串

389 阅读6分钟

如果您想将您的Flutter应用程序部署给讲其他语言的用户,您需要对其进行本地化。而Flutter文档中已经有一个详细的国际化指南,深入地涵盖了这个主题。

为了使生活更轻松,我之前写了一份分步指南,解释了如何生成AppLocalizations 类,并使用BuildContext 扩展在我们的小部件内访问它。

但是,如果我们有一些业务逻辑是在小部件之外的,那么我们如何才能读取本地化的字符串?

本文展示了我是如何在自己的应用程序中使用Riverpod包解决本地化问题的。

本文是基于官方的flutter_localization 包,它支持超过78种语言。Flutter Gems网站在此列出了许多其他的本地化包。

[

赞助者

Code with Andrea对每个人都是免费的。帮助我保持这种方式,看看这个赞助商。

Open-Source Backend Server for Flutter Developers

**面向Flutter开发者的开源后端服务器。**Appwrite是一个安全的、自我托管的解决方案,它为开发者提供了一套易于使用的REST API来管理他们的核心后台需求。你可以用Appwrite构建任何东西点击这里了解更多。

](appwrite.io/?utm_source…)

应用程序的本地化要求

在我们深入研究代码之前,让我们先弄清楚我们要做什么。

  • 在我们的小部件之外访问本地化的字符串**(不**使用BuildContext)
  • 确保任何监听器(提供者和小部件)在本地化发生变化时重新构建

为了满足这些要求,我们需要结合两件事。

  • 一个Provider<AppLocalizations> ,我们可以在应用程序的任何地方使用它
  • 一个LocaleObserver 类,我们可以用它来跟踪locale的变化。

下面是如何实现它们的。👇

1.创建AppLocalizations提供者

为了得到适合当前locale的AppLocalizations 对象,我们可以使用lookupAppLocalizations 方法,这个方法是在我们运行flutter gen-l10n 命令时产生的。

所以第一步是创建一个使用它的提供者。

import 'package:flutter_riverpod/flutter_riverpod.dart';
// lookupAppLocalizations is defined here 👇
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 
import 'dart:ui' as ui;

/// provider used to access the AppLocalizations object for the current locale
final appLocalizationsProvider = Provider<AppLocalizations>((ref) {
  return lookupAppLocalizations(ui.window.locale);
});

但这还不够,因为我们的提供者不会在locale发生变化时重建并通知其监听器

2.添加一个locale观察者

为了跟踪地区性变化,我们可以创建一个子类的 WidgetsBindingObserver的子类,并重写 didChangeLocales方法。

/// observer used to notify the caller when the locale changes
class _LocaleObserver extends WidgetsBindingObserver {
  _LocaleObserver(this._didChangeLocales);
  final void Function(List<Locale>? locales) _didChangeLocales;
  @override
  void didChangeLocales(List<Locale>? locales) {
    _didChangeLocales(locales);
  }
}

现在我们有了这个,我们可以更新我们的提供者代码。

/// provider used to access the AppLocalizations object for the current locale
final appLocalizationsProvider = Provider<AppLocalizations>((ref) {
  // 1. initialize from the initial locale
  ref.state = lookupAppLocalizations(ui.window.locale);
  // 2. create an observer to update the state
  final observer = _LocaleObserver((locales) {
    ref.state = lookupAppLocalizations(ui.window.locale);
  });
  // 3. register the observer and dispose it when no longer needed
  final binding = WidgetsBinding.instance;
  binding.addObserver(observer);
  ref.onDispose(() => binding.removeObserver(observer));
  // 4. return the state
  return ref.state;
});

这可以确保任何其他依赖appLocalizationsProvider 的提供者或小工具在locale发生变化时可以重建。

接下来,让我们看看如何使用我们的提供者。

3.使用AppLocalizations提供者

主要的想法是,我们可以使用一个ref 对象来读取 appLocalizationsProvider

// get the AppLocalizations object (read once)
final loc = ref.read(appLocalizationsProvider);
// read a property defined in the *.arb file
final error = loc.addToCartFailed; 

例如,考虑这个CartService 类,它被用来更新一个购物车。

class CartService {
  CartService(this.ref);
  // declare ref as a property
  final Ref ref;

  Future<void> addItem(Item item) async {
    try {
      // fetch the cart
      final cart = ref.read(cartRepositoryProvider).fetchCart();
      // return a copy with the updated data
      final updated = cart.addItem(item);
      // set the cart with the updated data
      await ref.read(cartRepositoryProvider).setCart(updated);
    } catch (e) {
      // get the localized error message
      final errorMessage = ref.read(appLocalizationsProvider).addToCartFailed;
      // throw it as an exception
      throw Exception(errorMessage);
    }
  }
}

// the corresponding provider
final cartServiceProvider = Provider<CartService>((ref) {
  return CartService(ref);
});

这个CartService 类需要一个Ref 参数,它被用来在catch块内调用ref.read(appLocalizationsProvider) - 从而使AppLocalizations 成为一个隐含的依赖关系。

在这种情况下,我们应该读取(而不是观察)appLocalizationsProvider ,因为我们需要在调用addItem 方法时检索一次错误信息。

但是如果我们想让AppLocalizations 的依赖关系更加明确,我们可以这样做。

class CartService {
  CartService({required this.cartRepository, required this.loc});
  // declare dependencies as explicit properties
  final CartRepository cartRepository;
  final AppLocalizations loc;

  Future<void> addItem(Item item) async {
    try {
      // fetch the cart
      final cart = cartRepository.fetchCart();
      // return a copy with the updated data
      final updated = cart.addItem(item);
      // set the cart with the updated data
      await cartRepository.setCart(updated);
    } catch (e) {
      // get the localized error message
      final errorMessage = loc.addToCartFailed;
      // throw it as an exception
      throw Exception(errorMessage);
    }
  }
}

// the corresponding provider
final cartServiceProvider = Provider<CartService>((ref) {
  return CartService(
    // pass the dependencies explicitly (using watch)
    cartRepository: ref.watch(cartRepositoryProvider),
    loc: ref.watch(appLocalizationsProvider),
  );
});

在这种情况下,我们应该监视(而不是读取)appLocalizationsProvider ,以便在locale改变时重建cartServiceProvider 返回的CartService

无论如何,我们现在可以在我们的提供者中的任何地方访问AppLocalizations ,而不需要使用BuildContext

奖励:使用AppLocalizations进行错误处理

如果我们使用freezed,我们可以定义我们自己的特定领域的异常类。

@freezed
class AppException with _$AppException {
  const factory AppException.permissionDenied() = PermissionDenied;
  const factory AppException.paymentFailed() = PaymentFailed;
  // add other error types here
}

而如果我们想把每个错误映射到一个本地化的错误信息(我们将在用户界面中显示),我们可以创建这个简单的扩展。

extension AppExceptionMessage on AppException {
  String message(AppLocalizations loc) {
    return when(
      permissionDenied: () => loc.permissionDeniedMessage,
      paymentFailed: () => loc.paymentFailedMessage,
      // and so on...
    );
  }
}

然后,我们可以在一个StateNotifier<AsyncValue> 子类中设置一个带有本地化字符串的错误状态,我们的小部件可以监听到。

// inside a StateNotifier subclass
final exception = AppException.permissionDenied();
final loc = ref.read(appLocalizationsProvider);
state = AsyncError(exception.message(loc));

再一次,不需要BuildContext ,就可以在我们的小部件之外读取本地化的字符串。

总结

我们现在已经知道了如何在我们的应用程序中访问本地化的字符串。

  • 在widgets内部→ 从BuildContext 获取AppLocalizations 对象(如前文所述)
  • 在widgets之外→从appLocalizationsProvider (这里解释过)获得AppLocalizations 对象。

无论哪种方式,如果地区设置发生变化,我们的小部件和提供者都会重新建立

关于如何在iOS和Android上测试locale变化的说明,请参阅GitHub上的这个例子项目。

如果你愿意,可以在你的应用程序中重新使用这些代码。

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;

/// provider used to access the AppLocalizations object for the current locale
final appLocalizationsProvider = Provider<AppLocalizations>((ref) {
  // 1. initialize from the initial locale
  ref.state = lookupAppLocalizations(ui.window.locale);
  // 2. create an observer to update the state
  final observer = _LocaleObserver((locales) {
    ref.state = lookupAppLocalizations(ui.window.locale);
  });
  // 3. register the observer and dispose it when no longer needed
  final binding = WidgetsBinding.instance;
  binding.addObserver(observer);
  ref.onDispose(() => binding.removeObserver(observer));
  // 4. return the state
  return ref.state;
});

/// observer used to notify the caller when the locale changes
class _LocaleObserver extends WidgetsBindingObserver {
  _LocaleObserver(this._didChangeLocales);
  final void Function(List<Locale>? locales) _didChangeLocales;
  @override
  void didChangeLocales(List<Locale>? locales) {
    _didChangeLocales(locales);
  }
}

编码愉快!