概要
- 生年月日をカレンダーで入力し、本日または指定した日付での年齢を計算する。和暦と西暦の対応表を閲覧でき、タップすると指定した年の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