更新日時: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