ソースコード source code

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

● ご注意 ● このページで公開しているのはDartコードのみです。アプリの動作には画像・音声・多言語テキストなどの追加データが必要なため、ここに掲載している情報だけではアプリを完全に再現することはできません。 ● アプリは継続的に機能拡張しているため、ストアで公開している最新版とコード内容が異なる場合があります。 ● コードはコピーして自由にご利用いただけますが、著作権は放棄していません。そのため、コード全体の再掲載はご遠慮ください。一部の引用や改変しての再掲載は問題ありません。個人利用・商用利用を問わず、アプリ開発の参考として自由にお使いください。 ● このコードは「お手本」として公開しているものではありません。ミニアプリとして作成しているため、変数名など細部に配慮していない箇所や誤りが含まれる可能性があります。参考程度にご覧ください。 ● 他の開発者の公開コードを参考にした部分も含まれています。アプリ開発の熟練者が書いたコードではない点もご了承ください。 ● ここはエンジニア向けの技術情報共有サービスではないため、詳細な説明は省略しています。GitHub などでの公開予定はありません。 ● 本コードは無保証であり、動作や品質を保証するものではありません。
Copyright© エーオーシステム株式会社

● Notice ● Only the Dart source code is provided on this page. Additional assets such as images, audio files, and multilingual text are required for the application to function. Therefore, the information here alone is not sufficient to fully reproduce the app. ● The app is continuously being improved, so the code published here may differ from the latest version available on the app stores. ● You are free to copy and use the code, but the copyright is not waived. For this reason, please refrain from redistributing (reposting) the entire code. Partial quotation or redistribution of modified portions is allowed. You may use the code freely for reference in your own app development, whether for personal or commercial use. ● This code is not intended to serve as a coding example or best practice. As this is a small sample app, some variable names and details may lack refinement, and there may be mistakes. Please use it only as a reference. ● Some parts of the code are based on publicly available examples from other developers. Please note that this code was not written by an expert in Android app development. ● This is not a technical knowledge-sharing service for engineers, so detailed explanations are omitted. There are no plans to publish the code on GitHub. ● The code is provided "as is" without any warranty of any kind.
Copyright© ao-system, Inc.

下記コードの最終ビルド日: 2025-03-09

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.1+2

environment:
  sdk: ^3.8.0-148.0.dev

# 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.8
  package_info_plus: ^8.3.0
  shared_preferences: ^2.0.17
  google_mobile_ads: ^5.3.1
  just_audio: ^0.9.36
  fluttertoast: ^8.2.4

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.14.3    #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: ^5.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: '#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/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

ad_manager.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:galmoji/ad_unit_id.dart';

class AdManager {

  BannerAd? _bannerAd;
  BannerAd? get bannerAd => _bannerAd;

  Future<void> loadBannerAd(VoidCallback onAdLoaded) async {
    try {
      BannerAd bannerAd = BannerAd(
        adUnitId: AdUnitId.bannerAdUnitId,
        request: AdRequest(),
        size: AdSize.banner,
        listener: BannerAdListener(
          onAdLoaded: (ad) {
            _bannerAd = ad as BannerAd;
            onAdLoaded();
          },
          onAdFailedToLoad: (ad, err) {
            ad.dispose();
            onAdLoaded();
            //debugPrint('Failed to load a banner ad: $err');
          },
        ),
      );
      await bannerAd.load();
    } catch (e) {
      onAdLoaded();
      //debugPrint('Error loading banner ad: $e');
    }
  }

  Widget widget() {
    if (_bannerAd != null) {
      return Padding(
          padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
          child: Align(
            alignment: Alignment.topCenter,
            child: SizedBox(
              width: _bannerAd!.size.width.toDouble(),
              height: _bannerAd!.size.height.toDouble(),
              child: AdWidget(ad: _bannerAd!),
            ),
          )
      );
    } else {
      return Container();
    }
  }

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

}

ad_unit_id.dart

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

import 'dart:io';

class AdUnitId {

  static String get bannerAdUnitId {
    if (Platform.isAndroid) {
      //return 'ca-app-pub-3940256099942544/6300978111';  //test
      return '<YOUR_ANDROID_INTERSTITIAL_AD_UNIT_ID>';
    } else if (Platform.isIOS) {
      //return 'ca-app-pub-3940256099942544/2934735716';  //test
      return '<YOUR_ANDROID_INTERSTITIAL_AD_UNIT_ID>';
    } else {
      throw UnsupportedError('Unsupported platform');
    }
  }

  static String get interstitialAdUnitId {
    if (Platform.isAndroid) {
      //return 'ca-app-pub-3940256099942544/1033173712';  //test
      return '<YOUR_ANDROID_INTERSTITIAL_AD_UNIT_ID>';
    } else if (Platform.isIOS) {
      //return 'ca-app-pub-3940256099942544/4411468910';  //test
      return '<YOUR_IOS_INTERSTITIAL_AD_UNIT_ID>';
    } else {
      throw UnsupportedError('Unsupported platform');
    }
  }

  static String get rewardedAdUnitId {
    if (Platform.isAndroid) {
      //return 'ca-app-pub-3940256099942544/5224354917';  //test
      return '<YOUR_ANDROID_REWARDED_AD_UNIT_ID>';
    } else if (Platform.isIOS) {
      //return 'ca-app-pub-3940256099942544/1712485313';  //test
      return '<YOUR_IOS_REWARDED_AD_UNIT_ID>';
    } else {
      throw UnsupportedError('Unsupported platform');
    }
  }
}

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

main.dart

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

import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:package_info_plus/package_info_plus.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_manager.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) {
    if (kIsWeb == false) {
      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 AdManager _adManager = AdManager();
  final AudioPlay _audioPlay = AudioPlay();
  bool _isInitialized = false;
  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();
    _initializeAsync();
    _adManager.loadBannerAd(() {
      setState(() {});
    });
  }

  void _initializeAsync() async {
    _getVersion();
    _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(() {});
    });
    //
    await Preferences.initial();
    _backImageFlag = Preferences.backImageFlag;
    _audioPlay.soundVolume = Preferences.soundVolume;
    setState(() {
      _isInitialized = true;
    });
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    _adManager.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) {
    if (!_isInitialized) {
      return const Center(
          child: CircularProgressIndicator() //初期化中のローディングインジケーターを表示
      );
    }
    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(),
                            ])
                          ]),
                        )
                      )
                    )
                  ),
                  //広告
                  _adManager.widget()
                ])
              ])
            )
          )
        )
      )
    );
  }
  //背景画像前側
  Decoration _decoration1() {
    if (_backImageFlag) {
      return BoxDecoration(
        image: DecorationImage(
          image: AssetImage(ConstValue.imageBackGrounds[_backImageNumber]),
          fit: BoxFit.cover,
          colorFilter: ColorFilter.mode(
            Color.alphaBlend(
              Colors.black.withAlpha((_opacityAnimation.value * 255).toInt()),
              Colors.transparent,
            ),
            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 % ConstValue.imageBackGrounds.length;
    _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, // テキストサイズ
    );
  }
}

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_manager.dart';

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

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

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

  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _initializeAsync();
    _adManager.loadBannerAd(() {
      setState(() {});
    });
  }
  void _initializeAsync() async {
    await Preferences.initial();
    _backImageFlag = Preferences.backImageFlag;
    _soundVolume = Preferences.soundVolume;
    setState(() {
      _isInitialized = true;
    });
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    _adManager.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    if (!_isInitialized) {
      return const Center(
          child: CircularProgressIndicator() //初期化中のローディングインジケーターを表示
      );
    }
    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: SafeArea(
        child: 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,
                          ),
                        ),
                      ),
                    ),
                  ]),
                ),
              ),
            ),
          ),
          //広告
          _adManager.widget()
        ]),
      )
    );
  }
  //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;
  }

}