Zenn
Open16

initializeDateFormattingを紐解いてみる

ピン留めされたアイテム
ashdikashdik

そもそものきっかけはある程度Flutterで開発したことある人は一度は見たことあるであろう

Unsupported operation: Cannot modify unmodifiable map

への対処のため

ashdikashdik

まとめ

僕の場合だと device_previewパッケージとの兼ね合いで起きていました。

どのようなコードで起きているか

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  ...
  await initializeDateFormatting('ja_JP');

  runApp(DevicePreview(enabled: kDebugMode, builder: (_) => MyApp()));
}

なぜ起きていたか

  • まず、 initializeDateFormattingdateTimeSymbolsdateTimePatterns が初期化される

https://zenn.dev/link/comments/94ff3261b5a332
https://zenn.dev/link/comments/ca2d0514e5f280

  • その後、 DevicePreview (v1.0.0) 内 602行目で Localizations ウィジェットが生成される

そうすると、この辺の処理が行われ、再度
dateTimeSymbolsdateTimePatterns への代入処理が走り起こっていた

ashdikashdik

intl.date_symbol_data_local.dart

Future<void> initializeDateFormatting([String? locale, String? ignored]) {
  initializeDateSymbols(dateTimeSymbolMap);
  initializeDatePatterns(dateTimePatternMap);
  return new Future.value();
}
ashdikashdik

めちゃくちゃ長い...!

Map<dynamic, dynamic> dateTimeSymbolMap() => {
      // Date/time formatting symbols for locale en_ISO.
      "en_ISO": DateSymbols(
      ),
};
ashdikashdik

こちらも5000行くらい

Map<String, Map<String, String>> dateTimePatternMap() => const {
      /// Extended set of localized date/time patterns for locale af.
      'af': const {
        'd': 'd', // DAY
        'E': 'ccc', // ABBR_WEEKDAY
...
};
ashdikashdik

intl.date_format_internal.dart

void initializeDateSymbols(Function symbols) {
  if (dateTimeSymbols is UninitializedLocaleData<dynamic>) {
    dateTimeSymbols = symbols();
  }
}
ashdikashdik

int.date_format_internal.dart

void initializeDatePatterns(Function patterns) {
  if (dateTimePatterns is UninitializedLocaleData<dynamic>) {
    dateTimePatterns = patterns();
  }
}
ashdikashdik

どんなスタックで起きているのか

StatefulElement._firstBuild

  
  void _firstBuild() {
    assert(state._debugLifecycleState == _StateLifecycle.created);
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final Object? debugCheckForReturnedFuture = state.initState() as dynamic; // ←ここ
      assert(() {
        if (debugCheckForReturnedFuture is Future) {
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('${state.runtimeType}.initState() returned a Future.'),
            ErrorDescription('State.initState() must be a void method without an `async` keyword.'),
            ErrorHint(
              'Rather than awaiting on asynchronous work directly inside of initState, '
              'call a separate method to do this work without awaiting it.',
            ),
          ]);
        }
        return true;
      }());
    } finally {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
    }
    assert(() {
      state._debugLifecycleState = _StateLifecycle.initialized;
      return true;
    }());
    state.didChangeDependencies();
    assert(() {
      state._debugLifecycleState = _StateLifecycle.ready;
      return true;
    }());
    super._firstBuild();
  }
ashdikashdik

localizations.dart

class _LocalizationsState extends State<Localizations> {
  ...
  
  void initState() {
    super.initState();
    load(widget.locale); // ←ここ
  }
  ...
}
ashdikashdik

localizations.dart

  void load(Locale locale) {
    final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
    if (delegates == null || delegates.isEmpty) {
      _locale = locale;
      return;
    }

    Map<Type, dynamic>? typeToResources;
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates) // ←ここ
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
        return typeToResources = value;
      });

    if (typeToResources != null) {
      // All of the delegates' resources loaded synchronously.
      _typeToResources = typeToResources!;
      _locale = locale;
    } else {
      // - Don't rebuild the dependent widgets until the resources for the new locale
      // have finished loading. Until then the old locale will continue to be used.
      // - If we're running at app startup time then defer reporting the first
      // "useful" frame until after the async load has completed.
      RendererBinding.instance!.deferFirstFrame();
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
        if (mounted) {
          setState(() {
            _typeToResources = value;
            _locale = locale;
          });
        }
        RendererBinding.instance!.allowFirstFrame();
      });
    }
  }
ashdikashdik

localization.dart

Future<Map<Type, dynamic>> _loadAll(Locale locale, Iterable<LocalizationsDelegate<dynamic>> allDelegates) {
  final Map<Type, dynamic> output = <Type, dynamic>{};
  List<_Pending>? pendingList;

  // Only load the first delegate for each delegate type that supports
  // locale.languageCode.
  final Set<Type> types = <Type>{};
  final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[];
  for (final LocalizationsDelegate<dynamic> delegate in allDelegates) {
    if (!types.contains(delegate.type) && delegate.isSupported(locale)) {
      types.add(delegate.type);
      delegates.add(delegate);
    }
  }

  for (final LocalizationsDelegate<dynamic> delegate in delegates) {
    final Future<dynamic> inputValue = delegate.load(locale); // ←ここ
    dynamic completedValue;
    final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) {
      return completedValue = value;
    });
    if (completedValue != null) { // inputValue was a SynchronousFuture
      final Type type = delegate.type;
      assert(!output.containsKey(type));
      output[type] = completedValue;
    } else {
      pendingList ??= <_Pending>[];
      pendingList.add(_Pending(delegate, futureValue));
    }
  }

  // All of the delegate.load() values were synchronous futures, we're done.
  if (pendingList == null)
    return SynchronousFuture<Map<Type, dynamic>>(output);

  // Some of delegate.load() values were asynchronous futures. Wait for them.
  return Future.wait<dynamic>(pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue))
    .then<Map<Type, dynamic>>((List<dynamic> values) {
      assert(values.length == pendingList!.length);
      for (int i = 0; i < values.length; i += 1) {
        final Type type = pendingList![i].delegate.type;
        assert(!output.containsKey(type));
        output[type] = values[i];
      }
      return output;
    });
}
ashdikashdik

material_localizations.dart

class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
  ...
    
  Future<MaterialLocalizations> load(Locale locale) {
    assert(isSupported(locale));
    return _loadedTranslations.putIfAbsent(locale, () {
      util.loadDateIntlDataIfNotLoaded(); // ←ここ

      final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
      assert(
        locale.toString() == localeName,
        'Flutter does not support the non-standard locale form $locale (which '
        'might be $localeName',
      );

      intl.DateFormat fullYearFormat;
      intl.DateFormat compactDateFormat;
      intl.DateFormat shortDateFormat;
      intl.DateFormat mediumDateFormat;
      intl.DateFormat longDateFormat;
      intl.DateFormat yearMonthFormat;
      intl.DateFormat shortMonthDayFormat;
      if (intl.DateFormat.localeExists(localeName)) {
        fullYearFormat = intl.DateFormat.y(localeName);
        compactDateFormat = intl.DateFormat.yMd(localeName);
        shortDateFormat = intl.DateFormat.yMMMd(localeName);
        mediumDateFormat = intl.DateFormat.MMMEd(localeName);
        longDateFormat = intl.DateFormat.yMMMMEEEEd(localeName);
        yearMonthFormat = intl.DateFormat.yMMMM(localeName);
        shortMonthDayFormat = intl.DateFormat.MMMd(localeName);
      } else if (intl.DateFormat.localeExists(locale.languageCode)) {
        fullYearFormat = intl.DateFormat.y(locale.languageCode);
        compactDateFormat = intl.DateFormat.yMd(locale.languageCode);
        shortDateFormat = intl.DateFormat.yMMMd(locale.languageCode);
        mediumDateFormat = intl.DateFormat.MMMEd(locale.languageCode);
        longDateFormat = intl.DateFormat.yMMMMEEEEd(locale.languageCode);
        yearMonthFormat = intl.DateFormat.yMMMM(locale.languageCode);
        shortMonthDayFormat = intl.DateFormat.MMMd(locale.languageCode);
      } else {
        fullYearFormat = intl.DateFormat.y();
        compactDateFormat = intl.DateFormat.yMd();
        shortDateFormat = intl.DateFormat.yMMMd();
        mediumDateFormat = intl.DateFormat.MMMEd();
        longDateFormat = intl.DateFormat.yMMMMEEEEd();
        yearMonthFormat = intl.DateFormat.yMMMM();
        shortMonthDayFormat = intl.DateFormat.MMMd();
      }

      intl.NumberFormat decimalFormat;
      intl.NumberFormat twoDigitZeroPaddedFormat;
      if (intl.NumberFormat.localeExists(localeName)) {
        decimalFormat = intl.NumberFormat.decimalPattern(localeName);
        twoDigitZeroPaddedFormat = intl.NumberFormat('00', localeName);
      } else if (intl.NumberFormat.localeExists(locale.languageCode)) {
        decimalFormat = intl.NumberFormat.decimalPattern(locale.languageCode);
        twoDigitZeroPaddedFormat = intl.NumberFormat('00', locale.languageCode);
      } else {
        decimalFormat = intl.NumberFormat.decimalPattern();
        twoDigitZeroPaddedFormat = intl.NumberFormat('00');
      }

      return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation(
        locale,
        fullYearFormat,
        compactDateFormat,
        shortDateFormat,
        mediumDateFormat,
        longDateFormat,
        yearMonthFormat,
        shortMonthDayFormat,
        decimalFormat,
        twoDigitZeroPaddedFormat,
      )!);
    });
  }
  ...
}
ashdikashdik

flutter_localizations パッケージ
date_localizations.dart

void loadDateIntlDataIfNotLoaded() {
  if (!_dateIntlDataInitialized) {
    date_localizations.dateSymbols
      .cast<String, Map<String, dynamic>>()
      .forEach((String locale, Map<String, dynamic> data) {
        // Perform initialization.
        assert(date_localizations.datePatterns.containsKey(locale));
        final intl.DateSymbols symbols = intl.DateSymbols.deserializeFromMap(data);
        date_symbol_data_custom.initializeDateFormattingCustom( // ←ここ
          locale: locale,
          symbols: symbols,
          patterns: date_localizations.datePatterns[locale],
        );
      });
    _dateIntlDataInitialized = true;
  }
}
ashdikashdik

intl パッケージ
date_symbol_data_custom.dart

void initializeDateFormattingCustom(
    {String? locale, DateSymbols? symbols, Map<String, String>? patterns}) {
  initializeDateSymbols(_emptySymbols);
  initializeDatePatterns(_emptyPatterns);
  if (symbols == null) {
    throw ArgumentError('Missing DateTime formatting symbols');
  }
  if (patterns == null) {
    throw ArgumentError('Missing DateTime formatting patterns');
  }
  if (locale != symbols.NAME) {
    throw ArgumentError.value(
        [locale, symbols.NAME], 'Locale does not match symbols.NAME');
  }
  dateTimeSymbols[symbols.NAME] = symbols;
  dateTimePatterns[symbols.NAME] = patterns; // ←ここ
}
ashdikashdik

MaterialApp.supportedLocalesMaterialApp.localizationsDelegate が設定されている場合でも起きる

ログインするとコメントできます