2023-07-02

[Flutter] Ultra-fast app development with Masamune (4) "Notepad application creation - Create and edit memos”

Hello. I'm Masaru Hirose.

I introduced the Masamune framework, which enables stable and fast development of applications in Flutter.

Hello. My name is Masaru Hirose.
https://medium.comhttps://medium.com
title

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.

Hello. My name is Masaru Hirose.
https://medium.comhttps://medium.com
title
// 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.

image block

If you edit the form and save it

image block

Data changes are detected and the page is also updated.

image block

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.

image block

When you save data to a new

image block

Data will be added.

image block

You can see that Firestore also contains data.

image block

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!

Offers app development and apps using Flutter and Unity. Includes information on music and videos created by the company. Distribution of images and video materials. We also accept orders for work.
https://mathru.nethttps://mathru.net
title

GitHub Sponsors

Sponsors are always welcome. Thank you for your support!

Developed the katana/masamune framework, which has dramatically improved the overall efficiency of Flutter-based application development.
https://github.comhttps://github.com
title


{content}

[Flutter] Ultra-fast app development with Masamune (5) "Notepad application creation - Text search and Image upload”

Development Flutter Dart pub Plugins Masamune

◀︎ Next
[Flutter] Ultra-fast app development with Masamune (3) "Notepad application creation - Display of notes”

Development Flutter Dart pub Plugins Masamune

▶︎ Previous