広告 Flutter Dart Unity アプリ開発

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

更新日時:2025/2/22 17:00

概要

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

使用しているパッケージ

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

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

現時点での問題

・西暦和暦変換の多言語対応が暫定対応です。('ja'のみ対応)もっとスマートなやり方があるような気がする。
・点滴速度を示す音がスマホで鳴りません。(要調査) 数字はカウントアップします。Chromeでは音が鳴ります。

サンプルソース

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

dialogutil.dartは、以下のURLで紹介されたものを流用しています。
Flutter 逆引き辞典 Chapter 16 ダイアログでもテキスト入力がしたい
https://zenn.dev/pressedkonbu/books/flutter-reverse-lookup-dictionary/viewer/016-input-text-on-dialog

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

//applocale.dart

mixin AppLocale {
  static const String title = 'title';
  static const String thisIs = 'thisIs';
  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.',
    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です。',
    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

//dialogutil.dart

import 'package:flutter/material.dart';

class DialogUtils {
  DialogUtils._();

  /// タイトルのみを表示するシンプルなダイアログを表示する
  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);
      },
    );
  }
}

/// 状態を持ったダイアログ
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,
        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 'package:intl/date_symbol_data_local.dart';

import 'applocale.dart';

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

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

  return gengo;
}

// 日付Stringを返す
String japanDate(DateTime dt) {
  const Locale("ja");
  initializeDateFormatting("ja");
  return '${DateFormat.yMMMd('ja').format(dt)}(${DateFormat.E('ja').format(dt)})';
}

//元号付きの日付Stringを返す
String japanDate2(DateTime dt, BuildContext context) {
  return '(${Gengo(dt, context)})${japanDate(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 'gengo.dart';

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

  @override
  State<GengoListW> createState() => _GengoListWState();
}

class _GengoListWState extends State<GengoListW> {
  final _gengoYear = <String>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Year List')),
      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 (何故かAndroidスマホでは音がでない。 要調査!)
(initAudioPlayer() と、AudioContext は、あってもなくても変わらなかった)

//dripsound.dart

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

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

  @override
  State<DripSoundW> createState() => _DripSoundWState();
}

class _DripSoundWState extends State<DripSoundW> {
  int dripvolume = 100; //mL
  int driptime = 30; // min
  int driptype = 20; // 60drip/1mL or 20drip/1mL
  bool isPlaying = false;
  bool _isPlaying2 = false;
  late AudioPlayer player = AudioPlayer();
  int counter = 0;

  final AudioContext audioContext = AudioContext(
    iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
    android: AudioContextAndroid(
      isSpeakerphoneOn: true,
      stayAwake: true,
      contentType: AndroidContentType.sonification,
      usageType: AndroidUsageType.assistanceSonification,
      audioFocus: AndroidAudioFocus.none,
    ),
  );

  void initAudioPlayer() => AudioPlayer.global.setAudioContext(audioContext);

  void _beat(Timer t) async {
    if (isPlaying) {
      setState(() {
        counter++;
      });
      if (!_isPlaying2) {
        _isPlaying2 = true;
        try {
          await player.play(AssetSource('sounds/click.mp3'));
        } catch (e) {
          debugPrint('Error playing audio: $e');
        }
        _isPlaying2 = false;
      }
    } else {
      t.cancel();
      setState(() {
        counter = 0;
      });
    }
  }

  // ウィジェットの初期化処理
  @override
  void initState() {
    super.initState();
    player = AudioPlayer();
    initAudioPlayer();
  }

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

  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;
                      isPlaying = 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;
                          isPlaying = 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;
                          isPlaying = 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!;
                    isPlaying = 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!;
                    isPlaying = 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,
              ),
              SizedBox(height: 20, width: 20),
              ElevatedButton(
                onPressed: () {
                  if (!isPlaying) {
                    int dur =
                        (60 * 1000 / (dripvolume / driptime * driptype))
                            .toInt();
                    if (dur >= 500) {
                      var duration = Duration(milliseconds: dur);
                      Timer.periodic(duration, (Timer t) => _beat(t));
                      setState(() {
                        isPlaying = true;
                      });
                    } else {
                      DialogUtils.showOnlyTitleDialog(
                        context,
                        AppLocale.dripSoundError.getString(context),
                      );
                    }
                  }
                },
                child: Text(
                  'Sound Start',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
              SizedBox(height: 20, width: 20),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    isPlaying = false;
                  });
                },
                child: Text(
                  'Sound Stop',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),
              SizedBox(height: 20, width: 20),
              Text(
                'Count : $counter',
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ],
          ),
        ],
      ),
    );
  }

  //  Navigator.pop(context);
  @override
  void dispose() {
    player.dispose();
    super.dispose();
  }
}

bmi.dat

//bmi.dart

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

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

  @override
  State<BMIW> createState() => _BMIWState();
}

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

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

  @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(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: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';
    
    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: 24),
            ),
          ),
          initialRoute: '/1',
          routes: {
            '/1': (context) => const DatePickerW(),
            '/2': (context) => const GengoListW(),
            '/3': (context) => const DripSoundW(),
            '/4': (context) => const BMIW(),
          },
        );
      }
    }
    
    //ホーム画面
    
    class DatePickerW extends StatefulWidget {
      const DatePickerW({super.key});
    
      @override
      State<DatePickerW> createState() => _DatePickerWState();
    }
    
    class _DatePickerWState extends State<DatePickerW> {
      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'); // 時刻を消すために使用
    
      //shared_preferences
      // 設定値を取得
      void _readSetting() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        // null なら、初期値で有効な日付文字列を渡す
        String sbirthday = prefs.getString('birthday') ?? '1968-06-10';
        setState(() {
          birthday = DateTime.parse(sbirthday);
        });
      }
    
      // 設定値を保存
      void _saveSetting() async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        // birthdayを1900-01-01形式の文字列に変換して保存
        String sbirthday = dateF.format(birthday);
        await prefs.setString('birthday', sbirthday);
      }
    
      // ウィジェットの初期化処理
      @override
      void initState() {
        super.initState();
        // 保存された設定を読み出す
        _readSetting();
        // 時刻情報を削除する
        setState(() {
          DateTime d =DateTime.now(); // 時刻が入っている。
          String ds = dateF.format(d); // 一旦文字列にして時刻を消す
          selecteddate1 =  DateTime.parse(ds);
          selecteddate2 = selecteddate1.add(Duration(days: 30));
        });
    
      }
    
      // カレンダーで日付を選択するダイアログボックス
      Future<void> _selectBirthDay(BuildContext context) async {
        final DateTime? picked = await showDatePicker(
          context: context,
          initialDate: birthday,
          firstDate: DateTime(DateTime.now().year - 120),
          lastDate: DateTime(DateTime.now().year + 120),
        );
        setState(() {
          DateTime d = picked ?? birthday;
          String sd = dateF.format(d);
          birthday = DateTime.parse(sd);
        });
      }
    
      // カレンダーで日付を選択するダイアログボックス
      Future<void> _selectDate1(BuildContext context) async {
        final DateTime? picked = await showDatePicker(
          context: context,
          initialDate: selecteddate1,
          firstDate: DateTime(DateTime.now().year - 120),
          lastDate: DateTime(DateTime.now().year + 120),
        );
        setState(() {
          DateTime d = picked ?? selecteddate1;
          String sd = dateF.format(d);
          selecteddate1 = DateTime.parse(sd);
        });
      }
    
      // カレンダーで日付を選択するダイアログボックス
      Future<void> _selectDate2(BuildContext context) async {
        final DateTime? picked = await showDatePicker(
          context: context,
          initialDate: selecteddate2,
          firstDate: DateTime(DateTime.now().year - 120),
          lastDate: DateTime(DateTime.now().year + 120),
        );
        setState(() {
          DateTime d = picked ?? selecteddate2;
          String sd = dateF.format(d);
          selecteddate2 = DateTime.parse(sd);
        });
      }
    
      // 西暦元号ページから、値をうけとる
      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('Drip Calc'),
                  onTap: () async {
                    await Navigator.of(context).pushNamed('/3');
                    if (context.mounted) Navigator.pop(context);
                  },
                ),
                ListTile(title: Text('')), // 1行あける
                ListTile(
                  title: Text('BMI Calc'),
                  onTap: () async {
                    await Navigator.of(context).pushNamed('/4');
                    if (context.mounted) Navigator.pop(context);
                  },
                ),
                ListTile(title: Text('')), // 1行あける
                ListTile(
                  title: Text('Save BirthDay'),
                  onTap: () {
                    _saveSetting();
                    Navigator.pop(context);
                  },
                ),
                ListTile(
                  title: Text('Load BirthDay'),
                  onTap: () {
                    _readSetting();
                    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(
                      japanDate2(birthday, context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    ElevatedButton(
                      onPressed: () {
                        _selectBirthDay(context);
                      },
                      child: const Text('CAL'),
                    ),
                  ],
                ),
    
                Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      AppLocale.selectDate1.getString(context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    Text(
                      japanDate2(selecteddate1, context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        ElevatedButton(
                          onPressed: () => _selectDate1(context),
                          child: const Text('CAL'),
                        ),
                        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)),
                        ),
                      ],
                    ),
                  ],
                ),
                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(
                      japanDate2(selecteddate2, context),
                      style: Theme.of(context).textTheme.bodyMedium,
                    ),
                    ElevatedButton(
                      onPressed: () => _selectDate2(context),
                      child: const Text('CAL'),
                    ),
                  ],
                ),
    
                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,
                ),
              ],
            ),
          ),
        );
      }
    }
    

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

    ......
    
    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 アプリ開発