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.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.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.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.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.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.
// main.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!
{content}