NCMBのFlutter SDKを使ってカレンダーアプリを作る（その3：予定の登録と更新、削除）

NCMBのFlutter SDKを使ってカレンダーアプリを作ります。予定を登録したり、カレンダーライブラリ（[table_calendar](https://pub.dev/packages/table_calendar)）を使って予定を表示できるというアプリです。

[前回の記事では認証処理を実装しました](https://qiita.com/goofmint/items/8a766ffd96c3e84d598a)ので、今回は予定の表示と作成・更新、そして削除処理を実装していきます。

## コードについて

今回のコードは[NCMBMania/flutter\-calendar](https://github.com/NCMBMania/flutter-calendar)にアップロードしてあります。実装時の参考にしてください。


## カレンダーの予定一覧について

### 画面について

![localhost_56942_ (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/3220fefd-2ece-eff0-628b-304fd0d53630.png)


カレンダー画面は以下のようになります。カレンダーの表示自体はtable_calendarを利用しています。

```dart
@override
Widget build(BuildContext context) {
	return Scaffold(
			appBar: AppBar(
				title: const Text("カレンダー"),
				actions: [
					IconButton(
						icon: const Icon(Icons.add),
						onPressed: _addSchedule,
					)
				],
			),
			body: Column(children: [
				TableCalendar(
					locale: 'ja',
					eventLoader: _eventLoader,
					firstDay: DateTime.utc(2010, 10, 16, 10),
					lastDay: DateTime.utc(2030, 3, 14),
					focusedDay: _focusDate,
					onDaySelected: _onDaySelected,
				),
				_selectedSchedules.isEmpty
						? const Text("該当日にイベントはありません")
						: Expanded(
								child: ListView.builder(
										itemCount: _selectedSchedules.length,
										itemBuilder: (BuildContext context, int index) {
											final schedule = _selectedSchedules[index];
											return ScheduleListPage(
												schedule: schedule,
												onEdited: (NCMBObject schedule) {
													setState(() {
														_selectedSchedules[index] = schedule;
													});
												},
											);
										}))
			]));
}
```

### 予定の取得とカレンダーへの設定

画面を表示したタイミングでNCMBのデータストアから予定一覧を取得しています。デフォルトでは表示月の月初から月末までの予定をすべて取得し、それを `_schedules` へセットします。

```dart
void _getSchedule() async {
	// 予定検索用クエリー
	var query = NCMBQuery("Schedule");
	// 基準日時
	var date = DateTime.now();
	// 月初
	DateTime startDate = DateTime(date.year, date.month, 1);
	// 翌月初
	DateTime endDate =
			DateTime(date.year, date.month + 1, 1).add(const Duration(days: -1));
	// 検索条件を設定
	query
		..greaterThanOrEqualTo("startDate", startDate)
		..lessThan("endDate", endDate);
	// 検索実行
	var schedules =
			(await query.fetchAll()).map((s) => s as NCMBObject).toList();
	// 表示に反映
	setState(() {
		_schedules = schedules;
	});
}
```

### カレンダー表示

カレンダーはtable_calendarで行っていますが、予定があるところにはドットが表示されます。以下はその該当部分のコードです。最大4つまでの表示で、それ以上を返しても結果は変わりませんでした。

```dart
// イベントの有無を返す関数
List<NCMBObject> _eventLoader(DateTime day) {
	// 日付毎に呼ばれる
	return _filterdSchedule(day, updateSelected: false);
}

// ある日付に存在するスケジュールを一覧で返す関数
List<NCMBObject> _filterdSchedule(DateTime day, {updateSelected = true}) {
	// 日付を文字列にするフォーマッタ
	final dateFormat = DateFormat('yyyy/MM/dd');
	final targetDate = dateFormat.format(day);
	// フィルタリング
	final schedules = _schedules
			.where((schedule) =>
					dateFormat.format(schedule.getDateTime("startDate",
							defaultValue: DateTime.now())) ==
					targetDate)
			.toList();
	// 一覧表示の場合はtrue
	if (updateSelected) {
		setState(() {
			_selectedSchedules = schedules;
		});
	}
	return schedules;
}
```

### 日付を選択後、予定を一覧表示する

カレンダーの日付を選択したら、該当日の予定を一覧表示します。まず該当日だけのデータにフィルタリングします。

```dart
// 日付を選択した際の処理
void _onDaySelected(selectedDay, focusedDay) {
	// フォーカス日を更新
	setState(() {
		_focusDate = selectedDay;
	});
	// フィルタリングを実行
	_filterdSchedule(selectedDay);
}
```

そして、この結果をListで表示します。

```dart
Expanded(
	child: ListView.builder(
			itemCount: _selectedSchedules.length,
			itemBuilder: (BuildContext context, int index) {
				final schedule = _selectedSchedules[index];
				return ScheduleListPage(
					schedule: schedule,
					onEdited: (NCMBObject schedule) {
						setState(() {
							_selectedSchedules[index] = schedule;
						});
					},
				);
			}))
```

ScheduleListPage は受け取ったスケジュールを描画します。そして、該当予定をタップすると、編集画面（ScheduleFormPage）に遷移します。

```dart
@override
Widget build(BuildContext context) {
	return GestureDetector(
      onTap: _onTap,
			child: Container(
					decoration: const BoxDecoration(
						border: Border(
							bottom: BorderSide(color: Colors.black38),
						),
					),
					child: Column(
						children: [
							Row(
								children: [
									Text(widget.schedule
											.getString("title", defaultValue: "タイトルなし")),
									const Spacer(),
									Text(_viewTime())
								],
							),
							Container(
									alignment: Alignment.centerLeft,
									child: Text(
											widget.schedule.getString("body", defaultValue: "")))
						],
					)));
}
```

タップして予定を編集した際には、返却値として編集後のスケジュールが返ってきます。そのデータをスケジュール一覧と差し替えるため、 `onEdited` を呼び出します。

```dart
void _onTap() async {
	var schedule = await Navigator.push(
			context,
			MaterialPageRoute(
					settings: const RouteSettings(name: '/new'),
					builder: (BuildContext context) =>
							ScheduleFormPage(schedule: widget.schedule)));
	if (schedule != null) {
		widget.onEdited(schedule);
	}
}
```


## 予定を追加・編集する

予定を追加したり、編集する際には ScheduleFormPage へ移動します。新規作成の場合は CalendarPage のナビゲーションーメニューにあるプラスアイコンより遷移します。

```dart
appBar: AppBar(
	title: const Text("カレンダー"),
	actions: [
		IconButton(
			icon: const Icon(Icons.add),
			onPressed: _addSchedule,
		)
	],
),
```

_addScheduleは画面遷移し、結果として作成したスケジュールが返ってきます。そのスケジュールをリストに追加します。

```dart
void _addSchedule() async {
	var schedule = await Navigator.push(
			context,
			MaterialPageRoute(
					settings: const RouteSettings(name: '/new'),
					builder: (BuildContext context) =>
							ScheduleFormPage(schedule: NCMBObject("Schedule"))));
	if (schedule != null) {
		_schedules.add(schedule as NCMBObject);
	}
}
```

### フォーム画面について

フォーム画面（ScheduleFormPage）は予定のタイトルと詳細、予定開始日時、終了日時などを入力して登録を行います。

```dart
@override
Widget build(BuildContext context) {
	return Scaffold(
			appBar: AppBar(
				title: const Text("予定の追加・編集"),
			),
			body: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
				Column(mainAxisAlignment: MainAxisAlignment.center, children: [
					SizedBox(
							height: 50,
							width: 250,
							child: TextFormField(
								initialValue: _title,
								decoration: const InputDecoration.collapsed(hintText: 'タイトル'),
								onChanged: (value) {
									setState(() {
										_title = value;
									});
								},
							)),
					SizedBox(
							height: 50,
							width: 250,
							child: TextFormField(
								initialValue: _body,
								decoration: const InputDecoration.collapsed(hintText: '予定詳細'),
								maxLines: 10,
								onChanged: (value) {
									setState(() {
										_body = value;
									});
								},
							)),
					SizedBox(
							height: 50,
							width: 250,
							child: TextFormField(
									controller: _startDateController,
									decoration:
											const InputDecoration.collapsed(hintText: '開始日時'),
									maxLines: 10,
									enableInteractiveSelection: false,
									onTap: () => _showDateTimePicker("startDate"))),
					SizedBox(
							height: 50,
							width: 250,
							child: TextFormField(
									controller: _endDateController,
									decoration:
											const InputDecoration.collapsed(hintText: '終了日時'),
									maxLines: 10,
									enableInteractiveSelection: false,
									onTap: () => _showDateTimePicker("endDate"))),
					TextButton(onPressed: _save, child: const Text("保存する")),
				])
			]));
}
```

### データの準備

NCMBObjectは編集にそのまま使えないので、画面表示を行ったタイミングで入力用変数に適用します。

```swift
@override
void initState() {
	setState(() {
		_title = widget.schedule.getString("title", defaultValue: "");
		_body = widget.schedule.getString("body", defaultValue: "");
		if (widget.schedule.objectId == null) {
			_endDate = _startDate.add(const Duration(hours: 1));
		} else {
			_startDate = widget.schedule
					.getDateTime("startDate", defaultValue: DateTime.now());
			_endDate = widget.schedule
					.getDateTime("endDate", defaultValue: DateTime.now());
		}
		_startDateController.text = _dateFormat.format(_startDate);
		_endDateController.text = _dateFormat.format(_endDate);
	});
	super.initState();
}
```

### データの保存

データを新規作成・更新する流れは同じです。入力値の適用と、ACL（アクセス権限）を設定します。ACLはデータをセキュアに扱うために必要です。今回はデータを作成した本人のみ、読み書きできるようにします。

```dart
void _save() async {
	// 入力データを適用
	widget.schedule
		..set("title", _title)
		..set("body", _body)
		..set("startDate", _startDate)
		..set("endDate", _endDate);
	// ACL（アクセス権限）を設定
	var acl = NCMBAcl();
	var user = await NCMBUser.currentUser();
	acl
		..setUserReadAccess(user!, true)
		..setUserWriteAccess(user, true);
	widget.schedule.set("acl", acl);
	// 保存実行
	await widget.schedule.save();
	// 前の画面に戻る
	Navigator.pop(context, widget.schedule);
}
```

## まとめ

ここまでの流れでカレンダーアプリの完成です。NCMBのデータストアでは日付や文字列（他にも数字や配列、真偽値、オブジェクト、位置情報）など様々な形式でデータの保存・取得ができます。ぜひ柔軟に皆さんのアプリに活かしてください。

[mBaaSでサーバー開発不要！ | ニフクラ mobile backend](https://mbaas.nifcloud.com/)