NCMBのFlutter SDKを使ってカレンダーアプリを作ります。予定を登録したり、カレンダーライブラリ(table_calendar)を使って予定を表示できるというアプリです。
前回の記事では認証処理を実装しましたので、今回は予定の表示と作成・更新、そして削除処理を実装していきます。
コードについて
今回のコードはNCMBMania/flutter-calendarにアップロードしてあります。実装時の参考にしてください。
カレンダーの予定一覧について
画面について
カレンダー画面は以下のようになります。カレンダーの表示自体はtable_calendarを利用しています。
@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
へセットします。
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つまでの表示で、それ以上を返しても結果は変わりませんでした。
// イベントの有無を返す関数
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;
}
日付を選択後、予定を一覧表示する
カレンダーの日付を選択したら、該当日の予定を一覧表示します。まず該当日だけのデータにフィルタリングします。
// 日付を選択した際の処理
void _onDaySelected(selectedDay, focusedDay) {
// フォーカス日を更新
setState(() {
_focusDate = selectedDay;
});
// フィルタリングを実行
_filterdSchedule(selectedDay);
}
そして、この結果をListで表示します。
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)に遷移します。
@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
を呼び出します。
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 のナビゲーションーメニューにあるプラスアイコンより遷移します。
appBar: AppBar(
title: const Text("カレンダー"),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: _addSchedule,
)
],
),
_addScheduleは画面遷移し、結果として作成したスケジュールが返ってきます。そのスケジュールをリストに追加します。
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)は予定のタイトルと詳細、予定開始日時、終了日時などを入力して登録を行います。
@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は編集にそのまま使えないので、画面表示を行ったタイミングで入力用変数に適用します。
@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はデータをセキュアに扱うために必要です。今回はデータを作成した本人のみ、読み書きできるようにします。
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のデータストアでは日付や文字列(他にも数字や配列、真偽値、オブジェクト、位置情報)など様々な形式でデータの保存・取得ができます。ぜひ柔軟に皆さんのアプリに活かしてください。