Hello. I'm Masaru Hirose.
I am constantly updating Masamune, an integration framework that runs on Flutter.
This time, I would like to introduce an update that could significantly change the app development experience.
masamune
Introduction
Masamune framework is a framework on Flutter that uses " Code generation by CLI " and " Automatic code generation by build_runnner " to reduce the amount of code writing as much as possible and increase stability and speed of application development.
As for the database, I use a NoSQL database based on
Firestore
and provide a mechanism that allows switching between the
runtime DB
, the
terminal local DB
and
Firestore
with just one line of update.
Previously, I provided a mechanism to replace all DBs in an app by switching
ModelAdapter
,
but starting from
Masamune Ver. 2.3.4
it is now possible to set a
ModelAdapter
for each data model
.
In other words, you can have different databases for different user data, content data, and associated data, and even link them together without awareness .
Let's take you step-by-step through what can be done.
Type and location of data
Data handled by the application should be divided into the following three categories depending on where the data is placed.
-
Data defined in the app
-
Fixed data list
- Category
- Gender
- Age-group
- Area
-
In-app settings
- Maximum number of characters to post
- Registration limit for favorites
-
Often in code with
enumorconst.
-
Fixed data list
-
Data stored locally on the terminal
-
Data to be viewed by you only
- Memorandum data
- Schedule
-
Data to store the status of the application
- Have the user completed the tutorial?
- Trophy Data
- Volume (sound) volume
- App Theme
-
Often saved as a file in the terminal or in
SharedPreferences
-
Data to be viewed by you only
-
Data stored in remote DB
-
Data to be shared with other users
- User Profile Data
- Article data from blogs, etc.
- Communication data such as direct messages
-
Data Persistence
- Enable "data to be viewed only by you" of "data stored locally on the terminal" to be viewed on a different terminal or even if you change the terminal.
-
Stored in
Firestore,RDB, etc.
-
Data to be shared with other users
These will vary depending on the requirements of the application.
(For example, if categories are not fixed but can be added or deleted freely by users or administrators, they need to be stored in a remote DB.)
In
Masamune framework
, it can be handled as 1 ->
RuntimeDatabase
, 2 ->
LocalDatabase
, 3 ->
FirestoreDatabase
.
Relationships between different DBs
Consider the following data.
-
Article data (use
FirestoreDatabaseto share with all users)- Article name
- Article content
-
Category data (use
RuntimeDatabasefor fixed data in app)- Category name
-
User data (use
FirestoreDatabaseto share with all users)- User name
- Icon
-
App configuration data (use
LocalDatabasefor in-app settings)- App theme
- Have you completed the tutorial?
The following relationships can be set up at this time.
-
Article data (use
FirestoreDatabaseto share with all users)- Article name
- Article content
- Reference to category data .
-
Category data (use
RuntimeDatabasefor fixed data in app)- Category name
-
User data (use
FirestoreDatabaseto share with all users)- User name
- Icon
- Reference to application configuration data .
-
App configuration data (use
LocalDatabasefor in-app settings)- App theme
- Have you completed the tutorial?
The
Masamune framework
executes the
load()
method when loading specific data, which
also loads the data contained in the reference
.
In other words, simply loading the article data not only reads the data stored in Firestore, but also the category data defined on the accompanying Runtime, which can be read together and treated as the same data .
In the same way, user data stored in Firestore and application settings data stored locally can be loaded together as user-owned data.
Database migration
For example, if a user wants to be able to add or delete categories that have been set as fixed, it is usually necessary to create a separate category table and implement a separate process to read/write data to/from it. This is a modification to a certain extent.
In the
Masamune framework
, if categories are defined in advance in the
RuntimeDatabase
, a single line change is all that is required.
Also, if you change the DB, you need to migrate the previous data to the new database, but you don't need to worry about that if you use
RuntimeDatabase
to
LocalDatabase
or
FirestoreDatabase
.
Let's actually do it!
Now I would like to proceed with the actual implementation.
First, create a project with
katana create
.
katana create net.mathru.modeltest
Wait a few moments and the project file will be created.
Next, let's create various data models.
# Article Data
katana code collection post
# Category Data
katana code collection category
# User data
katana code collection user
# Application setting data
katana code document prefs
App configuration data is created in documents, not collections.
Set up the fields for each model.
// models/post.dart
/// Value for model.
@freezed
@formValue
@immutable
@CollectionModelPath("post")
abstract class PostModel with _$PostModel {
const factory PostModel({
required String title,
required String text,
@refParam CategoryModelRef category,
}) = _PostModel;
const PostModel._();
~~~~
}
// models/category.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath("category")
abstract class CategoryModel with _$CategoryModel {
const factory CategoryModel({
required String name,
}) = _CategoryModel;
const CategoryModel._();
~~~~
}
// models/user.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath("user")
abstract class UserModel with _$UserModel {
const factory UserModel({
required String name,
ModelImageUri? icon,
@refParam PrefsModelRef? prefs,
}) = _UserModel;
const UserModel._();
~~~~
}
// models/prefs.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the document.
@DocumentModelPath("app/prefs")
abstract class PrefsModel with _$PrefsModel {
const factory PrefsModel({
String? theme,
@Default(false) bool isFinishTutorial,
}) = _PrefsModel;
const PrefsModel._();
~~~~
}
The following command will perform automatic generation by
build_runner
.
katana code generate
Firestore will be used, so please refer to the following article to enable Firestore.
Create a
FirestoreModelAdapter
by specifying
FirebaseOptions
for each platform to be written in
const
.
// main.dart
~~~~~
const firestoreModelAdapter = FirestoreModelAdapter(
iosOptions: DefaultFirebaseOptions.ios,
androidOptions: DefaultFirebaseOptions.android,
);
In addition, define a
RuntimeModelAdapter
and a
LocalModelAdapter
.
Pre-add category data to the
RuntimeModelAdapter
.
// main.dart
~~~~~
const localModelAdapter = LocalModelAdapter();
const runtimeModelAdapter = RuntimeModelAdapter(
initialValue: [
CategoryModelDataCollection(
{
"beauty_and_health": CategoryModel(
name: "Beauty & Health",
),
"it_and_programming": CategoryModel(
name: "IT & Programming",
),
"business": CategoryModel(
name: "Business",
),
},
),
],
);
You will specify a
ModelAdapter
for each model.
// models/post.dart
/// Value for model.
@freezed
@formValue
@immutable
@CollectionModelPath(
"post",
adapter: firestoreModelAdapter,
)
abstract class PostModel with _$PostModel {
const factory PostModel({
required String title,
required String text,
@refParam CategoryModelRef category,
}) = _PostModel;
const PostModel._();
~~~~
}
// models/category.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath(
"category",
adapter: runtimeModelAdapter,
)
abstract class CategoryModel with _$CategoryModel {
const factory CategoryModel({
required String name,
}) = _CategoryModel;
const CategoryModel._();
~~~~
}
// models/user.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath(
"user",
adapter: firestoreModelAdapter,
)
abstract class UserModel with _$UserModel {
const factory UserModel({
required String name,
ModelImageUri? icon,
@refParam PrefsModelRef? prefs,
}) = _UserModel;
const UserModel._();
~~~~
}
// models/prefs.dart
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the document.
@DocumentModelPath(
"app/prefs",
adapter: localModelAdapter,
)
abstract class PrefsModel with _$PrefsModel {
const factory PrefsModel({
String? theme,
@Default(false) bool isFinishTutorial,
}) = _PrefsModel;
const PrefsModel._();
~~~~
}
Now that the adapter has been changed, automatic generation by
build_runner
is performed again.
katana code generate
This completes the implementation on the data model side.
Create a page to review the data.
In this case, for the purpose of reviewing data, I will put it all on one page, but in reality, it will be like creating a profile page or an article page.
Edit
pages/home.dart
.
// pages/home.dart
// ignore: unused_import, unnecessary_import
import 'package:flutter/material.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune/masamune.dart';
import 'package:masamune_universal_ui/masamune_universal_ui.dart';
import 'package:modeltest/models/category.dart';
import 'package:modeltest/models/post.dart';
import 'package:modeltest/models/prefs.dart';
import 'package:modeltest/models/user.dart';
// ignore: unused_import, unnecessary_import
import '/main.dart';
part 'home.page.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.
const userId = "user1";
final user = ref.model(UserModel.document(userId))..load();
final posts = ref.model(PostModel.collection())..load();
final categories = ref.model(CategoryModel.collection())..load();
// Describes the structure of the page.
return UniversalScaffold(
appBar: UniversalAppBar(title: Text(l().appTitle)),
body: UniversalListView(
children: [
const ListTile(
title: Text(
"User Data",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Divider(color: theme.color.outline.withOpacity(0.25)),
if (user.value != null)
ListTile(
title: Text(user.value?.name ?? ""),
subtitle: Text(user.value?.prefs?.value?.theme ?? ""),
trailing: FormSwitch(
initialValue:
user.value?.prefs?.value?.isFinishTutorial ?? false,
onChanged: (value) async {
await user.value?.prefs?.save(
user.value?.prefs?.value?.copyWith(isFinishTutorial: value),
);
},
),
)
else
ListTile(
leading: const Icon(Icons.add),
title: const Text("Create user data"),
onTap: () async {
final prefs = ref.model(PrefsModel.document());
await prefs.load().showIndicator(context);
if (prefs.value == null) {
await prefs.save(const PrefsModel()).showIndicator(context);
}
await user
.save(
UserModel(
name: "User at ${DateTime.now().yyyyMMddHHmmss()}",
prefs: prefs,
),
)
.showIndicator(context);
},
),
const Divider(),
const ListTile(
title: Text(
"Post Data",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Divider(color: theme.color.outline.withOpacity(0.25)),
...posts.mapListenable((post) {
return ListTile(
title: Text(post.value?.title ?? ""),
subtitle: Text(post.value?.category?.value?.name ?? ""),
);
}),
Divider(color: theme.color.outline.withOpacity(0.25)),
...categories.map((category) {
return ListTile(
leading: const Icon(Icons.add),
title: Text(category.value?.name ?? ""),
onTap: () async {
final newPost = posts.create();
await newPost
.save(
PostModel(
title: "Post at ${DateTime.now().yyyyMMddHHmmss()}",
text: "",
category: category,
),
)
.showIndicator(context);
},
);
}),
],
),
);
}
}
When the application is built, the following screen is launched.
Tap
Create user data
to create user data.
When you open Firestore, this is how the data is stored.
If checked, it will be written locally and the data will be persistent.
The check remains checked even after the application is started again.
Tap each category to create a post.
Check Firestore and you will find the following data stored.
You can see that on the Firestore, only the reference path is stored in the
category
field, but the app is able to pick up the
name
data from that path and display the category name as well.
Finally, let's move the category to Firestore Management.
Open
main.dart
and port the
RuntimeModelAdapter
category setting to
FirestoreModelAdapter
.
// main.dart
~~~~~~
const firestoreModelAdapter = FirestoreModelAdapter(
iosOptions: DefaultFirebaseOptions.ios,
androidOptions: DefaultFirebaseOptions.android,
initialValue: [
CategoryModelInitialCollection(
{
"beauty_and_health": CategoryModel(
name: "Beauty & Health",
),
"it_and_programming": CategoryModel(
name: "IT & Programming",
),
"business": CategoryModel(
name: "Business",
),
},
),
],
);
const localModelAdapter = LocalModelAdapter();
const runtimeModelAdapter = RuntimeModelAdapter();
Change
adapter
in
models/category.dart
to
firestoreModelAdapter
.
// models/category.dart
~~~~~~
/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath(
"category",
adapter: firebaseModelAdapter,
)
abstract class CategoryModel with _$CategoryModel {
const factory CategoryModel({
required String name,
}) = _CategoryModel;
const CategoryModel._();
~~~~~~
}
When You build and start the program, You see that You can get the category names as before.
Let's add data to Firestore to try it out.
Restart the application and you will see that the data has been reflected.
Conclusion
This new feature allows you to write code that specifies the data source in detail.
The following various benefits will be created.
- Creation of mockups → shortened server implementation
- Centralized management of all data
- Securely specify data by defining data schemes
I made it for my own use, but if you think it fits your implementation philosophy, by all means, use it!
Also, I releasing the source here , so issues and PullRequests are welcome!
If you have any further work requests, please contact me directly through my Twitter or website !
GitHub Sponsors
Sponsors are always welcome. Thank you for your support!