Hello. I'm Masaru Hirose.
I introduced the Masamune framework, which enables stable and fast development of applications in Flutter.
I would like to write several introductory articles to let you know its charm and how to use it.
By mastering the Masamune framework, you will be able to develop ultra-fast, stable, and high-quality applications.
Since the last issue, I have started to create a "notepad application" and have even implemented a list of memos and display of their details.
This time, I aim to complete the CRUD application by allowing users to create, update, and delete memos.
Create and update memos
Forms
are created to create and update data within mobile and web apps.
Since I are basically editing the same content, there will be almost no difference in terms of screen when creating and updating.
However, the original data is not available at the time of creation , and the original data is required at the time of update .
The Masamune framework allows screens to be used interchangeably as much as possible and implemented in such a way as to separate data.
Open the project from the last time and execute the following command in a separate terminal to monitor the code changes.
katana code watch
The following command creates a template page for a form.
katana code tmp form memo_edit
The following class will be created in
pages/memo_edit.dart
.
MemoEditAddPage
is the page used to create a memo,
MemoEditEditPage
is the page used to update a memo, and
MemoEditForm
is the widget class to describe each form.
// 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();
}
}
Various functions for creating forms are provided in the
ref
passed to
MemoEditForm
's
build
method.
If
ref.isEditing
is
true
, the widget is being called because data is being updated.
In that case, the document ID for the edit is passed to
ref.editId
.
In addition, the
ref.select
method can be used to separate data for adding and editing.
This is used to load the original document data when updating.
In addition, the
form
method of the created data scheme can be used to obtain a controller for use in a 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();
}
Let's make a form.
A form widget for the Masamune framework is provided and will be used.
// 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
can be used to create form item labels. Since FormLabel also serves as a separator, it can be used in conjunction with
Divider
to create a cohesive design.
You can create text fields with
FormTextField
.
Specify a
form
controller in form and a callback that returns the updated form controller based on the value in
onSaved
.
By passing the form controller value to
initialValue
, you can give the initial value when updating.
When the save button is pressed,
controller.validate()
is executed to validate the form and perform
onSaved
. If validation fails,
null
is returned, so subsequent processing can be interrupted.
The value returned by
controller.validate()
is passed to the document save to
save
the value entered in the form.
When creating a new document, run
create
from the collection to obtain an empty document, which is then used.
The
showIndicator(context)
can be given to every
Future
, and the indicator is displayed until the
Future
is completed and cannot be controlled by the user.
Create a button on
MemoListPage
that will take you to the Create New page.
// 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),
),
);
}
Create a button on
MemoDetailPage
that moves to the memo content edit page.
// 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 ?? ""),
],
),
);
}
Deleting Notes
Data deletion is performed by executing the
delete
method of the document.
Let's create a button on the
MemoDetailPage
for this purpose.
// 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 ?? ""),
],
),
);
}
When the delete button is pressed, a confirmation dialog is displayed with
model.confirm
. Then, if the deletion is confirmed,
memoData.delete()
deletes the document itself.
If I delete data,
memoData.value
will be
null
, so I return
UniversalScaffold
as it is for that case.
After debugging, the form can now be edited.
If you edit the form and save it
Data changes are detected and the page is also updated.
Thus, there is no need to worry about updating screens since state management and screen updates, which were implemented with
StatefulWidget
and
Riverpod
, are also provided within the Masamune framework.
Extra: Switching to Firestore
Currently we are using
RuntimeModelAdapter
to check the app in the datamock.
This is a
ModelAdapter
that reverts back to its original state when the app is re-started.
By the way, now that this is completed, let's start actual operation with Firestore.
In that case, simply switch the
modelAdapter
as before.
// 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)),
// )
// },
// )
// ],
// );
When the application is launched, the data is empty.
When you save data to a new
Data will be added.
You can see that Firestore also contains data.
Conclusion
Forms that are often used in applications can also be created efficiently with the Masamune framework.
I have created a notepad application since the last issue, and I think it will change the fact that the implementation can be shortened considerably by using the Masamune framework.
Next time, let's brush up this notepad application by adding "
Text search
" and “
Image upload
" functions.
The Masamune framework is available here , and I welcome issues and PullRequests!
If you have any further job requests, please contact me directly through my Twitter or website !
GitHub Sponsors
Sponsors are always welcome. Thank you for your support!