[Flutter] l10nの操作をRiverpodのProviderで行う

エキサイトの武藤です。

Flutterで多言語対応をする際、基本的にはWidget内でBuildContextからAppLocalizationsを呼び出して利用します。

しかし、複雑なロジックが必要な表示テキストの場合、Viewでそのロジックを書いてしまうとViewのコードが肥大化してしまいます。

その場合、View以外に表示テキストの生成処理を行うようになると思います。

今回は、RiverpodのProviderで表示テキストの生成を行う方法について説明します。

実行環境

以下、実行環境です。

  • flutter: 3.3.8
  • dart : 2.18.4

pubspec.yml

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: "^0.17.0"
  hooks_riverpod: ^2.1.1
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.3.2
  flutter_hooks: ^0.18.5+1
  flutter_lints: ^2.0.0
flutter:
  uses-material-design: true
  generate: true

l10nを使ったテキストの表示

まずはシンプルな実装例です。

今回は日本語のみの対応とします。

app_ja.arbです。

{
  "@@locale": "ja",
  "sampleText": "サンプルテキストです。"
}

Viewのコードです。

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final appLocalizations = AppLocalizations.of(context)!;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(appLocalizations.sampleText),
          ],
        ),
      ),
    );
  }
}

appLocalizations.sampleText で該当のテキストを取得しています。

簡単なテキスト表示

ロジックを含む表示テキスト

次に、ロジックを含むのテキスト表示です。

app_ja.arbです。

{
  "@@locale": "ja",
  "taskTitle": "タスク名 : {title}",
  "@taskTitle": {
    "placeholders": {
      "title": {
        "type": "String"
      }
    }
  },
  "taskTitleWithMemo": "タスク名 : {title} メモ : {memo}",
  "@taskTitleWithMemo": {
    "placeholders": {
      "title": {
        "type": "String"
      },
      "memo": {
        "type": "String"
      }
    }
  }
}

簡単な例ですが、memo データのありなしで分岐することとします。

次に、ViewとProviderのコードです。

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(ref.watch(taskProvider(TaskProviderParam(
              title: 'ゴミ捨て',
              memo: 'ペットボトル、缶',
              context: context,
            )))),
          ],
        ),
      ),
    );
  }
}
class TaskProviderParam {
  TaskProviderParam({
    required this.title,
    required this.memo,
    required this.context,
  });

  final String title;

  final String memo;

  final BuildContext context;
}
final taskProvider = Provider.family<String, TaskProviderParam>((ref, param) {
  final context = param.context;
  final appLocalizations = AppLocalizations.of(context)!;
  if (param.title.isNotEmpty && param.memo.isNotEmpty) {
    return appLocalizations.taskTitleWithMemo(param.title, param.memo);
  }

  if (param.title.isNotEmpty) {
    return appLocalizations.taskTitle(param.title);
  }

  return '';
});

ロジックはviewに書くのを避けて、Providerに記述しました。

Providerには、表示の判定に使うデータとAppLocalizationsを生成するためのBuildContextを渡す必要があります。 Providerのfamily修飾子では引数を1つしか渡せないので、それらをまとめたParamクラスを作成しました。

viewでProviderを呼び出して、表示テキストを取得します。

ロジックを含む表示テキスト

l10nを取得するBuildContext を扱うユニットテスト

前項では、Providerにl10nを扱う実装を紹介しました。

今度は、そのProviderのユニットテストを書いてみます。

Future<BuildContext> getContext(WidgetTester tester) async {
  late BuildContext result;
  await tester.pumpWidget(
    MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: Material(
        child: Builder(
          builder: (BuildContext context) {
            result = context;
            return Container();
          },
        ),
      ),
    ),
  );
  return result;
}

void main() {
  final container = ProviderContainer();

  testWidgets('タスク名を表示', (WidgetTester tester) async {
    await tester.runAsync(() async {
      await getContext(tester).then((value) {
        final param = TaskProviderParam(
          title: "ゴミ捨て",
          memo: "",
          context: value
        );
        expect(
          container.read(taskProvider(param)),
          "タスク名 : ゴミ捨て",
        );
      });
    });
  });

  testWidgets('メモ付きのタスク名を表示', (WidgetTester tester) async {
    await tester.runAsync(() async {
      await getContext(tester).then((value) {
        final param = TaskProviderParam(
            title: "ゴミ捨て",
            memo: "ペットボトル、缶",
            context: value
        );
        expect(
          container.read(taskProvider(param)),
          "タスク名 : ゴミ捨て メモ : ペットボトル、缶",
        );
      });
    });
  });
}

l10nの設定を取得するために、context取得をメソッドに切り出しています。

実際のtest実行時には、testWidget()のWidgetTesterをgetContext()メソッドに渡すことで、l10nを設定されたcontextを取り出しています。

まとめ

Flutterにて、l10nの操作を含むロジックをProviderで行ってみました。

Viewが煩雑にならないために、ある程度複雑度が高くなった場合には有用かと思います。

どなたかの参考になれば幸いです。

参考記事

riverpod.dev

stackoverflow.com