ソースコード source code

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

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

下記コードの最終ビルド日: 2023-11-18

pubspec.yaml

name: galmoji
description: "ギャル文字"
# 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.0.0+1

environment:
  sdk: '>=3.3.0-127.0.dev <4.0.0'

# 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
  share_plus: ^7.0.2

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6
  package_info_plus: ^4.1.0
  shared_preferences: ^2.0.17
  google_mobile_ads: ^3.1.0
  just_audio: ^0.9.36
  fluttertoast: ^8.2.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.13.1    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.5     #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: ^3.0.1

# 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: '#c80064'
  image: 'assets/image/splash.png'
  color_dark: '#c80064'
  image_dark: 'assets/image/splash.png'
  fullscreen: true
  android_12:
    icon_background_color: '#c80064'
    image: 'assets/image/splash.png'
    icon_background_color_dark: '#c80064'
    image_dark: 'assets/image/splash.png'

# The following section is specific to Flutter packages.
flutter:

  # 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/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # 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/custom-fonts/#from-packages

ad_mob.dart

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

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-0000000000000000/0000000000';
      _isAdMob = true;
    }
    if (_isAdMob) {
      _adMobBanner = BannerAd(
        adUnitId: adBannerUnitId,
        size: AdSize.banner,
        request: const AdRequest(),
        listener: const BannerAdListener(),
      );
    }
  }
  void load() {
    if (_isAdMob) {
      _adMobBanner.load();
    }
  }
  void dispose() {
    if (_isAdMob) {
      _adMobBanner.dispose();
    }
  }
  Widget getAdBannerWidget() {
    if (_isAdMob) {
      return Container(
        alignment: Alignment.center,
        width: _adMobBanner.size.width.toDouble(),
        height: _adMobBanner.size.height.toDouble(),
        child: AdWidget(ad: _adMobBanner),
      );
    } else {
      return Container();
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

audio_play.dart

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

import 'package:just_audio/just_audio.dart';

import 'package:galmoji/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.audioHiyokos[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();
  }
}

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 prefBackImageFlag = 'backImageFlag';
  static const String prefSoundVolume = 'soundVolume';
  //image
  static const List<String> imageBackGrounds = [
    'assets/image/back001.webp',
    'assets/image/back002.webp',
    'assets/image/back003.webp',
    'assets/image/back004.webp',
    'assets/image/back005.webp',

	////////////
	// 非公開 //
	////////////


  ];
  //color
  static const Color colorButtonBack = Color.fromRGBO(255,255,255,0.7);
  static const Color colorButtonBackOn = Color.fromRGBO(255,255,255,0.4);
  static const Color colorHeader = Color.fromRGBO(200,0,100,0.5);
  static const Color colorSettingAccent = Color.fromRGBO(200,0,100,0.5);
  static const Color colorUiActiveColor = Color.fromRGBO(200,0,100,1);
  static const Color colorUiInactiveColor = Colors.black26;
  //sound
  static const String audioZero = 'assets/sound/zero.wav';    //無音1秒
  static const List<String> audioHiyokos = [
    'assets/sound/hiyoko1.wav',

	////////////
	// 非公開 //
	////////////

  ];
  //string
  static const Map<String,List<String>> galCharKana = {
    '現':['王見'],

	////////////
	// 非公開 //
	////////////

  };
  static const Map<String,List<String>> galCharAlphabet = {
    'A':['闩','月','Д','д','@','Å','∀'],

	////////////
	// 非公開 //
	////////////

  };

}

main.dart

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

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:share_plus/share_plus.dart';

//自身で作成したclassを読み込む
import 'package:galmoji/const_value.dart';
import 'package:galmoji/version_state.dart';
import 'package:galmoji/setting.dart';
import 'package:galmoji/ad_mob.dart';
import 'package:galmoji/page_state.dart';
import 'package:galmoji/preferences.dart';
import 'package:galmoji/audio_play.dart';

void main() {
  runApp(const MainApp());
}

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

class _MainAppState extends State<MainApp> {
  @override
  Widget build(BuildContext context) {
    MobileAds.instance.initialize();
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MainHomePage(),
    );
  }
}

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

class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
  final AdMob _adMob = AdMob(); //広告表示
  final AudioPlay _audioPlay = AudioPlay();
  bool _backImageFlag = true;
  final TextEditingController _textEditingController = TextEditingController();
  late AnimationController _animationController;
  late Animation<double> _opacityAnimation;
  int _backImageNumber = 0;
  int _lastBackImageNumber = 0;
  bool _convertKana = true;
  bool _convertAlphabet = false;
  Color _buttonBackColorRegeneration = ConstValue.colorButtonBack;
  Color _buttonBackColorCopyClipboard = ConstValue.colorButtonBack;
  Color _buttonBackColorShare = ConstValue.colorButtonBack;
  late Random _random;
  String _inputText = '';
  String _convertedText = '';

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }
  //ページ起動開始時に一度だけ呼ばれる
  @override
  void initState() {
    super.initState();
    _getVersion();
    _adMob.load();
    _audioPlay.playZero();
    int seed = (DateTime.now()).millisecondsSinceEpoch;
    _random = Random(seed);
    //background animation
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    _opacityAnimation = Tween<double>(begin: 0, end: 1).animate(_animationController);
    _animationController.addListener(() {
      setState(() {});
    });
    //
    (() async {
      await Preferences.initial();
      _backImageFlag = Preferences.backImageFlag;
      _audioPlay.soundVolume = Preferences.soundVolume;
    })();
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    _textEditingController.dispose();
    _animationController.dispose();
    super.dispose();
  }
  Widget _textFieldInput() {
    return Container(
      decoration: BoxDecoration(
        color: ConstValue.colorButtonBack,
        borderRadius: BorderRadius.circular(10.0),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: TextField(
          controller: _textEditingController,
          keyboardType: TextInputType.multiline,
          maxLines: null,
          onChanged: (text) {
            _inputText = text;
            _conversion();
          },
          decoration: const InputDecoration(
            labelText: '文章を入力',
            border: OutlineInputBorder(),
          )
        )
      )
    );
  }
  Widget _toggleKana() {
    return Expanded(
      child: Container(
        decoration: BoxDecoration(
          color: ConstValue.colorButtonBack,
          borderRadius: BorderRadius.circular(10.0),
        ),
        child: Padding(
          padding: const EdgeInsets.only(top: 1, left: 8, right: 8, bottom: 1),
          child: Row(children:<Widget>[
            const Expanded(
              child: Text('かなカナ漢字を変換',style: TextStyle(fontSize: 12)),
            ),
            Switch(
              value: _convertKana,
              onChanged: (bool value) {
                _audioPlay.play01();
                _backImageChange();
                setState(() {
                  _convertKana = value;
                  _conversion();
                });
              },
              activeColor: ConstValue.colorUiActiveColor,
              inactiveThumbColor: ConstValue.colorUiInactiveColor,
            ),
          ]),
        )
      )
    );
  }
  Widget _toggleAlphabet() {
    return Expanded(
      child: Container(
        decoration: BoxDecoration(
          color: ConstValue.colorButtonBack,
          borderRadius: BorderRadius.circular(10.0),
        ),
        child: Padding(
          padding: const EdgeInsets.only(top: 1, left: 8, right: 8, bottom: 1),
          child: Row(children:<Widget>[
            const Expanded(
              child: Text('英大文字を変換',style: TextStyle(fontSize: 12)),
            ),
            Switch(
              value: _convertAlphabet,
              onChanged: (bool value) {
                _audioPlay.play01();
                _backImageChange();
                setState(() {
                  _convertAlphabet = value;
                  _conversion();
                });
              },
              activeColor: ConstValue.colorUiActiveColor,
              inactiveThumbColor: ConstValue.colorUiInactiveColor,
            ),
          ]),
        )
      )
    );
  }
  Widget _textFieldResult() {
    return Container(
      decoration: BoxDecoration(
        color: ConstValue.colorButtonBack,
        borderRadius: BorderRadius.circular(10.0),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: TextField(
          readOnly: true,
          keyboardType: TextInputType.multiline,
          maxLines: null,
          controller: TextEditingController(
            text: _convertedText,
          ),
          decoration: const InputDecoration(
            labelText: '変換後',
            border: OutlineInputBorder(),
          ),
        )
      )
    );
  }
  //再生成
  Widget _regenerationButton() {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
        child: GestureDetector(
          onTap: () {
            _audioPlay.play01();
            _conversion();
            _backImageChange();
          },
          onTapDown: (TapDownDetails details) {
            setState(() {
              _buttonBackColorRegeneration = ConstValue.colorButtonBackOn;
            });
          },
          onTapUp: (TapUpDetails details) {
            setState(() {
              _buttonBackColorRegeneration = ConstValue.colorButtonBack;
            });
          },
          child: Container(
            padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
            color: _buttonBackColorRegeneration,
            child: const Center(
              child: Text('再生成',
                style: TextStyle(
                  fontSize: 12,
                )
              )
            )
          )
        )
      )
    );
  }
  //クリップボードにコピーボタン
  Widget _copyClipboardButton() {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.fromLTRB(3, 0, 0, 0),
        child: GestureDetector(
          onTap: () {
            _audioPlay.play01();
            if (_convertedText.isNotEmpty) {
              FocusScope.of(context).unfocus();
              Clipboard.setData(ClipboardData(text: _convertedText));
              showToast('コピーしました');
            }
          },
          onTapDown: (TapDownDetails details) {
            setState(() {
              _buttonBackColorCopyClipboard = ConstValue.colorButtonBackOn;
            });
          },
          onTapUp: (TapUpDetails details) {
            setState(() {
              _buttonBackColorCopyClipboard = ConstValue.colorButtonBack;
            });
          },
          child: Container(
            padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
            color: _buttonBackColorCopyClipboard,
            child: const Center(
              child: Text('コピー',
                style: TextStyle(
                  fontSize: 12,
                )
              )
            )
          )
        )
      )
    );
  }
  //送るボタン
  Widget _shareButton() {
    return Expanded(
      child: Container(
        padding: const EdgeInsets.fromLTRB(3,0,0,0),
        child: GestureDetector(
          onTap: () {
            _audioPlay.play01();
            if (_convertedText.isNotEmpty) {
              Share.share(_convertedText);
            }
          },
          onTapDown: (TapDownDetails details) {
            setState(() {
              _buttonBackColorShare = ConstValue.colorButtonBackOn;
            });
          },
          onTapUp: (TapUpDetails details) {
            setState(() {
              _buttonBackColorShare = ConstValue.colorButtonBack;
            });
          },
          child: Container(
            padding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
            color: _buttonBackColorShare,
            child: const Center(
              child: Text('送る',
                style: TextStyle(
                  fontSize: 12,
                )
              )
            )
          )
        )
      )
    );
  }
  //画面全体
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: const BoxDecoration(
        color: Color.fromRGBO(240,240,240,1)
      ),
      child: Container(
        decoration: _decoration2(),
        child: Container(
          decoration: _decoration1(),
          child: Scaffold(
            backgroundColor: Colors.transparent,
            appBar: AppBar(
              //タイトル表示
              title: const Text('ギャル文字',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 15.0,
                )
              ),
              foregroundColor: const Color.fromRGBO(255,255,255,1),
              backgroundColor: ConstValue.colorHeader,
              actions: <Widget>[
                TextButton(
                  onPressed: () async {
                    bool? ret = await Navigator.of(context).push(
                      MaterialPageRoute<bool>(
                        builder: (context) => const SettingPage(),
                      ),
                    );
                    //awaitで呼び出しているので、settingから戻ったら以下が実行される。
                    if (ret!) { //設定で適用だった場合
                      _backImageFlag = Preferences.backImageFlag;
                      _audioPlay.soundVolume = Preferences.soundVolume;
                      setState(() {});
                    }
                  },
                  child: const Text('設定',
                    style: TextStyle(
                      color: Colors.white,
                    )
                  )
                )
              ]
            ),
            body: SafeArea(
              child: Stack(children:[
                Column(children:[
                  Expanded(
                    child: GestureDetector(
                      onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
                      child: SingleChildScrollView(
                        child: Padding(
                          padding: const EdgeInsets.all(4.0),
                          child: Column(children:[
                            _textFieldInput(),
                            const SizedBox(height:5),
                            Row(children:[
                              _toggleKana(),
                              const SizedBox(width:8),
                              _toggleAlphabet(),
                            ]),
                            const SizedBox(height:5),
                            _textFieldResult(),
                            const SizedBox(height:5),
                            Row(children:[
                              _regenerationButton(),
                              _copyClipboardButton(),
                              _shareButton(),
                            ])
                          ]),
                        )
                      )
                    )
                  ),
                  //広告
                  Padding(
                    padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
                    child: SizedBox(
                      width: double.infinity,
                      child: _adMob.getAdBannerWidget(),
                    )
                  )
                ])
              ])
            )
          )
        )
      )
    );
  }
  //背景画像前側
  Decoration _decoration1() {
    if (_backImageFlag) {
      return BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageBackGrounds[_backImageNumber]),
          fit: BoxFit.cover,
          colorFilter: ColorFilter.mode(
            Colors.black.withOpacity(_opacityAnimation.value),
            BlendMode.dstATop,
          ),
        )
      );
    } else {
      return const BoxDecoration();
    }
  }
  //背景画像後ろ側
  Decoration _decoration2() {
    if (_backImageFlag) {
      return BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageBackGrounds[_lastBackImageNumber]),
          fit: BoxFit.cover,
        ),
      );
    } else {
      return const BoxDecoration();
    }
  }
  void _backImageChange() {
    final int second = (DateTime.now()).millisecondsSinceEpoch ~/ 1000 ~/ 3;  //3秒ごとに変化
    _backImageNumber = second % 100;
    _animationController.forward();
    Future.delayed(const Duration(milliseconds: 600), () {
      _lastBackImageNumber = _backImageNumber;
      _animationController.reverse();
    });
  }
  void _conversion() {
    String text = _inputText;
    Map<String, String> stock = {};
    int stockP = 1;
    if (_convertKana) {
      for (String key in ConstValue.galCharKana.keys) {
        String proxy = '#%%1$stockP%%#';
        text = text.replaceAll(key, proxy);
        List<String> valuesForKey = ConstValue.galCharKana[key]!;
        int p = _random.nextInt(valuesForKey.length);
        String to = valuesForKey[p];
        stock[proxy] = to;
        stockP += 1;
      }
    }
    if (_convertAlphabet) {
      for (String key in ConstValue.galCharAlphabet.keys) {
        String proxy = '#%%2$stockP%%#';
        text = text.replaceAll(key, proxy);
        List<String> valuesForKey = ConstValue.galCharAlphabet[key]!;
        int p = _random.nextInt(valuesForKey.length);
        String to = valuesForKey[p];
        stock[proxy] = to;
        stockP += 1;
      }
    }
    for (String key in stock.keys) {
      text = text.replaceAll(key, stock[key]!);
    }
    setState(() {
      _convertedText = text;
    });
  }
  void showToast(String message) {
    Fluttertoast.showToast(
      msg: message,
      toastLength: Toast.LENGTH_SHORT, // または Toast.LENGTH_LONG
      gravity: ToastGravity.BOTTOM, // トーストの表示位置
      timeInSecForIosWeb: 1, // iOSおよびWebの場合の表示時間
      backgroundColor: Colors.black87, // 背景色
      textColor: Colors.white, // テキスト色
      fontSize: 16.0, // テキストサイズ
    );
  }
}

page_state.dart

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

//現在のページを記録。initStateでタイミングが合わない時にbuild内で一度だけ実行させるために使用。
class PageState {

  static String _currentPage = '';

  static void setCurrentPage(String str) {
    _currentPage = str;
  }

  static String getCurrentPage() {
    return _currentPage;
  }

}

preferences.dart

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

import 'package:shared_preferences/shared_preferences.dart';

import 'package:galmoji/const_value.dart';

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

  static bool ready = false;

  //この値は常に最新にしておく
  static bool _backImageFlag = true;
  static double _soundVolume = 0.3;

  static bool get backImageFlag {
    return _backImageFlag;
  }
  static double get soundVolume {
    return _soundVolume;
  }

  static Future<void> initial() async {
    _backImageFlag = await getBackImageFlag();
    _soundVolume = await getSoundVolume();
    ready = true;
  }

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

  //背景画像On/Off
  static Future<void> setBackImageFlag(bool flag) async {
    _backImageFlag = flag;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(ConstValue.prefBackImageFlag, flag);
  }
  static Future<bool> getBackImageFlag() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final bool flag = prefs.getBool(ConstValue.prefBackImageFlag) ?? 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;
  }

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

}

setting.dart

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

import 'package:flutter/material.dart';

import 'package:galmoji/const_value.dart';
import 'package:galmoji/preferences.dart';
import 'package:galmoji/version_state.dart';
import 'package:galmoji/ad_mob.dart';
import 'package:galmoji/page_state.dart';

class SettingPage extends StatefulWidget {
  const SettingPage({super.key});

  @override
  State<SettingPage> createState() => _SettingPageState();
}

class _SettingPageState extends State<SettingPage> {
  final AdMob _adMob = AdMob(); //広告
  //これら変数はUIへの表示や入力の為に一時的に使用される。
  bool _backImageFlag = true;
  double _soundVolume = 0.0;

  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _adMob.load();
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    //このページが開いたときに一度だけ実行される処理を記述。initStateではタイミングが合わない為。
    if (PageState.getCurrentPage() != 'setting') {
      PageState.setCurrentPage('setting');
      (() async {
        await Preferences.initial();
        _backImageFlag = Preferences.backImageFlag;
        _soundVolume = Preferences.soundVolume;
        setState((){});
      })();
    }
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        //設定キャンセルボタン
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false); //falseを返す
          },
        ),
        title: const Text('設定'),
        foregroundColor: const Color.fromRGBO(255,255,255,1),
        backgroundColor: ConstValue.colorSettingAccent,
        actions: [
          //設定OKボタン
          IconButton(
            icon: const Icon(Icons.check),
            onPressed: () async {
              await Preferences.setBackImageFlag(_backImageFlag);
              await Preferences.setSoundVolume(_soundVolume);
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);  //trueを返す
            },
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 16),
                    child: Row(children:<Widget>[
                      const Expanded(
                        child: Text('背景画像表示',style: TextStyle(fontSize: 16)),
                      ),
                      Switch(
                        value: _backImageFlag,
                        onChanged: (bool value) {
                          setState(() {
                            _backImageFlag = value;
                          });
                        },
                        activeColor: ConstValue.colorUiActiveColor,
                        inactiveThumbColor: ConstValue.colorUiInactiveColor,
                      ),
                    ]),
                  ),
                  _border(),
                  const Padding(
                    padding: EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text('効果音量',style: TextStyle(fontSize: 16)),
                      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.colorUiActiveColor,
                          inactiveColor: ConstValue.colorUiInactiveColor,
                        )
                      )
                    ])
                  ),
                  _border(),
                  const Padding(
                    padding: EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 24),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children:[
                        Text('ギャル文字変換'),
                        SizedBox(height:15),
                        Text('日本語文字列を入力欄に入力またはペーストします。ギャル文字への変換結果がリアルタイムに表示されます。'),
                        SizedBox(height:15),
                        Text('かなカナ漢字は適宜変換されます。英字は大文字のみです。'),
                        SizedBox(height:15),
                        Text('ギャル文字とは、2002年~2005年ぐらいに女子中学生や女子高生の間で流行となった文字遊び。'),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
                    child: SizedBox(
                      child: Text('version  ${VersionState.versionLoad()}',
                        style: const TextStyle(
                          fontSize: 10,
                        ),
                      ),
                    ),
                  ),
                ]),
              ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
          child: SizedBox(
            width: double.infinity,
            child: _adMob.getAdBannerWidget(),
          ),
        ),
      ]),
    );
  }
  //UIの仕切り用ボーダーライン
  Widget _border() {
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }

}

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;
  }

}