こんにちは。広瀬マサルです。
Flutterでアプリケーションを安定して高速に開発することのできるMasamuneフレームワークを紹介しました。
その魅力と使い方を知っていただくためにこれから数回に分けて紹介記事を書きたいと思います。
Masamuneフレームワークを使いこなすことで超高速・安定・高品質なアプリを開発することができるでしょう。
前回から「メモ帳アプリ」の作成を開始しまして、メモの一覧と詳細の表示まで実装しました。
今回はメモの作成と更新、削除を行えるようにしCRUDアプリとしての完成を目指します。
メモの作成・更新
モバイルアプリやWebアプリ内でデータを作成や更新を行うには
フォーム
を作成します。
基本的には同じ内容を編集するわけなので作成時と更新時は画面的にはほぼ変わりないと思います。
ただ、 作成時は元のデータがなく 、 更新時は元のデータが必要 になります。
Masamuneフレームワークでは画面を可能な限り使いまわし、データを分けるように実装を行うことが可能です。
前回 からのプロジェクトを開いて、別ターミナルで下記コマンドを実行しコードの変更を監視してください。
katana code watch
下記のコマンドでフォーム用のテンプレートページを作成します。
katana code tmp form memo_edit
pages/memo_edit.dart
に下記のクラスが作成されます。
MemoEditAddPage
がメモを作成する時に使うページ、
MemoEditEditPage
がメモを更新するときに使うページです。
MemoEditForm
がそれぞれのフォームを記載するためのウィジェットクラスになります。
// memo_edit.dart
/// Page for forms to add data.
@immutable
@PagePath("memo_edit/add")
class MemoEditAddPage extends FormAddPageScopedWidget {
const MemoEditAddPage({
super.key,
});
/// Used to transition to the MemoEditAddPage screen.
///
/// ```dart
/// router.push(MemoEditAddPage.query(parameters)); // Push page to MemoEditAddPage.
/// router.replace(MemoEditAddPage.query(parameters)); // Replace page to MemoEditAddPage.
/// ```
@pageRouteQuery
static const query = _$MemoEditAddPageQuery();
@override
FormScopedWidget build(BuildContext context, PageRef ref) =>
const MemoEditForm();
}
/// Page for forms to edit data.
@immutable
@PagePath("memo_edit/:edit_id/edit")
class MemoEditEditPage extends FormEditPageScopedWidget {
const MemoEditEditPage({
super.key,
@PageParam("edit_id") required super.editId,
});
/// Used to transition to the MemoEditEditPage screen.
///
/// ```dart
/// router.push(MemoEditEditPage.query(parameters)); // Push page to MemoEditEditPage.
/// router.replace(MemoEditEditPage.query(parameters)); // Replace page to MemoEditEditPage.
/// ```
@pageRouteQuery
static const query = _$MemoEditEditPageQuery();
@override
FormScopedWidget build(BuildContext context, PageRef ref) =>
const MemoEditForm();
}
/// Widgets for form views.
@immutable
class MemoEditForm extends FormScopedWidget {
const MemoEditForm({super.key});
/// Used to transition to the MemoEditAddPage screen.
///
/// ```dart
/// router.push(MemoEditForm.addQuery(parameters)); // Push page to MemoEditAddPage.
/// router.replace(MemoEditForm.addQuery(parameters)); // Replace page to MemoEditAddPage.
/// ```
static const addQuery = MemoEditAddPage.query;
/// Used to transition to the MemoEditEditPage screen.
///
/// ```dart
/// router.push(MemoEditForm.editQuery(parameters)); // Push page to MemoEditEditPage.
/// router.replace(MemoEditForm.editQuery(parameters)); // Replace page to MemoEditEditPage.
/// ```
static const editQuery = MemoEditEditPage.query;
@override
Widget build(BuildContext context, FormRef ref) {
// Describes the process of loading
// and defining variables required for the page.
//
// You can use [ref.isAdding] or [ref.isEditing] to determine if the form is currently adding new data or editing data.
//
// If editing is in progress, it is possible to get the ID of the item being edited with [ref.editId].
// TODO: Implement the variable loading process.
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold();
}
}
MemoEditForm
の
build
メソッドに渡される
ref
にフォームを作成するための様々な機能が提供されてます。
ref.isEditing
が
true
の場合はデータが更新されているためにウィジェットが呼ばれていることがわかります。
その場合は編集用のドキュメントIDが
ref.editId
に渡されます。
また、
ref.select
メソッドを利用することでデータ追加時と編集時でデータを分けることが可能です。
これを利用して更新時に元のドキュメントデータをロードします。
また、作成されたデータスキームの
form
メソッドを利用することによりフォームで利用するためのコントローラーを取得することができます。
// memo_edit.dart
@override
Widget build(BuildContext context, FormRef ref) {
// Describes the process of loading
// and defining variables required for the page.
//
// You can use [ref.isAdding] or [ref.isEditing] to determine if the form is currently adding new data or editing data.
//
// If editing is in progress, it is possible to get the ID of the item being edited with [ref.editId].
// TODO: Implement the variable loading process.
final memoData = ref.select(
onEdit: (editId) => ref.model(MemoModel.document(editId))..load(),
onAdd: () => null,
);
final controller = ref.page.form(
MemoModel.form(memoData?.value ?? const MemoModel(title: "", text: "")),
);
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold();
}
フォームを作っていきましょう。
Masamuneフレームワーク用のフォームウィジェットが用意されているのでそれを利用していきます。
// memo_edit.dart
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text("Editing memo"),
actions: [
IconButton(
icon: const Icon(Icons.check),
onPressed: () async {
final value = controller.validate();
if (value == null) {
return;
}
if (ref.isEditing) {
await memoData?.save(value).showIndicator(context);
} else {
final memoData = ref.app.model(MemoModel.collection()).create();
await memoData.save(value).showIndicator(context);
}
router.pop();
},
),
],
),
body: UniversalColumn(
children: [
FormLabel("Title"),
FormTextField(
form: controller,
hintText: "Title",
initialValue: controller.value.title,
onSaved: (value) => controller.value.copyWith(
title: value,
),
),
FormLabel("Text"),
Expanded(
child: FormTextField(
form: controller,
hintText: "Text",
expands: true,
initialValue: controller.value.text,
style: const FormStyle(textAlignVertical: TextAlignVertical.top),
onSaved: (value) => controller.value.copyWith(
text: value,
),
),
)
],
),
);
FormLabel
で、フォームの項目ラベルを作成することができます。区切り線も兼ねているので
Divider
も合わせて利用するとまとまりのよいデザインを作成することができます。
FormTextField
でテキストフィールドを作成することができます。
form
にフォームコントローラー、
onSaved
にvalueを元に更新したフォームコントローラーを返すコールバックを指定します。
initialValue
にフォームコントローラーの値を渡すことで更新時の初期値を与えることができます。
保存ボタンが押されたときに
controller.validate()
を実行することでフォームの検証と
onSaved
の実行を行います。検証に失敗した場合は
null
が返されるのでその後の処理を中断することも可能です。
controller.validate()
で返された値をドキュメントの
save
に渡すことでフォームで入力された値が保存されます。
新規作成時はコレクションから
create
を実行して空のドキュメントを取得し、それを利用します。
showIndicator(context)
はすべての
Future
に付与することが可能で、
Future
が完了するまでインジケーターを表示してユーザーがコントロールできないようにしています。
新規作成ページへ遷移するボタンを
MemoListPage
に作成します。
// memo_list.dart
@override
Widget build(BuildContext context, PageRef ref) {
// Describes the process of loading
// and defining variables required for the page.
// TODO: Implement the variable loading process.
final memoList = ref.app.model(MemoModel.collection())..load();
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text("Memo List"),
),
body: UniversalListView(
children: memoList.mapListenable((e) {
return ListTile(
title: Text(e.value?.title ?? ""),
subtitle: Text(e.value?.createdAt.value.yyyyMMddHHmm() ?? ""),
onTap: () {
router.push(MemoDetailPage.query(memoId: e.uid));
},
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
router.push(MemoEditAddPage.query());
},
child: const Icon(Icons.add),
),
);
}
メモ内容編集ページへ遷移するボタンを
MemoDetailPage
に作成します。
// memo_detail.dart
@override
Widget build(BuildContext context, PageRef ref) {
// Describes the process of loading
// and defining variables required for the page.
// TODO: Implement the variable loading process.
final memoData = ref.app.model(MemoModel.document(memoId))..load();
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text(memoData.value?.title ?? ""),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: () {
router.push(MemoEditEditPage.query(editId: memoId));
},
),
],
),
body: UniversalListView(
padding: 16.p,
children: [
Text("投稿日時:${memoData.value?.createdAt.value.yyyyMMddHHmm() ?? ""}"),
16.sy,
Text(memoData.value?.text ?? ""),
],
),
);
}
メモの削除
データの削除はドキュメントの
delete
メソッドを実行します。
そのためのボタンを
MemoDetailPage
に作成していきましょう。
// memo_detail.dart
@override
Widget build(BuildContext context, PageRef ref) {
// Describes the process of loading
// and defining variables required for the page.
// TODO: Implement the variable loading process.
final memoData = ref.app.model(MemoModel.document(memoId))..load();
if (memoData.value == null) {
return UniversalScaffold();
}
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text(memoData.value?.title ?? ""),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
Modal.confirm(
context,
submitText: "Delete",
cancelText: "Cancel",
title: "Confirmation",
text: "Delete the memo. Are you sure?",
onSubmit: () async {
await memoData.delete().showIndicator(context);
router.pop();
},
);
},
),
IconButton(
icon: const Icon(Icons.save),
onPressed: () {
router.push(MemoEditEditPage.query(editId: memoId));
},
),
],
),
body: UniversalListView(
padding: 16.p,
children: [
Text("投稿日時:${memoData.value?.createdAt.value.yyyyMMddHHmm() ?? ""}"),
16.sy,
Text(memoData.value?.text ?? ""),
],
),
);
}
削除ボタンを押すと、
Model.confirm
で確認ダイアログを表示します。その後、削除が確定した場合
memoData.delete()
でドキュメント自体を削除します。
データを削除すると
memoData.value
が
null
になるのでそのときのために
UniversalScaffold
をそのまま返すようにしています。
デバッグするとフォームを編集できるようになりました。
フォームを編集して保存すると
データの変更を検知してページも更新されます。
このように
StatefulWidget
や
Riverpod
などで実装していた状態管理と画面の更新もMasamuneフレームワーク内で提供されるので画面の更新に悩む必要はありません。
番外編:Firestoreへの切り替え
現在はデータモックでアプリの確認を行うために
RuntimeModelAdapter
を利用しています。
これはアプリを起動し直すと元に戻る
ModelAdapter
です。
ちなみにこれで完成としてFirestoreで実運用を始めてみましょう。
その場合以前のように
modelAdapter
を切り替えるだけでOKです。
// adapter.dart
final modelAdapter = FirestoreModelAdapter(
options: DefaultFirebaseOptions.currentPlatform,
);
// final modelAdapter = RuntimeModelAdapter(
// initialValue: [
// MemoModelInitialCollection(
// {
// for (var i = 0; i < 10; i++)
// generateCode(32, seed: i): MemoModel(
// title: "メモタイトル$i",
// text: "メモテキスト$i",
// createdAt: ModelTimestamp(DateTime.now().subtract(i.h)),
// )
// },
// )
// ],
// );
アプリを立ち上げるとデータが空になっており
データを新規の保存すると
データ追加されます。
Firestoreにもデータが入っていることがわかります。
おわりに
アプリの中でも良く利用されるフォームもMasamuneフレームワークでは効率よく作成することができます。
前回からメモ帳アプリを作成しましたが、Masamuneフレームワークを利用することでかなり実装を短縮することができることが変わると思います。
次回はこのメモ帳アプリに「
テキスト検索
」と「
画像アップロード
」機能を追加してブラッシュアップしてみましょう。
Masamuneフレームワークは こちら にソースを公開しています。issueやPullRequestをお待ちしてます!
また仕事の依頼等ございましたら、私の Twitter や Webサイト で直接ご連絡をお願いいたします!
GitHub Sponsors
スポンサーを随時募集してます。ご支援お待ちしております!