概要
(見た目一緒なので画像は使いまわし)
ちょっと前にflutter_i18nというパッケージでの多言語対応を紹介したのですが、やっぱりもうちょっと標準的な方法でどうにかならないかと思ったので、flutter_localizationsベースでアプリ内で言語を切り替える方法を調べました。
状態管理は以下の3パターンでそれぞれやってみます。
- StatefulWidget
- provider 6.0.1
- Riverpod 1.0.0 + Flutter Hooks 0.18.0
flutter_localizationsの基本的な使い方については、各所に記事があるのでそれを参照してください。
以下は AppLocalizations
クラスが既に生成されている前提で話を進めます。
arbの内容はこんな感じです。
{
"@@locale":"en",
"languageSettings": "Language Settings",
"hello": "Hello"
}
{
"@@locale":"ja",
"languageSettings": "言語設定",
"hello": "こんにちは"
}
StatefulWidget版
とりあえずサンプルアプリ全体。
localeResolutionCallbackまわりは割愛してます。
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Locale locale = AppLocalizations.supportedLocales.first;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HelloPage(),
);
}
}
class HelloPage extends StatefulWidget {
const HelloPage({Key? key}) : super(key: key);
@override
_HelloPageState createState() => _HelloPageState();
}
class _HelloPageState extends State<HelloPage> {
SimpleDialogOption _changeLocaleDialogOption(
BuildContext context,
String text,
String languageCode) {
return SimpleDialogOption(
child: Text(text),
onPressed: () {
context.findAncestorStateOfType<_MyAppState>()!
..locale = Locale(languageCode, '')
..setState(() {});
Navigator.pop(context);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.hello)
),
body: Center(
child: TextButton(
child: Text(AppLocalizations.of(context)!.languageSettings),
onPressed: () =>
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(AppLocalizations.of(context)!.languageSettings),
children: <Widget>[
_changeLocaleDialogOption(context, 'English', 'en'),
_changeLocaleDialogOption(context, '日本語', 'ja'),
],
);
},
),
),
),
);
}
}
重要なのは
onPressed: () {
context.findAncestorStateOfType<_MyAppState>()!
..locale = Locale(languageCode, '')
..setState(() {});
Navigator.pop(context);
}
ここら辺で、上位Widgetのlocale
プロパティとsetState
を呼び出して全体の再描画を行っています。が、このやりかたでよいのか正直わからないです。
でも仕方ないのかなあ?
provider版
とりあえずサンプルアプリ全体。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LocaleState()),
],
child: const MyApp(),
),
);
}
class LocaleState with ChangeNotifier {
Locale _locale = AppLocalizations.supportedLocales.first;
Locale get locale => _locale;
set locale(Locale locale) {
_locale = locale;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: context.watch<LocaleState>().locale,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HelloPage(),
);
}
}
class HelloPage extends StatelessWidget {
const HelloPage({Key? key}) : super(key: key);
SimpleDialogOption _changeLocaleDialogOption(
BuildContext context,
String text,
String languageCode) {
return SimpleDialogOption(
child: Text(text),
onPressed: () {
context.read<LocaleState>().locale = Locale(languageCode, '');
Navigator.pop(context);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.hello)
),
body: Center(
child: TextButton(
child: Text(AppLocalizations.of(context)!.languageSettings),
onPressed: () =>
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(AppLocalizations.of(context)!.languageSettings),
children: <Widget>[
_changeLocaleDialogOption(context, 'English', 'en'),
_changeLocaleDialogOption(context, '日本語', 'ja'),
],
);
},
),
),
),
);
}
}
StatefulWidget版と基本的な構造は一緒ですが、localeをプロパティでなくプロバイダから取るためにChangeNotifier
をミックスインしたクラスを作っています。
重要なのは
locale: context.watch<LocaleState>().locale,
ここと
onPressed: () {
context.read<LocaleState>().locale = Locale(languageCode, '');
Navigator.pop(context);
},
ここら辺で、setState
のように能動的に変更を通知するのではなく、プロバイダを監視させることで自動的に変更が通知されます。
(onPressed
でLocaleState.locale
セッターを呼んでいますが、この中のnotifyListeners()
によって通知されるという流れです。)
StatefulWidget版よりはこっちの方がよさそうですかね。
Riverpod + Flutter Hooks版
とりあえずサンプルアプリ全体。
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
final localeProvider = StateProvider<Locale>(
(ref) => AppLocalizations.supportedLocales.first);
class MyApp extends HookConsumerWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: ref.watch(localeProvider),
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HelloPage(),
);
}
}
class HelloPage extends HookConsumerWidget {
const HelloPage({Key? key}) : super(key: key);
SimpleDialogOption _changeLocaleDialogOption(
BuildContext context,
StateController<Locale> localeController,
String text,
String languageCode) {
return SimpleDialogOption(
child: Text(text),
onPressed: () {
localeController.state = Locale(languageCode, '');
Navigator.pop(context);
},
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
StateController<Locale> localeController = ref.watch(localeProvider.state);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.hello)
),
body: Center(
child: TextButton(
child: Text(AppLocalizations.of(context)!.languageSettings),
onPressed: () =>
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text(AppLocalizations.of(context)!.languageSettings),
children: <Widget>[
_changeLocaleDialogOption(context, localeController, 'English', 'en'),
_changeLocaleDialogOption(context, localeController, '日本語', 'ja'),
],
);
},
),
),
),
);
}
}
providerでのChangeNotifier.notifyListeners()
のような明示的な変更通知はなく、代わりにStateProvider
を使うことで自動的に変更が通知されます。
細かい構文の違いはありますが、他は大体providerと一緒です。
これがぱっと見一番よさそうな感じがしますが、Riverpod自体がまだ枯れてなさそうなのが難点かなあ。