広告 Flutter Dart Unity アプリ開発

医療用の日数・点滴速度・BMI計算アプリの試作(多言語対応版)

概要

  • 生年月日をカレンダーで入力し、本日または指定した日付での年齢を計算する。和暦と西暦の対応表を閲覧でき、タップすると指定した年の1月1日が生年月日にセットされる。
  • 2つの選択日間の日数計算も行う。2泊3日を3とカウントするようにした。
    カレンダ入力時に時刻が混入し、日数計算が狂うことがあったので、時刻をクリアするようにした。
  • 点滴速度計算機能を追加した。
  • BMI計算機能を追加した。
  • 初期値の設定画面、英語・日本語切り替え機能を追加した。
  • 数値と日付のみ入力可能とした。

使用しているパッケージ

flutter pub add flutter_localization
flutter pub add intl
flutter pub add age_calculator
flutter pub add shared_preferences
flutter pub add flutter_launcher_icons
flutter pub add audioplayers

flutter_localizationsパッケージは廃止され、破壊的更新で、上記「flutter_localization」になりました。
StatefulWidgetが二重構造になっています。使用方法が完全に変わりました。

サンプルソース

以下のファイルを全てlibフォルダに格納してから、上記パッケージを導入しましょう。
flutter build apk でコンパイルしたものをAndroidスマホにインストールして、問題なくアプリが動くところまで確認しています。

assets/sound/pi2.mp3 等の音源ファイルも必要です。0.3秒くらいのmp3ファイルであれば何でもOK。

applocate.dart (多言語対応のコンテンツ集)

//applocale.dart

mixin AppLocale {
  static const String title = 'title';
  static const String thisIs = 'thisIs';

  static const String menuDripSound='menuDripSound';
  static const String menuBMI='menuBMI';
  static const String menuGengoList='menuGengoList';
  static const String menuSetting='menuSetting';
  
  static const String age = 'age';
  static const String birthday = 'birthday';
  static const String selectDate1 = 'selectDate1(Today)';
  static const String selectDate2 = 'selectDate2';
  static const String addDay = 'addDay';
  static const String fromSelectDate2 = 'fromSelectDate2';
  static const String dayWarning = 'dayWarning';

  static const String meiji = 'meiji';
  static const String taisho = 'taisho';
  static const String showa = 'showa';
  static const String heisei = 'heisei';
  static const String reiwa = 'reiwa';

  static const String dripSoundTitle = 'dripSoundTitle';
  static const String dripSoundVolume = 'dripSoundVolume';
  static const String dripSoundType60 = 'dripSoundType60';
  static const String dripSoundType20 = 'dripSoundType20';
  static const String dripSoundTimeHour = 'dripSoundTimeHour';
  static const String dripSoundTimeMin = 'dripSoundTimeMin';
  static const String dripSoundSpeed = 'dripSoundSpeed';
  static const String dripSoundSpeed2 = 'dripSoundSpeed2';
  static const String dripSoundError = 'dripSoundError';
  static const String dripSoundChange = 'dripSoundChange';

  static const String bmiHeight = 'bmiHeight';
  static const String bmiWeight = 'bmiWeight';


  static const Map<String, dynamic> EN = {
    title: 'Calc Date',
    thisIs: 'This is %a package, version %a.',
  
    menuDripSound: 'Drip Sound',
    menuBMI:'BMI Calc',
    menuGengoList:'Year (japanese) List',
    menuSetting: 'Setting',
   
    age: 'Age',
    birthday: 'Birthday',
    selectDate1: 'SelectDate1',
    selectDate2: 'SelectDate2',
    addDay: 'Add Days',
    fromSelectDate2: 'Days between D1 and D2',
    dayWarning: '[2 nights and 3 days = 3]',

    meiji: 'M',
    taisho: 'T',
    showa: 'S',
    heisei: 'H',
    reiwa: 'R',

    dripSoundTitle: 'Drip Sound',
    dripSoundVolume: 'Volume',
    dripSoundType60: '60drip/1mL',
    dripSoundType20: '20drip/1mL',
    dripSoundTimeHour: 'Hour',
    dripSoundTimeMin: 'Min',
    dripSoundSpeed: 'Drip Rate',
    dripSoundSpeed2: 'Drip Interval(sec)',
    dripSoundError: 'Cannot Play (over 120 drop!)',
    dripSoundChange: 'Change',

    bmiHeight:'Height',
    bmiWeight:'Weight',
  };

  static const Map<String, dynamic> JA = {
    title: '日付計算(Calc Date)',
    thisIs: 'これは%aパッケージ、バージョン%aです。',

    menuDripSound: '点滴速度',
    menuBMI:'BMI計算',
    menuGengoList:'西暦(元号) 対応表',
    menuSetting: '初期値設定',

    age: '満年齢',
    birthday: '生年月日',
    selectDate1: '選択日1(本日)',
    selectDate2: '選択日2',
    addDay: '追加日数',
    fromSelectDate2: '選択日1〜2の日数',
    dayWarning: '[注意:2泊3日 ⇒ 3日]',

    meiji: '明治',
    taisho: '大正',
    showa: '昭和',
    heisei: '平成',
    reiwa: '令和',

    dripSoundTitle: '点滴速度(音付き)',
    dripSoundVolume: '点滴残量',
    dripSoundType60: '60滴(小児)',
    dripSoundType20: '20滴(成人)',
    dripSoundTimeHour: '時間',
    dripSoundTimeMin: '分',
    dripSoundSpeed: '滴下速度',
    dripSoundSpeed2: '滴下間隔(秒)',
    dripSoundError: '速すぎて音が出せません(120滴まで)',
    dripSoundChange: '変更',

    bmiHeight:'身長',
    bmiWeight:'体重',
  };
}

dialogutil.dartは、以下のURLで紹介されたものを参考に、色々改良しています。(カレンダコントロール呼び出し追加等)
Flutter 逆引き辞典 Chapter 16 ダイアログでもテキスト入力がしたい
https://zenn.dev/pressedkonbu/books/flutter-reverse-lookup-dictionary/viewer/016-input-text-on-dialog

dialogutil.dart

//dialogutil.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:flutter_localization/flutter_localization.dart';

class DialogUtils {
  DialogUtils._();
  final FlutterLocalization localization = FlutterLocalization.instance;

  /// タイトルのみを表示するシンプルなダイアログを表示する
  static Future<void> showOnlyTitleDialog(
    BuildContext context,
    String title,
  ) async {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(title: Text(title));
      },
    );
  }

  /// 入力した文字列を返すダイアログを表示する
  static Future<String?> showEditingDialog(
    BuildContext context,
    String text,
  ) async {
    return showDialog<String>(
      context: context,
      builder: (context) {
        return TextEditingDialog(text: text);
      },
    );
  }

  // カレンダーで日付を選択するダイアログボックス(yyyy-MM-dd文字列を返す)
  static Future<String?> showCalender(
    BuildContext context,
    String sDate,
  ) async {
    final FlutterLocalization localization = FlutterLocalization.instance;
    String sLoc = localization.currentLocale.localeIdentifier.substring(
      0,
      2,
    ); // 頭2文字
    debugPrint(sLoc);
    DateTime dt = DateTime.parse(sDate);
    DateFormat dateF = DateFormat('yyyy-MM-dd'); // 時刻を消すために使用

    DateTime? picked = await showDatePicker(
      context: context,
      initialDate: dt,
      firstDate: DateTime(DateTime.now().year - 120),
      lastDate: DateTime(DateTime.now().year + 120),
      locale: Locale(sLoc),
      keyboardType: TextInputType.text,
    );

    DateTime d = picked ?? dt;
    String sDate2 = dateF.format(d);
    return sDate2;
  }
}

/// 状態を持ったダイアログ
class TextEditingDialog extends StatefulWidget {
  const TextEditingDialog({super.key, this.text});
  final String? text;

  @override
  State<TextEditingDialog> createState() => _TextEditingDialogState();
}

class _TextEditingDialogState extends State<TextEditingDialog> {
  final controller = TextEditingController();
  final focusNode = FocusNode();
  @override
  void dispose() {
    controller.dispose();
    focusNode.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    // TextFormFieldに初期値を代入する
    controller.text = widget.text ?? '';
    focusNode.addListener(() {
      // フォーカスが当たったときに文字列が選択された状態にする
      if (focusNode.hasFocus) {
        controller.selection = TextSelection(
          baseOffset: 0,
          extentOffset: controller.text.length,
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      content: TextFormField(
        autofocus: true, // ダイアログが開いたときに自動でフォーカスを当てる
        focusNode: focusNode,
        controller: controller,
        keyboardType: TextInputType.number,
        inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.]'))],
        onFieldSubmitted: (_) {
          // エンターを押したときに実行される
          Navigator.of(context).pop(controller.text);
        },
      ),
      actions: [
        TextButton(
          onPressed: () {
            debugPrint(controller.text);
            Navigator.of(context).pop(controller.text);
          },
          child: const Text('OK'),
        ),
      ],
    );
  }
}

gengo.dart

//gengo.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_localization/flutter_localization.dart';

import 'applocale.dart';

// DateTimeを渡すと、元号のStringを返す
String gengo(DateTime dt, BuildContext context) {
  String sGengo = '';

  if ((dt.year == 2019) && (dt.month < 5)) {
    sGengo = "${AppLocale.heisei.getString(context)}31";
  } else if ((dt.year == 1989) && (dt.month < 2) && (dt.day < 8)) {
    sGengo = "${AppLocale.showa.getString(context)}64";
  } else if ((dt.year == 1926) && (dt.month < 13) && (dt.day < 26)) {
    sGengo = "${AppLocale.taisho.getString(context)}15";
  } else if ((dt.year == 1926) && (dt.month < 12)) {
    sGengo = "${AppLocale.taisho.getString(context)}15";
  } else if ((dt.year == 1868) && (dt.month < 8) && (dt.day < 31)) {
    sGengo = "${AppLocale.meiji.getString(context)}45";
  } else if ((dt.year == 1868) && (dt.month < 7)) {
    sGengo = "${AppLocale.meiji.getString(context)}45";
  } else if (dt.year > 2018) {
    sGengo = "${AppLocale.reiwa.getString(context)}${dt.year - 2018}";
  } else if (dt.year > 1988) {
    sGengo = "${AppLocale.heisei.getString(context)}${dt.year - 1988}";
  } else if (dt.year > 1925) {
    sGengo = "${AppLocale.showa.getString(context)}${dt.year - 1925}";
  } else if (dt.year > 1911) {
    sGengo = "${AppLocale.taisho.getString(context)}${dt.year - 1911}";
  } else {
    sGengo = "${AppLocale.meiji.getString(context)}${dt.year - 1867}";
  }

  return sGengo;
}

// 日付Stringを返す
String sDate(DateTime dt) {
  final FlutterLocalization localization = FlutterLocalization.instance;
  String sDate=DateFormat.yMMMEd(localization.currentLocale.localeIdentifier).format(dt);

  return sDate;
}

//元号付きの日付Stringを返す
String sGDate(DateTime dt, BuildContext context) {
  return '(${gengo(dt, context)})${sDate(dt)}';
}

// 本日から120年前までさかのぼり、
// 指定したindexから指定個の西暦と元号の文字列リストを返す
List<String> mkGengoYearList(BuildContext context, int index, int count) {
  final List<String> gengoYear = [];
  DateTime dt = DateTime.now().subtract(Duration(days: 365 * (120 - index)));

  for (int i = 0; i <= count - 1; i++) {
    dt = dt.add(Duration(days: 365)); // だいたい1年
    gengoYear.add('${dt.year}(${gengo(dt, context)})');
  }

  return gengoYear;
}

gengolist.dart

//gengolist.dart

import 'package:flutter/material.dart';
import 'package:flutter_localization/flutter_localization.dart';

import 'applocale.dart';
import 'gengo.dart';

// 西暦と元号のリストを無限に表示する画面
class GengoListPage extends StatefulWidget {
  const GengoListPage({super.key});

  @override
  State<GengoListPage> createState() => _GengoListPageState();
}

class _GengoListPageState extends State<GengoListPage> {
  final _gengoYear = <String>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(AppLocale.menuGengoList.getString(context))),
      body: _buildGengo(),
    );
  }

  Widget _buildGengo() {
    return ListView.builder(
      padding: const EdgeInsets.all(16.0),
      itemBuilder: (context, i) {
        if (i.isOdd) return Divider();

        final index = i ~/ 2;
        if (index >= _gengoYear.length) {
          _gengoYear.addAll(mkGengoYearList(context, index, 10));
        }
        return _buildRow(_gengoYear[index]);
      },
    );
  }

  Widget _buildRow(String gengoYear) {
    return ListTile(
      onTap: () {
        debugPrint("$gengoYear is tapped.");
        Navigator.pop(context, gengoYear);
      },
      title: Text(gengoYear, style: _biggerFont),
    );
  }
}

dripsound.dart

//dripsound.dart

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

import 'applocale.dart';
import 'dialogutil.dart';

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

  @override
  State<DripSoundPage> createState() => _DripSoundPageState();
}

class _DripSoundPageState extends State<DripSoundPage> {
  int dripVolume = 100; //mL
  int dripTime = 30; // min
  int dripType = 20; // 60drip/1mL or 20drip/1mL
  bool dripIsPlaying = false;
  int counter = 0;
  Duration waitDuration = Duration(seconds: 1);

  late AudioPlayer player = AudioPlayer();

  // 設定値を取得
  void _readSetting() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // null なら、初期値文字列を渡す
    setState(() {
      dripVolume = int.parse(prefs.getString('dripVolume') ?? '100');
      dripTime = int.parse(prefs.getString('dripTime') ?? '30');
      dripType = int.parse(prefs.getString('dripType') ?? '20');
    });
  }

  // ウィジェットの初期化処理
  @override
  void initState() {
    super.initState();
    _readSetting();

    // Create the audio player.
    player = AudioPlayer();

    // Set the release mode to keep the source after playback has completed.
    player.setReleaseMode(ReleaseMode.stop);

    // Start the player as soon as the app is displayed.
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await player.setSource(AssetSource('sounds/pi2.mp3'));
      await player.resume();
    });
  }

  @override
  void dispose() {
    // Release all sources and dispose the player.
    player.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(AppLocale.menuDripSound.getString(context))),
      body: _buildDripSound(),
    );
  }

  void _beat(Timer t) async {
    if (dripIsPlaying) {
      await player.resume();
      setState(() {
        counter++;
      });
    } else {
      t.cancel();
      setState(() {
        counter = 0;
      });
    }
  }

  Widget _buildDripSound() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.dripSoundVolume.getString(context)} : $dripVolume mL',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  int num = int.parse(textBuf);
                  if (num > 0) {
                    setState(() {
                      dripVolume = num;
                      dripIsPlaying = false;
                    });
                  }
                },
                child: Text(
                  AppLocale.dripSoundChange.getString(context),
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${(dripTime / 60).toInt().toString()}:${dripTime % 60} ($dripTime ${AppLocale.dripSoundTimeMin.getString(context)})',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: () async {
                      String textBuf =
                          await DialogUtils.showEditingDialog(context, '') ??
                          '0';
                      debugPrint(textBuf);

                      int num = (double.parse(textBuf) * 60).toInt();
                      if (num > 0) {
                        setState(() {
                          dripTime = num;
                          dripIsPlaying = false;
                        });
                      }
                    },
                    child: Text(
                      AppLocale.dripSoundTimeHour.getString(context),
                      style: Theme.of(context).textTheme.bodyLarge,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () async {
                      String textBuf =
                          await DialogUtils.showEditingDialog(context, '') ??
                          '0';
                      debugPrint(textBuf);

                      int num = int.parse(textBuf);
                      if (num > 0) {
                        setState(() {
                          dripTime = num;
                          dripIsPlaying = false;
                        });
                      }
                    },
                    child: Text(
                      AppLocale.dripSoundTimeMin.getString(context),
                      style: Theme.of(context).textTheme.bodyLarge,
                    ),
                  ),
                ],
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Radio(
                value: 20,
                groupValue: dripType,
                onChanged: (value) {
                  setState(() {
                    dripType = value!;
                    dripIsPlaying = false;
                  });
                },
              ),
              Text(
                AppLocale.dripSoundType20.getString(context),
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              SizedBox(height: 20, width: 20),
              Radio(
                value: 60,
                groupValue: dripType,
                onChanged: (value) {
                  setState(() {
                    dripType = value!;
                    dripIsPlaying = false;
                  });
                },
              ),
              Text(
                AppLocale.dripSoundType60.getString(context),
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.dripSoundSpeed.getString(context)} : ${(dripVolume / dripTime * 60).toInt().toString()}mL/h',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              SizedBox(height: 20, width: 20),
              Text(
                '${AppLocale.dripSoundSpeed.getString(context)} : ${(dripVolume / dripTime * dripType).toInt().toString()}drip/min',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              SizedBox(height: 20, width: 20),
              Text(
                '${AppLocale.dripSoundSpeed2.getString(context)} : ${(60 / (dripVolume / dripTime * dripType)).toStringAsFixed(1)}sec',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),
          ElevatedButton(
            onPressed: () {
              if (!dripIsPlaying) {
                int dur =
                    (60 * 1000 / (dripVolume / dripTime * dripType)).toInt();
                if (dur >= 300) {
                  var duration = Duration(milliseconds: dur);
                  Timer.periodic(duration, (Timer t) => _beat(t));
                  setState(() {
                    waitDuration = Duration(milliseconds: dur);
                    dripIsPlaying = true;
                  });
                } else {
                  DialogUtils.showOnlyTitleDialog(
                    context,
                    AppLocale.dripSoundError.getString(context),
                  );
                }
              }
            },
            child: Text(
              'Sound Start',
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                dripIsPlaying = false;
              });
            },
            child: Text(
              'Sound Stop',
              style: Theme.of(context).textTheme.bodyLarge,
            ),
          ),

          Text(
            'Count : $counter',
            style: Theme.of(context).textTheme.bodyLarge,
          ),
        ],
      ),
    );
  }
}

bmi.dart

//bmi.dart

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

import 'applocale.dart';
import 'dialogutil.dart';

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

  @override
  State<BMIPage> createState() => _BMIPageState();
}

class _BMIPageState extends State<BMIPage> {
  double height = 172; //cm
  double weight = 74.5; //kg

  //shared_preferences
 
  // 設定値を取得
  void _readSetting() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // null なら、初期値文字列を渡す
    setState(() {
      height = double.parse(prefs.getString('height') ?? '172');
      weight = double.parse(prefs.getString('weight') ?? '72');
    });
  }
  // ウィジェットの初期化処理
  @override
  void initState() {
    super.initState();
    _readSetting();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('BMI')),
      body: _BMI(),
    );
  }

  Widget _BMI() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.bmiHeight.getString(context)} : ${height.toStringAsFixed(1)} cm',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  double ht = double.parse(textBuf);
                  if (ht > 0) {
                    setState(() {
                       height = ht;
                    });
                  }
                },
                child: Text(
                  AppLocale.bmiHeight.getString(context),
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.bmiWeight.getString(context)} : ${weight.toStringAsFixed(1)} kg',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  double wt = double.parse(textBuf);
                  if (wt > 0) {
                    setState(() {
                       weight = wt;
                    });
                  }
                },
                child: Text(
                  AppLocale.bmiWeight.getString(context),
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
            ],
          ),

          Text(
            'BMI = ${(weight*10000/height/height).toStringAsFixed(1)}',
                style: Theme.of(context).textTheme.bodyLarge,
          ),
          Text(
            'BMI(22) = ${(height*height*22/10000).toStringAsFixed(1)}kg',
                style: Theme.of(context).textTheme.bodyLarge,
          ),
          Text(
            'BMI(23) = ${(height*height*23/10000).toStringAsFixed(1)}kg',
                style: Theme.of(context).textTheme.bodyLarge,
          ),
          Text(
            'BMI(24) = ${(height*height*24/10000).toStringAsFixed(1)}kg',
                style: Theme.of(context).textTheme.bodyLarge,
          ),
        ],
      ),
    );
  }
}

main.dart

//main.dart

import 'package:flutter/material.dart';
import 'package:flutter_localization/flutter_localization.dart';

import 'applocale.dart';
import 'gengolist.dart';
import 'home.dart';
import 'dripsound.dart';
import 'bmi.dart';
import 'setting.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterLocalization.instance.ensureInitialized();
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final FlutterLocalization _localization = FlutterLocalization.instance;

  @override
  void initState() {
    _localization.init(
      mapLocales: [
        const MapLocale(
          'en',
          AppLocale.EN,
          countryCode: 'US',
          fontFamily: 'Font EN',
        ),
        const MapLocale(
          'ja',
          AppLocale.JA,
          countryCode: 'JP',
          fontFamily: 'Font JA',
        ),
      ],
      initLanguageCode: 'ja',
    );
    _localization.onTranslatedLanguage = _onTranslatedLanguage;
    super.initState();
  }

  void _onTranslatedLanguage(Locale? locale) {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      supportedLocales: _localization.supportedLocales,
      localizationsDelegates: _localization.localizationsDelegates,
      theme: ThemeData(
        fontFamily: _localization.fontFamily,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        textTheme: TextTheme(
          titleMedium: TextStyle(fontSize: 16),
          titleLarge: TextStyle(fontSize: 22),
          bodyMedium: TextStyle(fontSize: 22),
          bodyLarge: TextStyle(fontSize: 23),
        ),
      ),
      initialRoute: '/1',
      routes: {
        '/1': (context) => const MyHomePage(),
        '/2': (context) => const GengoListPage(),
        '/3': (context) => const DripSoundPage(),
        '/4': (context) => const BMIPage(),
        '/5': (context) => const SettingPage(),
      },
    );
  }
}

home.dart

//home.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter_localization/flutter_localization.dart';
import 'package:age_calculator/age_calculator.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'applocale.dart';
import 'dialogutil.dart';
import 'gengo.dart';
//import 'gengolist.dart';
//import 'dripsound.dart';
//import 'bmi.dart';
//import 'setting.dart';

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  DateTime birthday = DateTime.parse('1900-01-01'); //仮の初期値 意味はない
  DateTime selecteddate1 = DateTime.parse('1900-01-01'); //仮の初期値 意味はない
  DateTime selecteddate2 = DateTime.parse('1900-01-01'); //仮の初期値 意味はない
  DateFormat dateF = DateFormat('yyyy-MM-dd'); // 時刻を消すために使用
  int addDay = 30;

  //shared_preferences

  // 設定値を取得
  void _readSetting() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // null なら、初期値文字列を渡す
    setState(() {
      birthday = DateTime.parse(prefs.getString('birthday') ?? '1968-06-10');
      addDay = int.parse(prefs.getString('addDay') ?? '30');

      DateTime d = DateTime.now(); // 時刻が入っている。
      String ds = dateF.format(d); // 一旦文字列にして時刻を消す
      selecteddate1 = DateTime.parse(ds);
      selecteddate2 = selecteddate1.add(Duration(days: addDay));
    });
  }

  // ウィジェットの初期化処理
  @override
  void initState() {
    super.initState();
    // 保存された設定を読み出す
    _readSetting();
  }

  // 西暦元号ページから、値をうけとる
  void _gengoListPage(BuildContext context) async {
    final dataFromSecondPage =
        await Navigator.of(context).pushNamed('/2') as String;
    String result = dataFromSecondPage.toString();
    debugPrint("select $result");
    String sBDay = '${result.substring(0, 4)}-01-01';
    setState(() {
      birthday = DateTime.parse(sBDay);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(AppLocale.title.getString(context)),
        actions: [
          IconButton(
            icon: const Icon(Icons.arrow_forward_ios),
            onPressed: () {
              // 西暦と元号のリスト表示画面へ
              _gengoListPage(context);
            },
          ),
        ],
      ),
      // AppBarにドローワーメニューを追加
      drawer: Drawer(
        child: ListView(
          children: <Widget>[
            DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text('Menu'),
            ),
            ListTile(
              title: Text(AppLocale.menuDripSound.getString(context)),
              onTap: () async {
                await Navigator.of(context).pushNamed('/3');
                if (context.mounted) Navigator.pop(context);
              },
            ),
            ListTile(title: Text('')), // 1行あける
            ListTile(
              title: Text(AppLocale.menuBMI.getString(context)),
              onTap: () async {
                await Navigator.of(context).pushNamed('/4');
                if (context.mounted) Navigator.pop(context);
              },
            ),
            ListTile(title: Text('')), // 1行あける
            ListTile(
              title: Text(AppLocale.menuSetting.getString(context)),
              onTap: () async {
                await Navigator.of(context).pushNamed('/5');
                _readSetting();
                if (context.mounted) Navigator.pop(context);
              },
            ),
          ],
        ),
      ),

      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  AppLocale.birthday.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                Text(
                  sGDate(birthday, context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                ElevatedButton(
                  onPressed: () async {
                    String? sBDay = await DialogUtils.showCalender(
                      context,
                      dateF.format(birthday),
                    );
                    if (sBDay != null) {
                      setState(() {
                        birthday = DateTime.parse(sBDay);
                      });
                    }
                  },
                  child: Text(
                    'CAL',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ),
              ],
            ),

            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  AppLocale.selectDate1.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                Text(
                  sGDate(selecteddate1, context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: () async {
                        String? sDate = await DialogUtils.showCalender(
                          context,
                          dateF.format(selecteddate1),
                        );
                        if (sDate != null) {
                          setState(() {
                            selecteddate1 = DateTime.parse(sDate);
                          });
                        }
                      },
                      child: Text(
                        'CAL',
                        style: Theme.of(context).textTheme.bodyMedium,
                      ),
                    ),
                    SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: () async {
                        String textBuf =
                            await DialogUtils.showEditingDialog(context, '0') ??
                            '0';
                        debugPrint(textBuf);

                        int num = int.parse(textBuf);
                        if (num > 0) {
                          setState(() {
                            selecteddate2 = selecteddate1.add(
                              Duration(days: num),
                            );
                          });
                        }
                      },
                      child: Text(
                        AppLocale.addDay.getString(context),
                        style: Theme.of(context).textTheme.bodyMedium,
                      ),
                    ),
                  ],
                ),
              ],
            ),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  AppLocale.age.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                Text(
                  AgeCalculator.age(birthday, today: selecteddate1).toString(),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ],
            ),

            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  AppLocale.selectDate2.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                Text(
                  sGDate(selecteddate2, context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                ElevatedButton(
                  onPressed: () async {
                    String? sDate = await DialogUtils.showCalender(
                      context,
                      dateF.format(selecteddate2),
                    );
                    if (sDate != null) {
                      setState(() {
                        selecteddate2 = DateTime.parse(sDate);
                      });
                    }
                  },
                  child: Text(
                    'CAL',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ),
              ],
            ),

            Text(
              '${AppLocale.fromSelectDate2.getString(context)} : ${selecteddate2.difference(selecteddate1).inDays + 1}',
              style: Theme.of(context).textTheme.bodyMedium,
            ),

            Text(
              AppLocale.dayWarning.getString(context),
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ],
        ),
      ),
    );
  }
}

setting.dart

//setting.dart

import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_file.dart';
import 'package:intl/intl.dart';
import 'package:flutter_localization/flutter_localization.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'applocale.dart';
import 'gengo.dart';
import 'dialogutil.dart';

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

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

class _SettingPageState extends State<SettingPage> {
  // 仮の初期値設定
  DateTime birthday = DateTime.parse('1900-01-01');
  int addDay = 30;
  double height = 172;
  double weight = 72;
  int dripVolume = 100;
  int dripTime = 30;
  int dripType = 20;
  DateFormat dateF = DateFormat('yyyy-MM-dd'); // 時刻を消すために使用

  final FlutterLocalization _localization = FlutterLocalization.instance;

  //shared_preferences

  // 設定値を取得
  void _readSetting() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // null なら、初期値文字列を渡す
    setState(() {
      birthday = DateTime.parse(prefs.getString('birthday') ?? '1968-06-10');
      addDay = int.parse(prefs.getString('addDay') ?? '30');
      height = double.parse(prefs.getString('height') ?? '172');
      weight = double.parse(prefs.getString('weight') ?? '72');
      dripVolume = int.parse(prefs.getString('dripVolume') ?? '100');
      dripTime = int.parse(prefs.getString('dripTime') ?? '30');
      dripType = int.parse(prefs.getString('dripType') ?? '20');
    });
  }

  // 設定値を保存
  void _saveSetting() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    // birthdayを1900-01-01形式の文字列に変換して保存
    //   String sbirthday = dateF.format(birthday);
    await prefs.setString('birthday', dateF.format(birthday));
    await prefs.setString('addDay', addDay.toString());
    await prefs.setString('height', height.toStringAsFixed(1));
    await prefs.setString('weight', weight.toStringAsFixed(1));
    await prefs.setString('dripVolume', dripVolume.toString());
    await prefs.setString('dripTime', dripTime.toString());
    await prefs.setString('dripType', dripType.toString());
  }

  // ウィジェットの初期化処理
  @override
  void initState() {
    super.initState();
    // 保存された設定を読み出す
    _readSetting();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(AppLocale.menuSetting.getString(context))),
      body: _setting(),
    );
  }

  Widget _setting() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                AppLocale.birthday.getString(context),
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              Text(
                sGDate(birthday, context),
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              ElevatedButton(
                onPressed: () async {
                  String? sBDay = await DialogUtils.showCalender(
                    context,
                    dateF.format(birthday),
                  );
                  if (sBDay != null) {
                    setState(() {
                      birthday = DateTime.parse(sBDay);
                    });
                  }
                },
                child: Text(
                  'CAL',
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.addDay.getString(context)} : $addDay',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  int num = int.parse(textBuf);
                  if (num > 0) {
                    setState(() {
                      addDay = num;
                    });
                  }
                },
                child: Text(
                  AppLocale.addDay.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.bmiHeight.getString(context)} : ${height.toStringAsFixed(1)} cm',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  double ht = double.parse(textBuf);
                  if (ht > 0) {
                    setState(() {
                      height = ht;
                    });
                  }
                },
                child: Text(
                  AppLocale.bmiHeight.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.bmiWeight.getString(context)} : ${weight.toStringAsFixed(1)} kg',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  double wt = double.parse(textBuf);
                  if (wt > 0) {
                    setState(() {
                      weight = wt;
                    });
                  }
                },
                child: Text(
                  AppLocale.bmiWeight.getString(context),
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ),
            ],
          ),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${AppLocale.dripSoundVolume.getString(context)} : $dripVolume mL',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              ElevatedButton(
                onPressed: () async {
                  String textBuf =
                      await DialogUtils.showEditingDialog(context, '') ?? '0';
                  debugPrint(textBuf);

                  int num = int.parse(textBuf);
                  if (num > 0) {
                    setState(() {
                      dripVolume = num;
                    });
                  }
                },
                child: Text(
                  AppLocale.dripSoundVolume.getString(context),
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
            ],
          ),

          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                '${(dripTime / 60).toInt().toString()}:${dripTime % 60} ($dripTime ${AppLocale.dripSoundTimeMin.getString(context)})',
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: () async {
                      String textBuf =
                          await DialogUtils.showEditingDialog(context, '') ??
                          '0';
                      debugPrint(textBuf);

                      int num = (double.parse(textBuf) * 60).toInt();
                      if (num > 0) {
                        setState(() {
                          dripTime = num;
                        });
                      }
                    },
                    child: Text(
                      AppLocale.dripSoundTimeHour.getString(context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () async {
                      String textBuf =
                          await DialogUtils.showEditingDialog(context, '') ??
                          '0';
                      debugPrint(textBuf);

                      int num = int.parse(textBuf);
                      if (num > 0) {
                        setState(() {
                          dripTime = num;
                        });
                      }
                    },
                    child: Text(
                      AppLocale.dripSoundTimeMin.getString(context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                  ),
                ],
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Radio(
                value: 20,
                groupValue: dripType,
                onChanged: (value) {
                  setState(() {
                    dripType = value!;
                  });
                },
              ),
              Text(
                AppLocale.dripSoundType20.getString(context),
                style: Theme.of(context).textTheme.bodyMedium,
              ),
              SizedBox(height: 20, width: 20),
              Radio(
                value: 60,
                groupValue: dripType,
                onChanged: (value) {
                  setState(() {
                    dripType = value!;
                  });
                },
              ),
              Text(
                AppLocale.dripSoundType60.getString(context),
                style: Theme.of(context).textTheme.bodyMedium,
              ),
            ],
          ),
          ElevatedButton(
            onPressed: () async {
              _saveSetting();
              Navigator.pop(context);
            },
            child: Text('Save', style: Theme.of(context).textTheme.bodyMedium),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                child: const Text('English'),
                onPressed: () {
                  _localization.translate('en');
                  initializeDateFormatting('en', '');
                },
              ),
              SizedBox(width: 30),
              ElevatedButton(
                child: const Text('Japanese'),
                onPressed: () {
                  _localization.translate('ja');
                  initializeDateFormatting('ja', '');
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}

pubspec.yaml (icon類は別記事で解説 assets/images/launchar/にicon用画像を格納する。スペースによる段付けにも意味があります。)

......
flutter:
  # ......
  assets:
    - assets/sounds/
    - assets/images/

flutter_launcher_icons:
  ios: true
  image_path: "assets/images/launcher/calcdate_icon_ios.png"
  remove_alpha_ios: true
  android: true
  adaptive_icon_background: "assets/images/launcher/calcdate_icon_b.png"
  adaptive_icon_foreground: "assets/images/launcher/calcdate_icon_f.png"

注意:Playストアで配布する場合は、以下の作業も必要です。詳しくは成功してから別記事で解説します。

アップロード鍵を作成、pemに変換

android/app/build.gradle.ktsに必要な記載を行う。(赤字部分を適切なものに変更)

plugins {
    id("com.android.application")
    id("kotlin-android")
    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
    id("dev.flutter.flutter-gradle-plugin")
}

android {
    namespace = "com.your_project.calcdate"
    compileSdk = flutter.compileSdkVersion
    // ndkVersion = flutter.ndkVersion
    ndkVersion = "28.0.13004108"

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_11.toString()
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "com.your_project.calcdate"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = flutter.minSdkVersion
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }

    signingConfigs {
        create("release") {
            storeFile = file("key.jks")
            storePassword = "your_password"
            keyAlias = "key"
            keyPassword = "your_password"
        }
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

flutter {
    source = "../.."
}

・com.example.calcdate のexample を自分のプロジェクト名にする。(フォルダ名を変える)
 /android/app/src/main/kotlin/com/example/calcdate
 そのフォルダにあるMainActivity.kt ファイルの中身も同様に修正する。

下記コマンドでPlayストア用のapp-release.aabファイルが/build/app/outputs/bundle/releaseにできる。

自分のスマホでテストするなら、flutter build apkとしてapkファイルを作るとOK

flutter upgrade
flutter pub upgrade

flutter clean
flutter pub get
flutter pub run flutter_launcher_icons
flutter build appbundle

-Flutter Dart Unity アプリ開発