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.
The first session is "Project Creation".
Introduction
Naturally, you will need to install Flutter.
Install katana_cli
.
flutter pub global activate katana_cli
Run katana doctor
and check that the necessary commands are installed.
If the required commands are not present, install them as needed.
$ katana doctor
#### Check the installation status of the commands required for the `katana` command.
[o] `flutter` exists at `/Users/xxx/.flutter/bin/flutter`
[o] `dart` exists at `/Users/xxx/.flutter/bin/dart`
[o] `npm` exists at `/usr/local/bin/npm`
[o] `git` exists at `/usr/bin/git`
[o] `keytool` exists at `/opt/homebrew/opt/openjdk/bin/keytool`
[o] `openssl` exists at `/usr/bin/openssl`
[o] `pod` exists at `/usr/local/bin/pod`
#### Check the installation status of the npm commands required for the `katana` command.
[o] `eslint` exists at `/usr/local/bin/eslint`
[o] `firebase` exists at `/usr/local/bin/firebase`
[o] `lefthook` exists at `/opt/homebrew/bin/lefthook`
[o] `gh` exists at `/opt/homebrew/bin/gh`
#### Check the installation status of the flutter commands required for the `katana` command.
[o] `flutterfire` exists at `/Users/xxx/.pub-cache/bin/flutterfire`
Create Project
Now that you have done the preliminary work, let's get started on the project!
Create an empty folder and execute the following command under it.
katana create [Application ID(e.g. com.test.myapplication)]
This time I will create an application with an application ID of net.mathru.test
.
katana create net.mathru.test
Wait a few moments and the initial project will be created.
Once completed, do a test build.
flutter run --dart-define=FLAVOR=dev
If you are using VisualStudioCode, launch.json
is created and can be debugged by F5
.
The familiar counter application is now activated.
Explanation
The lib
folder structure looks like this.
In these files, models/counter.freezed.dart
, models/counter.g.dart
, models/counter.m.dart
, pages/home.page.dart
, main.localize.dart
, main.theme.dart
are automatically created files, so I omit their description.
main.dart
A file containing the entry points of the application.
Other settings such as application themes, translations, routing, adapters, etc. can be configured. This will be explained shortly.
models/counter.dart
A file in which the model portion (data portion) of the counter application is described.
An object that can be handled immutably (freezed
), a function that can serialize and deserialize to Json (json_serializable
), and a function that allows input and output to the database through that object (masamune_builder
) are created together.
// counter.dart
/// Value for model.
@freezed
@formValue
@immutable
@DocumentModelPath("app/counter")
class CounterModel with _$CounterModel {
const factory CounterModel({
@Default(ModelCounter(0)) ModelCounter counter,
}) = _CounterModel;
const CounterModel._();
factory CounterModel.fromJson(Map<String, Object?> json) => _$CounterModelFromJson(json);
/// Query for document.
///
/// ```dart
/// appRef.model(CounterModel.document()); // Get the document.
/// ref.model(CounterModel.document())..load(); // Load the document.
/// ```
static const document = _$CounterModelDocumentQuery();
/// Query for form value.
///
/// ```dart
/// ref.form(CounterModel.form(CounterModel())); // Get the form controller.
/// ```
static const form = _$CounterModelFormQuery();
}
If the following part of the above code is augmented with parameters according to the freezed and json_serializable conventions, it will become the data scheme
for storing the data in the database.
const factory CounterModel({
@Default(ModelCounter(0)) ModelCounter counter,
}) = _CounterModel;
ModelCounter
is a special class that performs counting.
The value can be increased or decreased by counter.increment(1)
.
In this pattern, int
is fine, but it is very useful to be able to count values without conflicts when using Firestore later on.
pages/home.dart
A file containing the views of the counter application.
// home.dart
@immutable
@PagePath("/")
class HomePage extends PageScopedWidget {
const HomePage({
super.key,
});
/// Used to transition to the HomePage screen.
///
/// ```dart
/// router.push(HomePage.query(parameters)); // Push page to HomePage.
/// router.replace(HomePage.query(parameters)); // Replace page to HomePage.
/// ```
@pageRouteQuery
static const query = _$HomePageQuery();
@override
Widget build(BuildContext context, PageRef ref) {
// Describes the process of loading
// and defining variables required for the page.
final model = ref.model(CounterModel.document())..load();
// Describes the structure of the page.
return UniversalScaffold(
appBar: UniversalAppBar(title: Text(l().appTitle)),
body: UniversalColumn(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
"You have pushed the button this many times:",
),
Text(
"${model.value?.counter.value ?? 0}",
style: theme.text.displayMedium,
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final value = model.value ?? const CounterModel();
model.save(
value.copyWith(counter: value.counter.increment(1)),
);
},
tooltip: "Increment",
child: const Icon(Icons.add),
),
);
}
}
The following line obtains the CounterModel
object introduced earlier and reads it from the database. model.value
contains the value read from the database, or null
if there is no value in the database to begin with.
The view is rebuilt as appropriate when reads from the database are completed or updated.
final model = ref.model(CounterModel.document())..load();
Here, the counter value of CounterModel
is directly retrieved and displayed.
Text(
"${model.value?.counter.value ?? 0}",
style: theme.text.displayMedium,
),
Counting up here.
If there is no value in the database, model.value
will be null
, in which case a new CounterModel
is created.
The value is then saved as is by passing the CounterModel
to be saved to model.save
.
final value = model.value ?? const CounterModel();
model.save(
value.copyWith(counter: value.counter.increment(1)),
);
Data Persistence
As it is now, the count goes back to 0 when the app is restarted, just like the usual Flutter counter app.
So, let's create a local DB in the terminal and make the data persistent.
Open main.dart and replace modelAdapter
with LocalModelAdapter
instead of RuntimeModelAdapter
.
// main.dart
final modelAdapter = LocalModelAdapter();
// final modelAdapter = RuntimeModelAdapter();
With just this, the counter value will be retained without going to zero even if the application is restarted.
Conclusion
In the next issue, I will try to use Firestore to maintain data even if the app is reinstalled. Enjoy!
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!