[Flutter] flutter_localizationsによる多言語化対応（アプリ内で言語切り替え）

# 概要
![スクリーンショット 2021-10-04 18.14.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141950/dfff3bf0-e20a-0c46-1558-7454406931d2.png)

（見た目一緒なので画像は使いまわし）
ちょっと前に[flutter_i18nというパッケージでの多言語対応を紹介した](https://qiita.com/satoshii/items/3d882552917e1d8346f1)のですが、やっぱりもうちょっと標準的な方法でどうにかならないかと思ったので、[flutter_localizations](https://flutter.dev/docs/development/accessibility-and-localization/internationalization)ベースでアプリ内で言語を切り替える方法を調べました。
状態管理は以下の3パターンでそれぞれやってみます。

- StatefulWidget
- [provider](https://pub.dev/packages/provider) 6.0.1
- [Riverpod](https://riverpod.dev/) 1.0.0 + [Flutter Hooks](https://pub.dev/packages/flutter_hooks) 0.18.0

flutter_localizationsの基本的な使い方については、各所に記事があるのでそれを参照してください。
以下は `AppLocalizations` クラスが既に生成されている前提で話を進めます。

arbの内容はこんな感じです。

```json:app_en.arb
{
  "@@locale":"en",
  "languageSettings": "Language Settings",
  "hello": "Hello"
}
```

```json:app_ja.arb
{
  "@@locale":"ja",
  "languageSettings": "言語設定",
  "hello": "こんにちは"
}
```


# StatefulWidget版

とりあえずサンプルアプリ全体。
localeResolutionCallbackまわりは割愛してます。

```dart:main.dart
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'),
                    ],
                  );
                },
              ),
        ),
      ),
    );
  }
}
```

重要なのは

```dart
onPressed: () {
  context.findAncestorStateOfType<_MyAppState>()!
    ..locale = Locale(languageCode, '')
    ..setState(() {});
  Navigator.pop(context);
}
```

ここら辺で、上位Widgetの`locale`プロパティと`setState`を呼び出して全体の再描画を行っています。が、このやりかたでよいのか正直わからないです。
でも仕方ないのかなあ？


# provider版

とりあえずサンプルアプリ全体。

```dart:main.dart
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`をミックスインしたクラスを作っています。

重要なのは

```dart
locale: context.watch<LocaleState>().locale,
```

ここと

```dart
onPressed: () {
  context.read<LocaleState>().locale = Locale(languageCode, '');
  Navigator.pop(context);
},
```

ここら辺で、`setState`のように能動的に変更を通知するのではなく、プロバイダを監視させることで自動的に変更が通知されます。
（`onPressed`で`LocaleState.locale`セッターを呼んでいますが、この中の`notifyListeners()`によって通知されるという流れです。）
StatefulWidget版よりはこっちの方がよさそうですかね。


# Riverpod + Flutter Hooks版

とりあえずサンプルアプリ全体。

```dart:main.dart
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自体がまだ枯れてなさそうなのが難点かなあ。