Flutter iOS 音声メモアプリ開発(5)HealthKitの組込み
HealthKitのリポジトリに血圧データを登録するパッケージ(health_kit_reporter)の使い方について、前回記事化しました。
本記事では、音声メモアプリに血圧登録のためのhealth_kit_reporterモジュールを組み込んでいきます。
ちなみに、今回ちょっとした文字列操作のコードをChatGPTで書かせてみました。最後におまけとして記述しています。ちょっと面倒なロジックを書くときなどは、自分より正確かもしれません。
血圧登録操作
まず、想定される操作を考えてみます。
1)音声メモを入力
2)音声メモをタップすると、編集画面に遷移
3)音声メモの内容が、血圧登録に関するものだったら、登録用のWidgetを表示
4)登録ボタンを押して、HealthKitリポジトリに登録
画面遷移
1)音声メモを入力
2)音声メモをタップすると、編集画面に遷移します。アプリをインストール後の最初のこの操作で、HealthKitへのアクセス許可をユーザに求めます。この画面は一度しか表示させることができません。 もし、ユーザから許可されなかった場合は、アプリを再インストールするか、iOSの設定画面からユーザに許可の操作をしてもらう必要があります。
3)音声メモに「血圧」という単語と、2つの数字が含まれている場合には、登録用のwidgetsを表示します。日時を変更するためにflutter_datetime_pickerパッケージを使っています。
4)「ヘルスケアアプリに登録」ボタンを押すと登録を実行し、結果を表示させます。
登録済みである印(✅)をSentimentの顔絵文字の代わりに設定します。
サンプルコード
元のコードは、main.dartとcard.dartの2つでした。今回は、card.dartを改変し、health_reporter.dartを追加します。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:health_kit_reporter/health_kit_reporter.dart';
import 'package:health_kit_reporter/model/payload/correlation.dart';
import 'package:health_kit_reporter/model/payload/device.dart';
import 'package:health_kit_reporter/model/payload/quantity.dart';
import 'package:health_kit_reporter/model/payload/source.dart';
import 'package:health_kit_reporter/model/payload/source_revision.dart';
import 'package:health_kit_reporter/model/predicate.dart';
import 'package:health_kit_reporter/model/type/correlation_type.dart';
import 'package:health_kit_reporter/model/type/quantity_type.dart';
mixin HealthKitReporterMixin {
// 省略 前記事参照 デバイス情報等
}
class MyHealth extends State with HealthKitReporterMixin {
final String bd = '';
final String bs = '';
static bool saved = false;
Future<void> authorize() async {
// 省略 前記事参照 ユーザに血圧データへのアクセスを求める
}
Future<bool> saveBloodPressureCorrelation(
int bs, int bd, DateTime bDate) async {
try {
//final now = DateTime.now();
//final minuteAgo = now.add(const Duration(minutes: -1));
final sys = Quantity(
'testSysUUID234',
QuantityType.bloodPressureSystolic.identifier,
bDate.millisecondsSinceEpoch,
bDate.millisecondsSinceEpoch,
device,
sourceRevision,
QuantityHarmonized(bs, 'mmHg', null));
final dia = Quantity(
'testDiaUUID456',
QuantityType.bloodPressureDiastolic.identifier,
bDate.millisecondsSinceEpoch,
bDate.millisecondsSinceEpoch,
device,
sourceRevision,
QuantityHarmonized(bd, 'mmHg', null));
final correlationJarmonized = CorrelationHarmonized([sys, dia], [], null);
final correlation = Correlation(
'test',
CorrelationType.bloodPressure.identifier,
bDate.millisecondsSinceEpoch,
bDate.millisecondsSinceEpoch,
device,
sourceRevision,
correlationJarmonized);
saved = await HealthKitReporter.save(correlation);
print('BloodPressureCorrelationSaved: $saved');
return saved;
} catch (e) {
return false;
print(e);
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
}
前回の記事(パッケージのサンプルコード)からの変更点はsaveBloodPressureCorrelation()で、引数を受け取れるようにして、登録できたかの結果をbool値で返すようにしています。
デバイス情報は、今回はサンプルコードの固定のままにしていますが、デバイス情報を取得するパッケージdevice_info_plusを使えば取得可能なようです。
import 'package:flutter/material.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'main.dart';
import 'health_reporter.dart';
class CardPage extends StatefulWidget {
const CardPage({Key? key, required this.card_item}) : super(key: key);
final Todo card_item;
@override
CardPageState createState() => CardPageState();
}
class CardPageState extends State<CardPage> {
late Todo item;
final TextEditingController _textController = TextEditingController();
bool resTextValue = false;
int sbp = 0; // 最高血圧 収縮期血圧
int dbp = 0; // 最低血圧 拡張期血圧
DateTime _myDate = DateTime.now();
final MyHealth _myHealth = MyHealth();
void _textValue(String input) {
sbp = 0;
dbp = 0;
try {
bool bloodP = input.contains('血圧'); // 血圧という文字列を含む
RegExp exp = RegExp(r"(\d{2,3}).*?(\d{2,3})"); // 2つの数字を含む
Match match = exp.firstMatch(input) as Match;
if (match != null) {
String? firstNumber = match.group(1);
String? secondNumber = match.group(2);
int blood1 = int.parse(firstNumber!);
int blood2 = int.parse(secondNumber!);
if (blood1 > blood2) { // 大きい方を最大血圧にする
sbp = blood1;
dbp = blood2;
} else {
sbp = blood2;
dbp = blood1;
}
if (bloodP && blood1 != 0 && blood2 != 0) {
resTextValue = true; // 血圧登録データですよ
} else {
resTextValue = false;
}
}
setState(() {});
} catch (e) {
resTextValue = false;
}
}
@override
void initState() {
super.initState();
item = widget.card_item;
_textValue(item.title); // メモが血圧登録データか調べる
initializeDateFormatting('ja_JP');
_myDate = item.time; // DatePickerの初期値は、メモの登録日時とする
_myHealth.authorize();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(item.time.toIso8601String())),
body: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Card(
child: ListTile(
onTap: () async {
var ans = await _showEditDialog();
if (_textController.text != '' && ans == 'save') {
_editDataText(_textController.text);
_textValue(item.title);
setState(() {});
}
if (ans == 'cancel') {
_textController.text = '';
}
},
title: Text(item.title),
//subtitle: Text(item.time.toString()),
trailing: Text(item.sentiment,
style: Theme.of(context).textTheme.headlineSmall),
),
),
const SizedBox(height: 10),
if (resTextValue) // 血圧登録用のメモだった場合にCardを表示
Card(
child: Column(children: [
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(
Colors.blue.withOpacity(0.7)),
),
onPressed: () {
DatePicker.showDateTimePicker(context,
showTitleActions: true, onConfirm: (date) {
initializeDateFormatting('ja_JP');
setState(() {
_myDate = date;
});
}, currentTime: _myDate, locale: LocaleType.jp);
},
child: Text(
DateFormat.yMMMEd('ja_JP').add_jm().format(_myDate),
style: TextStyle(fontSize: 20),
),
),
const SizedBox(height: 20),
Text("最高血圧:$sbp", style: const TextStyle(fontSize: 20)),
Text("最低血圧:$dbp", style: const TextStyle(fontSize: 20)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
_textValue(item.title);
if (resTextValue) {
bool saved = await _myHealth.saveBloodPressureCorrelation(
sbp, dbp, _myDate);
if (saved) {
var ans = await _showSavedDialog(
'血圧を登録しました。\nヘルスケアアプリから確認できます。');
setState(() {
item.sentiment = '✅';
});
} else {
var ans = await _showSavedDialog(
'血圧を登録できませんでした。\niOSの設定からヘルスケアを選択し、アプリからのデータアクセスを許可してください');
}
}
},
child: const Text("ヘルスケアアプリに登録"),
//icon: const Icon(Icons.health_and_safety))
)
]))
],
),
),
floatingActionButton: FloatingActionButton(
// 省略 前記事参照 編集用ダイアログの表示
);
}
Future<void> _editDataText(String inputText) async {
if (inputText != '') {
item.title = inputText;
item.save();
}
}
Future<String> _showEditDialog() async {
// 省略 前記事参照 編集用ダイアログ
}
Future<String> _showSavedDialog(String message) async {
var result = await showDialog(
context: context,
builder: (_) {
return AlertDialog(
insetPadding: const EdgeInsets.all(8),
title: const Text('ヘルスケアの登録'),
content: Text(message),
actions: [
TextButton(
child: const Text('close'),
onPressed: () {
Navigator.pop(context, 'close');
},
),
],
);
},
);
return result;
}
}
おまけ)ChatGPTのコード生成
card.dartの変更点は_textValue()を追加した以外は、ほぼUIの変更点です。DatePickerの使い方は少し難しかったので、後日記事化したいと思います。
_textValue()はメモの内容が、血圧登録用のメモか否かを判断して、血圧登録用のメモであった場合に、最高血圧と最低血圧をsbp,dbpにセットするものです。
こちらは、正規表現を使っていますが、あまり得意ではないので、ChatGPTにコードを書いてもらいました。
質問:flutterで文字列から二つの数字を取り出す方法は
回答:Flutterでは、Dart言語の正規表現を使用して、文字列から二つの数字を取り出すことができます。以下は例です。
void main() { String str = "The numbers are 123 and 456"; RegExp exp = RegExp(r"(\d+).*?(\d+)"); Match match = exp.firstMatch(str); if (match != null) { String firstNumber = match.group(1); String secondNumber = match.group(2); print("First number: $firstNumber"); // Output: First number: 123 print("Second number: $secondNumber"); // Output: Second number: 456 } }
この例では、
RegExp
クラスを使用して正規表現パターンを定義し、firstMatch
メソッドを使用して、文字列中で最初にマッチするパターンを取得します。Match
オブジェクトが返され、group
メソッドを使用して、パターンのグループ化された部分を取得します。group(1)
は、正規表現パターンの最初のグループにマッチする文字列を返し、group(2)
は2番目のグループにマッチする文字列を返します。正規表現パターン
(\d+).*?(\d+)
は、上記の例の正規表現パターンに相当し、同様に、数字をグループ化するために丸括弧を使用します。.+?
は、最短一致を表す非貪欲マッチングで、2番目の数字を取得するために使用されます。
ほぼそのまま使えるコードでした。なかなか便利なものです。