FlutterのLocalizationについできたこと~CupertinoDatePickerを日本語にできた~

こんにちは。VyseArtのトナカイです。

最近Flutterを学んでいます。
私はIT関連企業の事務系で社内ツールを作っているのですが、
今後、今の仕事でもFlutterつかえたらいいなという淡い期待をしていて、
ちょっとずつ勉強しています。

あとFlutterを使ってどうしても作ってみたいツールがあるんです。
SNS要素があるのですが、使ってくれた人が希望を持てるようなツールを作りたいと思っています!




今回は下記のような実装ができました。
Cupertino(iOS風)のウィジェットってかっこいいですよね^^

f:id:odekakeneko:20200113161105p:plain:w300
CupertinoDatePickerを日本語化

実装にあたり、もっと簡単な方法があるんじゃないだろうかと迷ったり、私的につまずいた部分があったので、
備忘録もかねて記事を書いていこうと思います。

Localizationしたくなった理由

そのように思った発端ですが、CupertinoDatePickerを使いたいと思ったら、英語表記だったことです。

同じ質問がstack over flowにありました。

stackoverflow.com

この質問の解決方法を真似て日本語版のスクリプトを作ったところ、上記の画像のように日本語化することができました。

Localizationてどうやるの?

pubspec.yamlに下記を記述し、ターミナルでflutter pub getします。

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  flutter_cupertino_localizations: ^1.0.1

次に、下記をインポートし、localizationsDelegates と supportedLocalesを記述します。
↓main.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(


      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        DefaultCupertinoLocalizations.delegate,
      ],

      supportedLocales: [

        const Locale('en', 'US'),
        const Locale('ja', 'JP'),
      ],
      locale:  Locale('ja', 'JP'),


次に、stack over flowの質問回答記事にあるように、Localizeしたい国の言語(自国語)のスクリプトを作っていきます。


↓JapaneseCupertinoLocalizations.dart


import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

class _CupertinoLocalizationDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
  const _CupertinoLocalizationDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'ja';

  @override
  Future<CupertinoLocalizations> load(Locale locale)=> JapaneseCupertinoLocalizations.load(locale);

  @override
  bool shouldReload( _CupertinoLocalizationDelegate old) => false;

  @override
  String toString() => 'DefaultCupertinoLocalizations.delegate(ja_JP)';
}

class JapaneseCupertinoLocalizations implements CupertinoLocalizations{

  const JapaneseCupertinoLocalizations();

  static const List<String> _shortWeekdays = <String>[
    '(月)',
    '(火)',
    '(水)',
    '(木)',
    '(金)',
    '(土)',
    '(日)',
  ];

  static const List<String> _shortMonths = <String> [
    '1月',
    '2月',
    '3月',
    '4月',
    '5月',
    '6月',
    '7月',
    '8月',
    '9月',
    '10月',
    '11月',
    '12月',
  ];

  static const List<String> _months = <String>[
    '1月',
    '2月',
    '3月',
    '4月',
    '5月',
    '6月',
    '7月',
    '8月',
    '9月',
    '10月',
    '11月',
    '12月',
  ];

  @override
  String datePickerYear(int yearIndex) => yearIndex.toString();

  @override
  String datePickerMonth(int monthIndex) => _months[monthIndex -1];

  @override
  String datePickerDayOfMonth(int dayIndex) => dayIndex.toString();

  @override
  String datePickerHour(int hour) => hour.toString();

  @override
  String datePickerHourSemanticsLabel( int hour) => hour.toString() + "時";

  @override
  String datePickerMinute(int minute) => minute.toString().padLeft(2, '0');

  @override
  String datePickerMinuteSemanticsLabel( int minute) {
    if(minute == 1)
      return '1 分';
    return minute.toString() + '分';
  }

  @override
  String datePickerMediumDate(DateTime date) {
    return
        '${_shortMonths[date.month - DateTime.january]} '
        '${date.day.toString().padRight(2)+ '日'}'
        '${_shortWeekdays[date.weekday - DateTime.monday] } ';
  }

  @override
  DatePickerDateOrder get datePickerDateOrder => DatePickerDateOrder.mdy;

  @override
  DatePickerDateTimeOrder get datePickerDateTimeOrder => DatePickerDateTimeOrder.date_time_dayPeriod;

  @override
  String get anteMeridiemAbbreviation => '午前';

  @override
  String get postMeridiemAbbreviation => '午後';

  @override
  String get alertDialogLabel => 'Info';

  @override
  String timerPickerHour(int hour ) => hour.toString();

  @override
  String timerPickerMinute(int minute) => minute.toString();

  @override
  String timerPickerSecond(int second) => second.toString();

  @override
  String timerPickerHourLabel(int hour)  => hour == 1 ? '時' : '時'; //参考の書式のまま

  @override
  String timerPickerMinuteLabel(int minute) => '分';

  @override
  String timerPickerSecondLabel(int second) => '秒';

  @override
  String get cutButtonLabel => 'カット';

  @override
  String get copyButtonLabel => 'コピー';

  @override
  String get pasteButtonLabel => 'ペースト';

  @override
  String get selectAllButtonLabel => '選択';

  @override
  String get todayLabel => '今日';  //参考サイトにはなかったが、参照しないとエラーが出るので独自で追加
  /// Creates an object that provides US English resource values for the
  /// cupertino library widgets.
  ///
  /// The [locale] parameter is ignored.
  ///
  /// This method is typically used to create a [LocalizationsDelegate].
  /// 
  static Future<CupertinoLocalizations> load(Locale locale) {
    return SynchronousFuture<CupertinoLocalizations>(const JapaneseCupertinoLocalizations());
  }

  /// A [LocalizationsDelegate] that uses [DefaultCupertinoLocalizations.load]
  /// to create an instance of this class.
  static const LocalizationsDelegate<CupertinoLocalizations> delegate = _CupertinoLocalizationDelegate();
}

このJapaneseCupertinoLocalizations.dartを先ほどのmain.dartにインポートします。

import 'JapaneseCupertinoLocalizations.dart' as jcl;  //(ほかのライブラリと競合したのでas jclとしている)

そして、先ほどのlocalizationsDelegatesにこのライブラリのデリゲートを追加します。

      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        DefaultCupertinoLocalizations.delegate,
        jcl.JapaneseCupertinoLocalizations.delegate,
      ],

以上でCupertinoDatePickerを呼び出したときにLocalizeできているはずです。

CupertinoDatePickerの呼び出し方

基礎から学ぶFlutter 石井幸次著より引用

Widget _buildBottomPicker(Widget picker) {

  return Container(
    height:216,
    padding:const EdgeInsets.only(top:6.0),
    color: CupertinoColors.white,
    child:DefaultTextStyle(
      style:const TextStyle(
        color:CupertinoColors.black,
        fontSize:22.0,
      ),
      child: GestureDetector(
        onTap:(){},
        child:SafeArea(
            top:false,
            child: picker,
        )
      )
    ),
  );
}

_buildBottomPickerという関数を作ります。Containerでラップして、GestureDetectorはタップ処理を入れたいときに使うのでしょうか。その子にSafeAreaでラップしてウィジェットがはみ出ないようにしています。

引数のpickerが一番下階層の子になりそれが表示される仕組みです。

                    CupertinoButton(
                      child:Text('来所時間'),
                      onPressed: () async{
                        await showCupertinoModalPopup<void>(
                          context: context,
                          builder: (BuildContext context) {
                            return _buildBottomPicker(
                              CupertinoDatePicker(
                                mode: CupertinoDatePickerMode.dateAndTime,
                                initialDateTime: _date,
                                onDateTimeChanged: (DateTime newDateTime) {
                                  setState(() {
                                    _date = newDateTime;
                                  });
                                },
                              ),
                            );
                          },
                        );
                        debugPrint('閉じました');
                      },
                    ),

実際にこの_buildBottomPickerを使うときは下記のようにButtonのonPressedプロパティに、
showCupertinoModalPopup関数を入れ(てawaitさせ)ました。
showCupertinoModalPopupが返すウィジェットに_buildBottomPickerを設定して、
_buildBottomPickerの引数pikerにCupertinoDatePickerを設定しています。
こうすればポップアップが開かれ、CupertinoDatePickerのホイールが回り、
ポップアップが閉じられたとき
debugPrint('閉じました');
のログが残ります。

awaitの使い方がこれでいいのかなど、
初心者ながら自身薄ですが、
こういう風にしてFlutterの使えるコードを増やし、
成果物を出すことによって自信をつけていこうと思います。
以上。