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.
In the previous issue, I discussed "Easy Connection to Firestore".
The ability to seamlessly migrate from local databases to Firestore is appealing.
I would like to start actual application development in the third session.
I will develop the simplest "Notepad application
".
Basic Concept
The basic concept in creating an application is described below in advance.
CRUD(Create Read Update Delete)
This is the basic data handling in the application.
Create data
, Retrieve data
, Update data
, Delete data
. It is said that most applications can be created with just this process between database = application.
The Masamune framework provides features that make CRUD very easy to handle.
Documents and Collections
The Masamune framework employs a data structure of documents
that collect multiple Firestore key/value pairs and collections
of those documents.
For more information, please see below.
How to develop apps in Masamune
With the Masamune framework, you can create a CRUD application by simply building a data model
that basically determines how data will be stored and a page
that determines how the screen will be displayed.
You are free to think about the data model
first based on how the application is used or the pages
first based on the image of the application.
In this issue, I would like to consider this from the data model
.
Creating a data model for Notepad
If I were to create a simple notepad, I would need the following data.
- Title of memo
- Text of memo
- Date and time the memo was posted
Now let's actually make this work.
Open the previous Flutter project and enter the following command in a separate terminal
katana code watch
This ensures that file changes are monitored and auto-generated files are created immediately.
In addition, enter the Create Data Model command.
katana code collection memo
The following MemoModel
will be created in models/memo.dart
.
// memo.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath("memo")
class MemoModel with _$MemoModel {
const factory MemoModel({
// TODO: Set the data schema.
}) = _MemoModel;
const MemoModel._();
factory MemoModel.fromJson(Map<String, Object?> json) => _$MemoModelFromJson(json);
/// Query for document.
///
/// ```dart
/// appRef.model(MemoModel.document(id)); // Get the document.
/// ref.model(MemoModel.document(id))..load(); // Load the document.
/// ```
static const document = _$MemoModelDocumentQuery();
/// Query for collection.
///
/// ```dart
/// appRef.model(MemoModel.collection()); // Get the collection.
/// ref.model(MemoModel.collection())..load(); // Load the collection.
/// ref.model(
/// MemoModel.collection().data.equal(
/// "data",
/// )
/// )..load(); // Load the collection with filter.
/// ```
static const collection = _$MemoModelCollectionQuery();
/// Query for form value.
///
/// ```dart
/// ref.form(MemoModel.form(MemoModel())); // Get the form controller.
/// ```
static const form = _$MemoModelFormQuery();
}
Add the following parameters to this constructor part.
const factory MemoModel({
// TODO: Set the data schema.
required String title,
required String text,
@Default(ModelTimestamp()) ModelTimestamp createdAt,
}) = _MemoModel;
It corresponds to the memo title (title
), memo body (text
), and memo posting date and time (createdAt
).
ModelTimestamp
is a special class for handling dates and times within the Masamune framework.
When you save the file, katana code watch
will automatically create memo.freed.dart
, memo.g.dart
, and memo.m.dart
.
This completes the data model definition.
Create memo list page
Next, a memo list page is created based on the memo data created above.
The following command creates a page.
katana code page memo_list
The following MemoListPage
will be created in pages/memo_list.dart
.
// memo_list.dart
@immutable
// TODO: Set the path for the page.
@PagePath("memo_list")
class MemoListPage extends PageScopedWidget {
const MemoListPage({
super.key,
// TODO: Set parameters for the page.
});
// TODO: Set parameters for the page in the form [final String xxx].
/// Used to transition to the MemoListPage screen.
///
/// ```dart
/// router.push(MemoListPage.query(parameters)); // Push page to MemoListPage.
/// router.replace(MemoListPage.query(parameters)); // Replace page to MemoListPage.
/// ```
@pageRouteQuery
static const query = _$MemoListPageQuery();
@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.
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold();
}
}
First, the process for loading memo list data is described in the build
method.
// 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();
}
Arrange the contents of memoList
using UniversalListView
and ListTile
.
(UniversalListView
is a ListView
available within the Masamune framework)
// memo_list.dart
// 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() ?? ""),
);
}),
),
);
A memoList
is converted to a ListTile
with mapListenable
.
By using mapListenable
, the contents of the ListTile
will be automatically updated when each element of the memoList
is updated.
This completes the creation of the memo list screen.
Creating and debugging mock data
You may be currently connected to Firestore, but in order to check the status of the application data, disconnect it and display mock data instead.
Mock data can be handled by RuntimeModelAdapter
.
Open main.dart
and rewrite it as follows
// main.dart
import 'models/memo.dart';
~~~~~~
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)),
)
},
)
],
);
// final modelAdapter = FirestoreModelAdapter(
// options: DefaultFirebaseOptions.currentPlatform,
// );
Mock data can be specified for initialValue
in the RuntimeModelAdapter
.
Each mock data is defined by passing the automatically created ModelInitialCollection data, including pairs of IDs and Model objects.
The ID can be created in any way, but since a 32-digit string is used within the Masamune framework, a 32-digit random string is created using generateCode
.
Also in main.dart
, set the first page to MemoListPage
.
// main.dart
import 'pages/memo_list.dart';
~~~~~~~~
final initialQuery = MemoListPage.query();
// final initialQuery = HomePage.query();
Now let's do a debug build in this state.
You see a list of memos.
Creation of memo content page
Let's display the contents of the memo.
The following command creates a page.
katana code page memo_detail
A MemoDetailPage
will be created in pages/memo_detail.dart
.
// memo_detail.dart
@immutable
// TODO: Set the path for the page.
@PagePath("memo_detail")
class MemoDetailPage extends PageScopedWidget {
const MemoDetailPage({
super.key,
// TODO: Set parameters for the page.
});
// TODO: Set parameters for the page in the form [final String xxx].
/// Used to transition to the MemoDetailPage screen.
///
/// ```dart
/// router.push(MemoDetailPage.query(parameters)); // Push page to MemoDetailPage.
/// router.replace(MemoDetailPage.query(parameters)); // Replace page to MemoDetailPage.
/// ```
@pageRouteQuery
static const query = _$MemoDetailPageQuery();
@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.
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold();
}
}
First, the memo ID
to be displayed is required and is specified as an argument.
// memo_detail.dart
const MemoDetailPage({
super.key,
// TODO: Set parameters for the page.
required this.memoId,
});
// TODO: Set parameters for the page in the form [final String xxx].
final String memoId;
Describe the process for loading the corresponding memo data based on the memo ID in the build
method.
// 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();
}
Create UI based on memoData
.
// memo_detail.dart
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text(memoData.value?.title ?? ""),
),
body: UniversalListView(
padding: 16.p,
children: [
Text("投稿日時:${memoData.value?.createdAt.value.yyyyMMddHHmm() ?? ""}"),
16.sy,
Text(memoData.value?.text ?? ""),
],
),
);
Allows the user to navigate to MemoDetailPage
from each element of MemoListPage
.
// memo_list.dart
// 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));
},
);
}),
),
);
The uid
of each document is the ID set in the document itself, so it can be passed as memoId
as it is.
Let's debug with this.
A memo detail page has been created.
Conclusion
As you may have noticed during the creation process, I have written almost exclusively for the UI.
In this way, the Masamune framework can minimize implementation around data by combining automatic generation by command and automatic generation by katana code watch
.
If the UI side also uses GithubCopilot, etc., it may be possible to further shorten the implementation by using suggestions. It's a good time!
In the next issue, I would like to proceed with the implementation of the memo data addition/update section. Please look forward to it!
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!