ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2025-10-04

pubspec.yaml

name: wattconversion
description: "WattConversion"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.0+7

environment:
  sdk: ^3.9.2

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  package_info_plus: ^9.0.0
  shared_preferences: ^2.0.17
  flutter_localizations:    #多言語ライブラリの本体    # .arbファイルを更新したら flutter gen-l10n
    sdk: flutter
  intl: ^0.20.2     #多言語やフォーマッタなどの関連ライブラリ
  google_mobile_ads: ^6.0.0
  just_audio: ^0.10.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.14.4    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.6     #flutter pub run flutter_native_splash:create

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^6.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

flutter_launcher_icons:
  android: "launcher_icon"
  ios: true
  image_path: "assets/icon/icon.png"
  adaptive_icon_background: "assets/icon/icon_back.png"
  adaptive_icon_foreground: "assets/icon/icon_fore.png"

flutter_native_splash:
  color: '#3dbdd0'
  image: 'assets/image/splash.png'
  color_dark: '#3dbdd0'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#3dbdd0'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#3dbdd0'
    image_dark: 'assets/image/splash.png'

# The following section is specific to Flutter packages.
flutter:
  generate: true    #自動生成フラグの有効化

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  assets:
    - assets/image/
    - assets/sound/

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/to/font-from-package

lib/ad_banner_widget.dart

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

import 'package:wattconversion/ad_manager.dart';

class AdBannerWidget extends StatefulWidget {
  final AdManager adManager;
  const AdBannerWidget({super.key, required this.adManager});
  @override
  State<AdBannerWidget> createState() => _AdBannerWidgetState();
}

class _AdBannerWidgetState extends State<AdBannerWidget> {
  int _lastBannerWidthDp = 0;
  bool _isAdLoaded = false;
  bool _isLoading = false;
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: LayoutBuilder(
        builder: (context, constraints) {
          final int width = constraints.maxWidth.isFinite
              ? constraints.maxWidth.truncate()
              : MediaQuery.of(context).size.width.truncate();
          final bannerAd = widget.adManager.bannerAd;
          if (width > 0) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted) {
                final bannerAd = widget.adManager.bannerAd;
                final bool widthChanged = _lastBannerWidthDp != width;
                final bool sizeMismatch =
                    bannerAd == null || bannerAd.size.width != width;
                if ((widthChanged || !_isAdLoaded || sizeMismatch) &&
                    !_isLoading) {
                  _lastBannerWidthDp = width;
                  setState(() {
                    _isAdLoaded = false;
                    _isLoading = true;
                  });
                  widget.adManager.loadAdaptiveBannerAd(width, () {
                    if (mounted) {
                      setState(() {
                        _isAdLoaded = true;
                        _isLoading = false;
                      });
                    }
                  });
                }
              }
            });
          }
          if (_isAdLoaded && bannerAd != null) {
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    SizedBox(
                      width: bannerAd.size.width.toDouble(),
                      height: bannerAd.size.height.toDouble(),
                      child: AdWidget(ad: bannerAd),
                    ),
                  ],
                )
              ]
            );
          } else {
            return const SizedBox.shrink();
          }
        },
      ),
    );
  }
}

lib/ad_manager.dart

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class AdManager {
  // Test IDs
  // static const String _androidAdUnitId = "ca-app-pub-3940256099942544/6300978111";
  // static const String _iosAdUnitId     = "ca-app-pub-3940256099942544/2934735716";

  // Production IDs
  static const String _androidAdUnitId = "ca-app-pub-0/0";
  static const String _iosAdUnitId     = "ca-app-pub-0/0";

  static String get _adUnitId =>
      Platform.isIOS ? _iosAdUnitId : _androidAdUnitId;

  BannerAd? _bannerAd;
  int _lastWidthPx = 0;
  VoidCallback? _onLoadedCb;
  Timer? _retryTimer;
  int _retryAttempt = 0;

  BannerAd? get bannerAd => _bannerAd;

  Future<void> loadAdaptiveBannerAd(
    int widthPx,
    VoidCallback onAdLoaded,
  ) async {
    _onLoadedCb = onAdLoaded;
    _lastWidthPx = widthPx;
    _retryAttempt = 0;
    _retryTimer?.cancel();
    _startLoad(widthPx);
  }

  Future<void> _startLoad(int widthPx) async {
    _bannerAd?.dispose();

    AnchoredAdaptiveBannerAdSize? adaptiveSize;
    try {
      adaptiveSize =
          await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
            widthPx,
          );
    } catch (_) {
      adaptiveSize = null;
    }
    final AdSize size = adaptiveSize ?? AdSize.fullBanner;

    _bannerAd = BannerAd(
      adUnitId: _adUnitId,
      request: const AdRequest(),
      size: size,
      listener: BannerAdListener(
        onAdLoaded: (ad) {
          _retryTimer?.cancel();
          _retryAttempt = 0;
          final cb = _onLoadedCb;
          if (cb != null) {
            cb();
          }
        },
        onAdFailedToLoad: (ad, err) {
          ad.dispose();
          _scheduleRetry();
        },
      ),
    )..load();
  }

  void _scheduleRetry() {
    _retryTimer?.cancel();
    // Exponential backoff: 3s, 6s, 12s, max 30s
    _retryAttempt = (_retryAttempt + 1).clamp(1, 5);
    final seconds = _retryAttempt >= 4 ? 30 : (3 << (_retryAttempt - 1));
    _retryTimer = Timer(Duration(seconds: seconds), () {
      _startLoad(_lastWidthPx > 0 ? _lastWidthPx : 320);
    });
  }

  void dispose() {
    _bannerAd?.dispose();
    _retryTimer?.cancel();
  }
}

lib/audio_play.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

import 'package:just_audio/just_audio.dart';

import 'package:wattconversion/const_value.dart';

class AudioPlay {
  //音を重ねて連続再生できるようにインスタンスを用意しておき、順繰りに使う。
  static final List<AudioPlayer> _player01 = [
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),
    AudioPlayer(),  //6個
  ];
  int _player01Ptr = 0;

  double _soundVolume = 0.0;

  //constructor
  AudioPlay() {
    constructor();
  }
  void constructor() async {
    for (int i = 0; i < _player01.length; i++) {
      await _player01[i].setVolume(0);
      await _player01[i].setAsset(ConstValue.audioHiyoko[i]);
    }
    playZero();
  }
  void dispose() {
    for (int i = 0; i < _player01.length; i++) {
      _player01[i].dispose();
    }
  }
  //getter
  double get soundVolume {
    return _soundVolume;
  }
  //setter
  set soundVolume(double vol) {
    _soundVolume = vol;
  }
  //最初に音が鳴らないのを回避する方法
  void playZero() async {
    AudioPlayer ap = AudioPlayer();
    await ap.setAsset(ConstValue.audioZero);
    await ap.load();
    await ap.play();
  }
  //
  void play01() async {
    if (_soundVolume == 0) {
      return;
    }
    _player01Ptr += 1;
    if (_player01Ptr >= _player01.length) {
      _player01Ptr = 0;
    }
    await _player01[_player01Ptr].setVolume(_soundVolume);
    await _player01[_player01Ptr].pause();
    await _player01[_player01Ptr].seek(Duration.zero);
    await _player01[_player01Ptr].play();
  }
}

lib/const_value.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';

class ConstValue {
  //pref
  static const String prefLanguageCode = 'languageCode';
  static const String prefShowBackImage = 'showBackImage';
  static const String prefSoundVolume = 'soundVolume';
  static const String prefWattFrom = 'wattFrom';
  static const String prefWattTo = 'wattTo';
  static const String prefMinute = 'minute';
  static const String prefSecond = 'second';
  static const String prefThemeNumber = 'themeNumber';
  static const List<String> imageBacks = [
    'assets/image/kitchen001.webp',
    'assets/image/kitchen002.webp',
    'assets/image/kitchen003.webp',
    'assets/image/kitchen004.webp',
    'assets/image/kitchen005.webp',
    'assets/image/kitchen006.webp',
    'assets/image/kitchen007.webp',
    'assets/image/kitchen008.webp',
    'assets/image/kitchen009.webp',
    'assets/image/kitchen010.webp',
    'assets/image/kitchen011.webp',
    'assets/image/kitchen012.webp',
    'assets/image/kitchen013.webp',
    'assets/image/kitchen014.webp',
    'assets/image/kitchen015.webp',
    'assets/image/kitchen016.webp',
    'assets/image/kitchen017.webp',
    'assets/image/kitchen018.webp',
    'assets/image/kitchen019.webp',
    'assets/image/kitchen020.webp',
    'assets/image/kitchen021.webp',
    'assets/image/kitchen022.webp',
    'assets/image/kitchen023.webp',
    'assets/image/kitchen024.webp',
    'assets/image/kitchen025.webp',
    'assets/image/kitchen026.webp',
    'assets/image/kitchen027.webp',
    'assets/image/kitchen028.webp',
    'assets/image/kitchen029.webp',
    'assets/image/kitchen030.webp',
    'assets/image/kitchen031.webp',
    'assets/image/kitchen032.webp',
    'assets/image/kitchen033.webp',
    'assets/image/kitchen034.webp',
    'assets/image/kitchen035.webp',
    'assets/image/kitchen036.webp',
    'assets/image/kitchen037.webp',
    'assets/image/kitchen038.webp',
    'assets/image/kitchen039.webp',
    'assets/image/kitchen040.webp',
    'assets/image/kitchen041.webp',
    'assets/image/kitchen042.webp',
    'assets/image/kitchen043.webp',
    'assets/image/kitchen044.webp',
    'assets/image/kitchen045.webp',
    'assets/image/kitchen046.webp',
    'assets/image/kitchen047.webp',
    'assets/image/kitchen048.webp',
    'assets/image/kitchen049.webp',
    'assets/image/kitchen050.webp',
    'assets/image/kitchen051.webp',
    'assets/image/kitchen052.webp',
    'assets/image/kitchen053.webp',
    'assets/image/kitchen054.webp',
    'assets/image/kitchen055.webp',
    'assets/image/kitchen056.webp',
    'assets/image/kitchen057.webp',
    'assets/image/kitchen058.webp',
    'assets/image/kitchen059.webp',
    'assets/image/kitchen060.webp',
    'assets/image/kitchen061.webp',
    'assets/image/kitchen062.webp',
    'assets/image/kitchen063.webp',
    'assets/image/kitchen064.webp',
    'assets/image/kitchen065.webp',
    'assets/image/kitchen066.webp',
    'assets/image/kitchen067.webp',
    'assets/image/kitchen068.webp',
    'assets/image/kitchen069.webp',
    'assets/image/kitchen070.webp',
    'assets/image/kitchen071.webp',
    'assets/image/kitchen072.webp',
    'assets/image/kitchen073.webp',
    'assets/image/kitchen074.webp',
    'assets/image/kitchen075.webp',
    'assets/image/kitchen076.webp',
    'assets/image/kitchen077.webp',
    'assets/image/kitchen078.webp',
    'assets/image/kitchen079.webp',
    'assets/image/kitchen080.webp',
    'assets/image/kitchen081.webp',
    'assets/image/kitchen082.webp',
    'assets/image/kitchen083.webp',
    'assets/image/kitchen084.webp',
    'assets/image/kitchen085.webp',
    'assets/image/kitchen086.webp',
    'assets/image/kitchen087.webp',
    'assets/image/kitchen088.webp',
    'assets/image/kitchen089.webp',
    'assets/image/kitchen090.webp',
    'assets/image/kitchen091.webp',
    'assets/image/kitchen092.webp',
    'assets/image/kitchen093.webp',
    'assets/image/kitchen094.webp',
    'assets/image/kitchen095.webp',
    'assets/image/kitchen096.webp',
    'assets/image/kitchen097.webp',
    'assets/image/kitchen098.webp',
    'assets/image/kitchen099.webp',
    'assets/image/kitchen100.webp',
    'assets/image/kitchen101.webp',
    'assets/image/kitchen102.webp',
    'assets/image/kitchen103.webp',
    'assets/image/kitchen104.webp',
    'assets/image/kitchen105.webp',
    'assets/image/kitchen106.webp',
    'assets/image/kitchen107.webp',
    'assets/image/kitchen108.webp',
    'assets/image/kitchen109.webp',
    'assets/image/kitchen110.webp',
    'assets/image/kitchen111.webp',
    'assets/image/kitchen112.webp',
    'assets/image/kitchen113.webp',
    'assets/image/kitchen114.webp',
    'assets/image/kitchen115.webp',
    'assets/image/kitchen116.webp',
    'assets/image/kitchen117.webp',
    'assets/image/kitchen118.webp',
    'assets/image/kitchen119.webp',
    'assets/image/kitchen120.webp',
    'assets/image/kitchen121.webp',
    'assets/image/kitchen122.webp',
    'assets/image/kitchen123.webp',
    'assets/image/kitchen124.webp',
    'assets/image/kitchen125.webp',
    'assets/image/kitchen126.webp',
    'assets/image/kitchen127.webp',
    'assets/image/kitchen128.webp',
    'assets/image/kitchen129.webp',
    'assets/image/kitchen130.webp',
    'assets/image/kitchen131.webp',
    'assets/image/kitchen132.webp',
    'assets/image/kitchen133.webp',
    'assets/image/kitchen134.webp',
    'assets/image/kitchen135.webp',
    'assets/image/kitchen136.webp',
    'assets/image/kitchen137.webp',
    'assets/image/kitchen138.webp',
    'assets/image/kitchen139.webp',
    'assets/image/kitchen140.webp',
    'assets/image/kitchen141.webp',
    'assets/image/kitchen142.webp',
    'assets/image/kitchen143.webp',
    'assets/image/kitchen144.webp',
    'assets/image/kitchen145.webp',
    'assets/image/kitchen146.webp',
    'assets/image/kitchen147.webp',
    'assets/image/kitchen148.webp',
    'assets/image/kitchen149.webp',
    'assets/image/kitchen150.webp',
    'assets/image/kitchen151.webp',
    'assets/image/kitchen152.webp',
    'assets/image/kitchen153.webp',
    'assets/image/kitchen154.webp',
    'assets/image/kitchen155.webp',
    'assets/image/kitchen156.webp',
    'assets/image/kitchen157.webp',
    'assets/image/kitchen158.webp',
    'assets/image/kitchen159.webp',
    'assets/image/kitchen160.webp',
    'assets/image/kitchen161.webp',
    'assets/image/kitchen162.webp',
    'assets/image/kitchen163.webp',
    'assets/image/kitchen164.webp',
    'assets/image/kitchen165.webp',
    'assets/image/kitchen166.webp',
    'assets/image/kitchen167.webp',
    'assets/image/kitchen168.webp',
    'assets/image/kitchen169.webp',
    'assets/image/kitchen170.webp',
    'assets/image/kitchen171.webp',
    'assets/image/kitchen172.webp',
    'assets/image/kitchen173.webp',
    'assets/image/kitchen174.webp',
    'assets/image/kitchen175.webp',
    'assets/image/kitchen176.webp',
    'assets/image/kitchen177.webp',
    'assets/image/kitchen178.webp',
    'assets/image/kitchen179.webp',
    'assets/image/kitchen180.webp',
    'assets/image/kitchen181.webp',
    'assets/image/kitchen182.webp',
    'assets/image/kitchen183.webp',
    'assets/image/kitchen184.webp',
    'assets/image/kitchen185.webp',
    'assets/image/kitchen186.webp',
    'assets/image/kitchen187.webp',
    'assets/image/kitchen188.webp',
    'assets/image/kitchen189.webp',
    'assets/image/kitchen190.webp',
    'assets/image/kitchen191.webp',
    'assets/image/kitchen192.webp',
    'assets/image/kitchen193.webp',
    'assets/image/kitchen194.webp',
    'assets/image/kitchen195.webp',
    'assets/image/kitchen196.webp',
    'assets/image/kitchen197.webp',
    'assets/image/kitchen198.webp',
    'assets/image/kitchen199.webp',
    'assets/image/kitchen200.webp',
  ];
  //color
  static const Color colorMainHeader = Color.fromRGBO(0,0,0,0.4);
  static const Color colorMainBack = Color.fromRGBO(220, 220, 220, 1);
  static const Color colorMainUiActiveColorFrom = Color.fromRGBO(255,30,80,1);
  static const Color colorMainUiActiveColorTo = Color.fromRGBO(120,120,255,1);
  static const Color colorSettingUiActiveColor = Colors.deepPurple;
  static const Color colorUiInactiveColor = Color.fromRGBO(50,50,50,1);
  //sound
  static const String audioZero = 'assets/sound/zero.wav';    //無音1秒
  static const List<String> audioHiyoko = [
    'assets/sound/hiyoko1.wav',
    'assets/sound/hiyoko2.wav',
    'assets/sound/hiyoko3.wav',
    'assets/sound/hiyoko4.wav',
    'assets/sound/hiyoko5.wav',
    'assets/sound/hiyoko6.wav',
  ];

}

lib/empty.dart

lib/language_support.dart

///
/// Consolidated language utilities.
///
library;

/// Usage:
///   await LanguageState.ensureInitialized();
///   final currentCode = await LanguageState.getLanguageCode();
///   await LanguageState.setLanguageCode('en');
///   final locale = parseLocaleTag(currentCode);
///
/// Use LanguageCatalog when presenting selectable language names.
/// Configure LanguageState.configure if you need custom storage.
///

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LanguageCatalog {
  const LanguageCatalog._();

  static const String prefLanguageCode = 'languageCode';

  static const Map<String, String> names = {
    'en': 'English',
    'bg': 'Български',
    'cs': 'Čeština',
    'da': 'Dansk',
    'de': 'Deutsch',
    'el': 'Ελληνικά',
    'es': 'Español',
    'et': 'Eesti',
    'fi': 'Suomi',
    'fr': 'Français',
    'hu': 'Magyar',
    'it': 'Italiano',
    'ja': '日本語',
    'lt': 'Lietuvių',
    'lv': 'Latviešu',
    'nl': 'Nederlands',
    'pl': 'Polski',
    'pt': 'Português',
    'ro': 'Română',
    'ru': 'Русский',
    'sk': 'Slovenčina',
    'sv': 'Svenska',
    'th': 'ไทย',
    'zh': '中文',
  };

  static List<String> get supportedCodes => names.keys.toList(growable: false);

  static List<Locale> buildSupportedLocales() {
    return supportedCodes.map((code) => Locale(code)).toList(growable: false);
  }

  static String labelFor(String? code) {
    if (code == null || code.isEmpty) {
      return 'Default';
    }
    return names[code] ?? code;
  }
}

abstract class LanguageStorage {
  const LanguageStorage();

  Future<void> saveLanguageCode(String code);

  Future<String> loadLanguageCode();
}

class SharedPreferencesLanguageStorage extends LanguageStorage {
  const SharedPreferencesLanguageStorage({
    this.prefLanguageCode = LanguageCatalog.prefLanguageCode,
  });

  final String prefLanguageCode;

  Future<SharedPreferences> _ensurePrefs() async {
    return SharedPreferences.getInstance();
  }

  @override
  Future<void> saveLanguageCode(String code) async {
    final prefs = await _ensurePrefs();
    await prefs.setString(prefLanguageCode, code);
  }

  @override
  Future<String> loadLanguageCode() async {
    final prefs = await _ensurePrefs();
    return prefs.getString(prefLanguageCode) ?? '';
  }
}

class LanguageState {
  LanguageState._();

  static LanguageStorage _storage = const SharedPreferencesLanguageStorage();
  static String _languageCode = '';
  static bool _initialized = false;
  static Completer<void>? _initializing;

  static String get currentCode => _languageCode;

  static void configure({LanguageStorage? storage, String? initialCode}) {
    if (storage != null) {
      _storage = storage;
    }
    if (initialCode != null) {
      _languageCode = initialCode;
      _initialized = true;
    }
  }

  static Future<void> ensureInitialized() async {
    if (_initialized) {
      return;
    }
    final completer = _initializing;
    if (completer != null) {
      await completer.future;
      return;
    }
    final newCompleter = Completer<void>();
    _initializing = newCompleter;
    try {
      _languageCode = await _storage.loadLanguageCode();
      _initialized = true;
      newCompleter.complete();
    } catch (error, stackTrace) {
      if (!newCompleter.isCompleted) {
        newCompleter.completeError(error, stackTrace);
      }
      rethrow;
    } finally {
      _initializing = null;
    }
  }

  static Future<void> setLanguageCode(String? code) async {
    final value = code?.trim() ?? '';
    _languageCode = value;
    _initialized = true;
    await _storage.saveLanguageCode(value);
  }

  static Future<String> getLanguageCode() async {
    await ensureInitialized();
    return _languageCode;
  }
}

Locale? parseLocaleTag(String tag) {
  if (tag.isEmpty) {
    return null;
  }
  final parts = tag.split('-');
  final language = parts.isNotEmpty ? parts[0] : tag;
  String? script;
  String? country;
  if (parts.length >= 2) {
    final p1 = parts[1];
    if (p1.length == 4) {
      script = p1;
    } else {
      country = p1;
    }
  }
  if (parts.length >= 3) {
    final p2 = parts[2];
    if (p2.length == 4) {
      script = p2;
    } else {
      country = p2;
    }
  }
  return Locale.fromSubtags(
    languageCode: language,
    scriptCode: script,
    countryCode: country,
  );
}

lib/loading_screen.dart

import 'package:flutter/material.dart';

class LoadingScreen extends StatelessWidget {
  const LoadingScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.indigo,
      body: const Center(
        child: CircularProgressIndicator(
          valueColor: AlwaysStoppedAnimation<Color>(Colors.indigoAccent),
          backgroundColor: Colors.white,
        ),
      ),
    );
  }
}

lib/main.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-02
///
library;

import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'l10n/app_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart' if (dart.library.html) 'empty.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

import 'package:wattconversion/language_support.dart';
import 'package:wattconversion/const_value.dart';
import 'package:wattconversion/version_state.dart';
import 'package:wattconversion/setting.dart';
import 'package:wattconversion/ad_manager.dart';
import 'package:wattconversion/ad_banner_widget.dart';
import 'package:wattconversion/preferences.dart';
import 'package:wattconversion/audio_play.dart';
import 'package:wattconversion/loading_screen.dart';
import 'package:wattconversion/theme_color.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb) {
    MobileAds.instance.initialize();
  }
  await Preferences.initial();
  await LanguageState.ensureInitialized();
  runApp(const MainApp());
}

ThemeMode _themeModeFromNumber(int value) {
  switch (value) {
    case 1:
      return ThemeMode.light;
    case 2:
      return ThemeMode.dark;
    default:
      return ThemeMode.system;
  }
}

class MainApp extends StatefulWidget {    //statefulに変更して言語変更に対応
  const MainApp({super.key});
  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  Locale? localeLanguage;
  ThemeMode themeMode = ThemeMode.system;
  @override
  void initState() {
    super.initState();
    localeLanguage = parseLocaleTag(LanguageState.currentCode);
    themeMode = _themeModeFromNumber(Preferences.themeNumber);
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: AppLocalizations.localizationsDelegates,   //多言語化
      supportedLocales: AppLocalizations.supportedLocales,  //自動で言語リストを生成
      locale: localeLanguage,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: ConstValue.colorMainBack),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: ConstValue.colorMainBack,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: themeMode,
      home: const MainHomePage(),
    );
  }
}

class MainHomePage extends StatefulWidget {
  const MainHomePage({super.key});
  @override
  State<MainHomePage> createState() => _MainHomePageState();
}

class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
  late AdManager _adManager;
  late ThemeColor _themeColor;
  final AudioPlay _audioPlay = AudioPlay(); //効果音
  late AnimationController _animationController;
  late Animation<double> _opacityAnimation;
  bool _showBackImage = true;
  int _backImageNumber = 0;
  int _lastBackImageNumber = 0;
  int _wattFrom = 600;
  int _wattTo = 500;
  int _minute = 5;
  int _second = 0;
  int _themeNumber = 0;
  bool _isReady = false;
  bool _isFirst = true;

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }
  //言語準備
  Future<void> _getCurrentLocale() async {
    final String code = await LanguageState.getLanguageCode();
    if (!mounted) {
      return;
    }
    final mainState = context.findAncestorStateOfType<_MainAppState>();
    if (mainState == null) {
      return;
    }
    mainState
      ..localeLanguage = parseLocaleTag(code)
      ..setState(() {});
  }
  //Theme
  void _getThemeColor() {
    _themeColor = ThemeColor(themeNumber: _themeNumber, context: context);
  }
  void _applyThemeMode() {
    final mainState = context.findAncestorStateOfType<_MainAppState>();
    if (mainState == null) {
      return;
    }
    mainState
      ..themeMode = _themeModeFromNumber(Preferences.themeNumber)
      ..setState(() {});
  }

  @override
  void initState() {
    super.initState();
    _initState();
  }

  void _initState() async {
    _getVersion();
    _getCurrentLocale();
    LanguageState.getLanguageCode();
    _adManager = AdManager();
    _audioPlay.playZero();
    final int subSecond = (DateTime.now()).millisecondsSinceEpoch ~/ 100;
    _backImageNumber = subSecond % ConstValue.imageBacks.length;
    //background animation
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    _opacityAnimation = Tween<double>(begin: 0, end: 1).animate(_animationController);
    _animationController.addListener(() {
      setState(() {});
    });
    _backImageChange();
    //
    await Preferences.initial();
    _showBackImage = Preferences.showBackImage;
    _audioPlay.soundVolume = Preferences.soundVolume;
    _wattFrom = Preferences.wattFrom;
    _wattTo = Preferences.wattTo;
    _minute = Preferences.minute;
    _second = Preferences.second;
    _themeNumber = Preferences.themeNumber;
    _applyThemeMode();
    setState(() {
      _isReady = true;
    });
  }

  @override
  void dispose() {
    _adManager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_isReady == false) {
      return const LoadingScreen();
    }
    if (_isFirst) {
      _isFirst = false;
      _getThemeColor();
    }
    return Container(
      decoration: const BoxDecoration(
        color: ConstValue.colorMainBack,
      ),
      child: Container(
        decoration: _decoration2(),
        child: Container(
          decoration: _decoration1(),
          child: Scaffold(
            backgroundColor: Colors.transparent, //ここは透明
            appBar: AppBar(
              backgroundColor: ConstValue.colorMainHeader,
              //タイトル表示
              title: Text(AppLocalizations.of(context)!.title,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 12.0,
                )
              ),
              //設定ボタン
              actions: <Widget>[
                TextButton(
                  onPressed: () async {
                    bool? ret = await Navigator.of(context).push(
                      MaterialPageRoute<bool>(builder:(context) => const SettingPage()),
                    );
                    //awaitで呼び出しているので、settingから戻ったら以下が実行される。
                    if (ret!) { //設定で適用だった場合
                      await _getCurrentLocale();
                      _showBackImage = Preferences.showBackImage;
                      _audioPlay.soundVolume = Preferences.soundVolume;
                      _themeNumber = Preferences.themeNumber;
                      _getThemeColor();
                      _applyThemeMode();
                      setState(() {});
                    }
                  },
                  child: Text(
                    AppLocalizations.of(context)!.setting,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 12.0,
                    )
                  )
                )
              ]
            ),
            body: SafeArea(
              child: GestureDetector(
                onTap: () {
                  _audioPlay.play01();
                  _backImageChange();
                },
                child: Column(children: [
                  Expanded(
                    child: SingleChildScrollView(
                      child: Padding(
                        padding: const EdgeInsets.only(top: 5, left: 5, right: 5, bottom: 100),
                        child: Column(children: [
                          _content(),
                        ])
                      )
                    )
                  ),
                ])
              )
            ),
            bottomNavigationBar: AdBannerWidget(adManager: _adManager),
          )
        )
      )
    );
  }
  //背景画像前側
  Decoration _decoration1() {
    if (_showBackImage) {
      return BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageBacks[_backImageNumber]),
          fit: BoxFit.cover,
          colorFilter: ColorFilter.mode(
            Colors.black.withValues(alpha: _opacityAnimation.value),
            BlendMode.dstATop,
          ),
        )
      );
    } else {
      return const BoxDecoration();
    }
  }
  //背景画像後ろ側
  Decoration _decoration2() {
    if (_showBackImage) {
      return BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageBacks[_lastBackImageNumber]),
          fit: BoxFit.cover,
        ),
      );
    } else {
      return const BoxDecoration();
    }
  }
  void _backImageChange() {
    final int subSecond = (DateTime.now()).millisecondsSinceEpoch ~/ 100;
    _backImageNumber = subSecond % ConstValue.imageBacks.length;
    _animationController.forward();
    Future.delayed(const Duration(milliseconds: 600), () {
      _lastBackImageNumber = _backImageNumber;
      _animationController.reverse();
    });
  }

  Widget _content() {
    return Column(children:[
      _widgetWattFrom(),
      _widgetMinute(),
      _widgetSecond(),
      _widgetWattTo(),
      _widgetResult(),
    ]);
  }

  Widget _widgetWattFrom() {
    final l = AppLocalizations.of(context)!;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 4, right: 4, bottom: 0),
        color: _themeColor.backColorMono.withValues(alpha: 0.9),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(12),
            topRight: Radius.circular(12),
            bottomLeft: Radius.circular(0),
            bottomRight: Radius.circular(0),
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Padding(
                padding: const EdgeInsets.only(top: 5, left: 10, right: 0, bottom: 0),
                child: Row(children: [
                  Text(l.wattFrom),
                  const Spacer(),
                ])
              ),
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 10, right: 0, bottom: 0),
                child: Row(children: <Widget>[
                  Container(
                    color: _themeColor.backColorMono,
                    child: SizedBox(
                      width: 80,
                      child: Text(_wattFrom.toString(),textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.titleLarge
                      ),
                    )
                  ),
                  Expanded(
                    child: Slider(
                      value: _wattFrom.toDouble(),
                      min: 300,
                      max: 1800,
                      divisions: 15,
                      onChanged: (double value) {
                        setState(() {
                          _wattFrom = value.toInt();
                          Preferences.setWattFrom(_wattFrom);
                        });
                      },
                      activeColor: ConstValue.colorMainUiActiveColorFrom,
                      inactiveColor: ConstValue.colorUiInactiveColor,
                    )
                  )
                ])
              )
            ]
          ),
        ),
      )
    );
  }

  Widget _widgetMinute() {
    final l = AppLocalizations.of(context)!;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 3, right: 4, bottom: 0),
        color: _themeColor.backColorMono.withValues(alpha: 0.9),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(0),
            topRight: Radius.circular(0),
            bottomLeft: Radius.circular(0),
            bottomRight: Radius.circular(0),
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Padding(
                padding: const EdgeInsets.only(top: 5, left: 10, right: 0, bottom: 0),
                child: Row(children: [
                  Text(l.fromMinute),
                  const Spacer(),
                ])
              ),
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 10, right: 0, bottom: 0),
                child: Row(children: <Widget>[
                  Container(
                    color: _themeColor.backColorMono,
                    child: SizedBox(
                      width: 80,
                      child: Text(_minute.toString(),textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.titleLarge
                      ),
                    )
                  ),
                  Expanded(
                    child: Slider(
                      value: _minute.toDouble(),
                      min: 1,
                      max: 30,
                      divisions: 29,
                      onChanged: (double value) {
                        setState(() {
                          _minute = value.toInt();
                          Preferences.setMinute(_minute);
                        });
                      },
                      activeColor: ConstValue.colorMainUiActiveColorFrom,
                      inactiveColor: ConstValue.colorUiInactiveColor,
                    )
                  )
                ])
              )
            ]
          ),
        ),
      )
    );
  }

  Widget _widgetSecond() {
    final l = AppLocalizations.of(context)!;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 3, right: 4, bottom: 0),
        color: _themeColor.backColorMono.withValues(alpha: 0.9),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(0),
            topRight: Radius.circular(0),
            bottomLeft: Radius.circular(12),
            bottomRight: Radius.circular(12),
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Padding(
                padding: const EdgeInsets.only(top: 5, left: 10, right: 0, bottom: 0),
                child: Row(children: [
                  Text(l.fromSecond),
                  const Spacer(),
                ])
              ),
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 10, right: 0, bottom: 0),
                child: Row(children: <Widget>[
                  Container(
                    color: _themeColor.backColorMono,
                    child: SizedBox(
                      width: 80,
                      child: Text(_second.toString(),textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.titleLarge
                      ),
                    )
                  ),
                  Expanded(
                    child: Slider(
                      value: _second.toDouble(),
                      min: 0,
                      max: 50,
                      divisions: 5,
                      onChanged: (double value) {
                        setState(() {
                          _second = value.toInt();
                          Preferences.setMinute(_second);
                        });
                      },
                      activeColor: ConstValue.colorMainUiActiveColorFrom,
                      inactiveColor: ConstValue.colorUiInactiveColor,
                    )
                  )
                ])
              )
            ]
          ),
        ),
      )
    );
  }

  Widget _widgetWattTo() {
    final l = AppLocalizations.of(context)!;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.backColorMono.withValues(alpha: 0.9),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Padding(
                padding: const EdgeInsets.only(top: 5, left: 10, right: 0, bottom: 0),
                child: Row(children: [
                  Text(l.wattTo),
                  const Spacer(),
                ])
              ),
              Padding(
                padding: const EdgeInsets.only(top: 0, left: 10, right: 0, bottom: 0),
                child: Row(children: <Widget>[
                  Container(
                    color: _themeColor.backColorMono,
                    child: SizedBox(
                      width: 80,
                      child: Text(_wattTo.toString(),textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.titleLarge
                      ),
                    )
                  ),
                  Expanded(
                    child: Slider(
                      value: _wattTo.toDouble(),
                      min: 300,
                      max: 1800,
                      divisions: 15,
                      onChanged: (double value) {
                        setState(() {
                          _wattTo = value.toInt();
                          Preferences.setWattTo(_wattTo);
                        });
                      },
                      activeColor: ConstValue.colorMainUiActiveColorTo,
                      inactiveColor: ConstValue.colorUiInactiveColor,
                    )
                  )
                ])
              ),
            ]
          ),
        ),
      )
    );
  }

  Widget _widgetResult() {
    final l = AppLocalizations.of(context)!;
    final int sec = ((_minute * 60 + _second) / _wattTo * _wattFrom).toInt();
    final int answerMinute = (sec / 60).floor();
    final int answerSecond = sec % 60;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.backColorMono.withValues(alpha: 0.9),
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Row(children: [
                const Spacer(),
                Text('${l.specified} ${_wattFrom}W',
                  style: TextStyle(
                    color: ConstValue.colorMainUiActiveColorFrom,
                    fontSize: Theme.of(context).textTheme.titleLarge?.fontSize,
                  )
                ),
                const Spacer(),
              ]),
              Row(children: [
                const Spacer(),
                Text('${_minute} ${l.minute} ${_second} ${l.second}',
                    style: TextStyle(
                      color: ConstValue.colorMainUiActiveColorFrom,
                      fontSize: Theme.of(context).textTheme.titleLarge?.fontSize,
                    )
                ),
                const Spacer(),
              ]),
              SizedBox(height: 5),
              Row(children: [
                const Spacer(),
                Text('${l.conversion} ${_wattTo}W',
                  style: TextStyle(
                    color: ConstValue.colorMainUiActiveColorTo,
                    fontSize: Theme.of(context).textTheme.titleLarge?.fontSize,
                  )
                ),
                const Spacer(),
              ]),
              Row(children: [
                const Spacer(),
                Text('${answerMinute} ${l.minute} ${answerSecond} ${l.second}',
                  style: TextStyle(
                    color: ConstValue.colorMainUiActiveColorTo,
                    fontSize: Theme.of(context).textTheme.titleLarge?.fontSize,
                  ),
                ),
                const Spacer(),
              ])
            ]
          ),
        ),
      )
    );
  }

}

lib/preferences.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-26
///
library;

import 'package:shared_preferences/shared_preferences.dart';

import 'package:wattconversion/const_value.dart';

//デバイスに情報を保存
class Preferences {

  static bool ready = false;
  //この値は常に最新にしておく
  static String _languageCode = '';
  static bool _showBackImage = true;
  static double _soundVolume = 0.3;
  static int _wattFrom = 600;
  static int _wattTo = 500;
  static int _minute = 5;
  static int _second = 0;
  static int _themeNumber = 0;

  static String get languageCode => _languageCode;
  static bool get showBackImage => _showBackImage;
  static double get soundVolume => _soundVolume;
  static int get wattFrom => _wattFrom;
  static int get wattTo => _wattTo;
  static int get minute => _minute;
  static int get second => _second;
  static int get themeNumber => _themeNumber;

  static Future<void> initial() async {
    _languageCode = await getLanguageCode();
    _showBackImage = await getShowBackImage();
    _soundVolume = await getSoundVolume();
    _wattFrom = await getWattFrom();
    _wattTo = await getWattTo();
    _minute = await getMinute();
    _second = await getSecond();
    _themeNumber = await getThemeNumber();
    ready = true;
  }

  //----------------------------

  //言語コード
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefLanguageCode, str);
  }
  static Future<String> getLanguageCode() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String str = prefs.getString(ConstValue.prefLanguageCode) ?? 'en';
    return str;
  }

  //----------------------------

  //背景画像表示
  static Future<void> setShowBackImage(bool flag) async {
    _showBackImage = flag;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(ConstValue.prefShowBackImage, flag);
  }
  static Future<bool> getShowBackImage() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final bool flag = prefs.getBool(ConstValue.prefShowBackImage) ?? true;
    return flag;
  }

  //----------------------------

  //効果音音量
  static Future<void> setSoundVolume(double num) async {
    _soundVolume = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefSoundVolume, num);
  }
  static Future<double> getSoundVolume() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefSoundVolume) ?? 0.3;
    return num;
  }

  //----------------------------

  //watt from
  static Future<void> setWattFrom(int num) async {
    _wattFrom = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefWattFrom, num);
  }
  static Future<int> getWattFrom() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefWattFrom) ?? 600;
    return num;
  }

  //----------------------------

  //watt to
  static Future<void> setWattTo(int num) async {
    _wattTo = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefWattTo, num);
  }
  static Future<int> getWattTo() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefWattTo) ?? 500;
    return num;
  }
  //----------------------------

  //minute
  static Future<void> setMinute(int num) async {
    _minute = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefMinute, num);
  }
  static Future<int> getMinute() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefMinute) ?? 5;
    return num;
  }

  //----------------------------

  //second
  static Future<void> setSecond(int num) async {
    _second = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefSecond, num);
  }
  static Future<int> getSecond() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefSecond) ?? 0;
    return num;
  }

  //----------------------------

  //themeNumber
  static Future<void> setThemeNumber(int num) async {
    _themeNumber = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setInt(ConstValue.prefThemeNumber, num);
  }
  static Future<int> getThemeNumber() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final int num = prefs.getInt(ConstValue.prefThemeNumber) ?? 0;
    return num;
  }

  //----------------------------

}

lib/setting.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';

import 'package:wattconversion/language_support.dart';
import 'package:wattconversion/preferences.dart';
import 'package:wattconversion/const_value.dart';
import 'package:wattconversion/version_state.dart';
import 'package:wattconversion/ad_manager.dart';
import 'package:wattconversion/ad_banner_widget.dart';
import 'package:wattconversion/loading_screen.dart';
import 'package:wattconversion/theme_color.dart';

class SettingPage extends StatefulWidget {
  const SettingPage({super.key});
  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  String? _languageKey;
  late AdManager _adManager;
  late ThemeColor _themeColor;
  bool _showBackImage = true;
  double _soundVolume = 0.0;
  int _themeNumber = 0;
  bool _isReady = false;
  bool _isFirst = true;

  @override
  void initState() {
    super.initState();
    _initState();
  }

  void _initState() async {
    _adManager = AdManager();
    final String languageCode = await LanguageState.getLanguageCode();
    if (!mounted) {
      return;
    }
    _languageKey = languageCode.isEmpty ? null : languageCode;
    await Preferences.initial();
    if (!mounted) {
      return;
    }
    _showBackImage = Preferences.showBackImage;
    _soundVolume = Preferences.soundVolume;
    _themeNumber = Preferences.themeNumber;
    setState(() {
      _isReady = true;
    });
  }

  @override
  void dispose() {
    _adManager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_isReady == false) {
      return LoadingScreen();
    }
    if (_isFirst) {
      _isFirst = false;
      _themeColor = ThemeColor(themeNumber: _themeNumber, context: context);
    }
    final l = AppLocalizations.of(context)!;
    return Scaffold(
      backgroundColor: _themeColor.backColor,
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false);
          },
        ),
        title: Text(l.setting),
        foregroundColor: _themeColor.appBarForegroundColor,
        backgroundColor: Colors.transparent,
        actions: [
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: () async {
              await LanguageState.setLanguageCode(_languageKey);
              await Preferences.setShowBackImage(_showBackImage);
              await Preferences.setSoundVolume(_soundVolume);
              await Preferences.setThemeNumber(_themeNumber);
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);
            },
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.only(left: 4, top: 4, right: 4, bottom: 100),
                child: Column(children: [
                  _buildBackgroundImage(),
                  _buildVolume(),
                  _buildLanguage(),
                  _buildTheme(),
                  _buildUsage(),
                  _buildVersion(),
                ]),
              ),
            ),
          ),
        ),
      ]),
      bottomNavigationBar: AdBannerWidget(adManager: _adManager),
    );
  }

  Widget _buildBackgroundImage() {
    final l = AppLocalizations.of(context)!;
    return Card(
      margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
      color: _themeColor.cardColor,
      elevation: 0,
      shadowColor: Colors.transparent,
      surfaceTintColor: Colors.transparent,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 16),
            child: Row(children:<Widget>[
              Expanded(child: Text(l.showBackImage)),
              Switch(
                value: _showBackImage,
                onChanged: (bool value) {
                  setState(() {
                    _showBackImage = value;
                  });
                },
                activeThumbColor: ConstValue.colorSettingUiActiveColor,
                inactiveThumbColor: ConstValue.colorUiInactiveColor,
              ),
            ]),
          ),
        ],
      ),
    );
  }

  Widget _buildVolume() {
    final l = AppLocalizations.of(context)!;
    return Card(
      margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
      color: _themeColor.cardColor,
      elevation: 0,
      shadowColor: Colors.transparent,
      surfaceTintColor: Colors.transparent,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
            child: Row(children: [
              Text(l.soundVolume),
              const Spacer(),
            ])
          ),
          Padding(
            padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
            child: Row(children: <Widget>[
              Text(_soundVolume.toString()),
              Expanded(
                child: Slider(
                  value: _soundVolume,
                  min: 0.0,
                  max: 1.0,
                  divisions: 10,
                  onChanged: (double value) {
                    setState(() {
                      _soundVolume = value;
                    });
                  },
                  activeColor: ConstValue.colorSettingUiActiveColor,
                  inactiveColor: ConstValue.colorUiInactiveColor,
                )
              )
            ])
          ),
        ],
      ),
    );
  }

  Widget _buildLanguage() {
    final l = AppLocalizations.of(context)!;
    return Card(
      margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
      color: _themeColor.cardColor,
      elevation: 0,
      shadowColor: Colors.transparent,
      surfaceTintColor: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ListTile(
              title: Text(l.language,style: Theme.of(context).textTheme.bodyMedium),
              contentPadding: const EdgeInsets.symmetric(horizontal: 0),
              trailing: DropdownButton<String?>(
                value: _languageKey,
                dropdownColor: _themeColor.dropdownColor,
                items: [
                  const DropdownMenuItem<String?>(
                    value: null,
                    child: Text('Default'),
                  ),
                  ...LanguageCatalog.names.entries.map(
                        (entry) => DropdownMenuItem<String?>(
                      value: entry.key,
                      child: Text(entry.value),
                    ),
                  ),
                ],
                onChanged: (String? value) {
                  setState(() {
                    _languageKey = value;
                  });
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildTheme() {
    final l = AppLocalizations.of(context)!;
    return Card(
      margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
      color: _themeColor.cardColor,
      elevation: 0,
      shadowColor: Colors.transparent,
      surfaceTintColor: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ListTile(
              title: Text(l.theme,style: Theme.of(context).textTheme.bodyMedium),
              contentPadding: const EdgeInsets.symmetric(horizontal: 0),
              trailing: DropdownButton<int>(
                value: _themeNumber,
                dropdownColor: _themeColor.dropdownColor,
                items: [
                  DropdownMenuItem(value: 0, child: Text(l.systemDefault)),
                  DropdownMenuItem(value: 1, child: Text(l.lightTheme)),
                  DropdownMenuItem(value: 2, child: Text(l.darkTheme)),
                ],
                onChanged: (int? value) {
                  if (value == null) {
                    return;
                  }
                  setState(() {
                    _themeNumber = value;
                  });
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildUsage() {
    final l = AppLocalizations.of(context)!;
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children:[
              Text(l.usage1),
              const SizedBox(height:15),
              Text(l.usage2),
              if (l.usage3 != '') Column(children: [
                const SizedBox(height:15),
                Text(l.usage3),
              ]),
              if (l.usage4 != '') Column(children: [
                const SizedBox(height:15),
                Text(l.usage4),
              ])
            ]
          ),
        ),
      )
    );
  }

  Widget _buildVersion() {
    return SizedBox(
      width: double.infinity,
      child: Card(
        margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
        color: _themeColor.cardColor,
        elevation: 0,
        shadowColor: Colors.transparent,
        surfaceTintColor: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Center(
            child: Text(
              'version  ${VersionState.versionLoad()}',
              style: const TextStyle(fontSize: 10),
            ),
          ),
        ),
      ),
    );
  }

}

lib/version_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

class VersionState {

  static String _version = '';

  //バージョンを記録
  static void versionSave(String str) {
    _version = str;
  }
  //バージョンを返す
  static String versionLoad() {
    return _version;
  }

}