easy_localizationの罠
現在、個人開発でeasy_localization[1]を使って多言語対応を実装しています。
多くの開発者に利用されているので、信頼できるライブラリではないかと思いつつ、導入し始めました。
しかし、基盤を作って動作確認をしようとしているところ、意外なことが起こってしまいました。
発生事象
class ParentWidget extends StatelessWidget {
const ParentWidget({super.key});
Widget build(BuildContext context) {
return Column(
children: [
const Text('hello').tr(),
// 内部にText('hello').tr()がある
const ChildWidgetA(),
// 内部にText('hello').tr()がある
const ChildWidgetB(),
],
);
}
}
上記のサンプルコードのように、親Widgetと子Widgetが存在するという構造の画面を作ってます。
そして、ChildWidgetB
の中に言語を変換する処理context.setLocale(ja)
を実行すると、
本来であれば、Widgetの種類を問わず、全体のhello
がこんにちは
になるはずですが、
実際には、ChildWidgetB
のテキストがこんにちは
に変わっただけで、それ以外のところはhello
のままになっています。
それはなぜでしょうか?
結論から申し上げますと、context.setLocale(ja)
を呼ぶときのcontext
が原因だと考えられます。
それはなぜでしょうか?
まずはsetLocale()
のソースコードを見てみましょう。
Future<void> setLocale(Locale l) async {
_locale = l;
await loadTranslations();
notifyListeners();
EasyLocalization.logger('Locale $locale changed');
await _saveLocale(_locale);
}
上から順に説明していきます。
-
変更しようとしている
locale
をメモリに保存 -
locale
の値をもとに翻訳データを取得 -
notifyListeners()
を実行してUIを更新 -
デバッグログを出力
-
設定がリセットされないよう、
locale
の値をローカルに保存
パッと見た感じでは、すごく単純なことで特に問題なさそうですね…
でもなぜこういう挙動になっているのでしょうか?
easy_localizationのロジックを見極める
easy_localization
を導入する際に、以下のようにアプリをEasyLocalization
に囲んでいます。
return EasyLocalization(
child: const MyApp(),
);
ソースコードを見れば分かると思いますが、EasyLocalization
実はStatefulWidget
です。
アプリを起動すると、initState()
の一つの処理として、
EasyLocalizationController(InheritedWidget)
のインスタンスを作成して、
addListener()
で状態変化を監視するようにしています。
上記のcontext.setLocale(ja)
の処理の正体は、
-
context
からEasyLocalizationController
のインスタンスを取得 -
諸々処理をした後、
notifyListeners()
を実行してListenerに通知 -
状態変化を検知できたら、
setState()
を実行してUIを更新
ということになります。
しかし、EasyLocalizationController
のインスタンスはcontext
から取得したものなので、
setState()
を呼んだとしても、更新されたのはcontext
が所属しているWidgetのみで、親Widgetでは何も起こらないです。
そのため、setLocale()
を呼ぶときのcontext
は親Widget(上記の例でいうとParentWidget
)のものでなければなりません。
さっそく直してみましょう
class ParentWidget extends StatelessWidget {
const ParentWidget({super.key});
Widget build(BuildContext context) {
return Column(
children: [
const Text('hello').tr(),
// 内部にText('hello').tr()がある
const ChildWidgetA(),
// 内部にText('hello').tr()がある
ChildWidgetB(context),
],
);
}
}
ParentWidget
のcontext
を引数としてChildWidgetB
に渡してsetLocale()
を呼びますと、
ParentWidget
、ChildWidgetA
、ChildWidgetB
のテキストが全部こんにちは
になってます!
おめでとうございます!
でも、これで本当に直したと言えるのでしょうか?
新たな課題
言語を変えるとアプリ全体のテキストが一緒に変わるのが一般的だと思います。
つまり、setLocale()
を呼ぶときのcontext
はアプリそのもののcontext
でないといけません。
アプリのcontext
をどこかのボタンに渡すって、ちょっと変な実装ですね…
context
以外に何か良い方法はないのか?
contextのことを考えなくてもOKの方法
以下のように、context.setLocale(ja)
を呼んだあと、
WidgetsFlutterBinding.ensureInitialized().performReassemble()
も呼べばいいです。
await context.setLocale(locale);
await WidgetsFlutterBinding.ensureInitialized().performReassemble();
reassembleApplicationは、Flutter公式から提供されているメソッドです。
一回呼ぶと、ホットリロードみたいな感じでアプリ全体のUIが更新されます。
処理コストは高いので使うのが推奨されていないのですが、言語変更みたいな実行頻度が極めて低い処理であれば問題ないでしょう。
他の多言語対応系のライブラリのソースコードを見たところ、このメソッドを利用しているものもいくつかあります。
おまけ
上記以外の解決方法も存在するのでおまけに追記します。
const Text('hello').tr(context: context)
このように、.tr()
を呼ぶたびにcontext
を渡すようにすると、言語の変化を検知して即時反映できます。
毎回やるのは手間がかかるのですが、アプリ全体のリビルドに気になる方はこちらをご利用いただければと思います。
最後に
とりあえず、一行だけでこの問題を解決できました。
この結果へ辿り着くため、いろんなライブラリのソースコードを見たり、資料を読んだりして疲れたけど、楽しかったです。
FlutterのUI周りは単純に見えるがなかなか深いので、これからも勉強を続けて行きたいと思います。
-
easy_localization: 3.0.2 ↩︎
Discussion