Next2D FrameworkはMVVM(Model-View-ViewModel)パターンを採用しています。1画面にViewとViewModelをワンセット作成するのが基本スタイルです。
graph TB
subgraph ViewLayer["View Layer"]
ViewRole["画面の構造と表示を担当"]
ViewRule["ビジネスロジックは持たない"]
end
subgraph ViewModelLayer["ViewModel Layer"]
VMRole1["ViewとModelの橋渡し"]
VMRole2["UseCaseを保持"]
VMRole3["イベントハンドリング"]
end
subgraph ModelLayer["Model Layer"]
ModelRole1["ビジネスロジック(UseCase)"]
ModelRole2["データアクセス(Repository)"]
end
ViewLayer <-->|双方向| ViewModelLayer
ViewModelLayer <--> ModelLayer
src/
└── view/
├── top/
│ ├── TopView.ts
│ └── TopViewModel.ts
└── home/
├── HomeView.ts
└── HomeViewModel.ts
Viewはメインコンテキストにアタッチされるコンテナです。Viewは表示構造のみを担当し、ビジネスロジックはViewModelに委譲します。
initialize, onEnter, onExitimport type { TopViewModel } from "./TopViewModel";
import { View } from "@next2d/framework";
import { TopPage } from "@/ui/component/page/top/TopPage";
export class TopView extends View<TopViewModel>
{
private readonly _topPage: TopPage;
constructor(vm: TopViewModel)
{
super(vm);
this._topPage = new TopPage();
this.addChild(this._topPage);
}
async initialize(): Promise<void>
{
this._topPage.initialize(this.vm);
}
async onEnter(): Promise<void>
{
await this._topPage.onEnter();
}
async onExit(): Promise<void>
{
return void 0;
}
}
sequenceDiagram
participant Framework as Framework
participant VM as ViewModel
participant View as View
participant UI as UI Components
Note over Framework,UI: 画面遷移開始
Framework->>VM: new ViewModel()
Framework->>VM: initialize()
Note over VM: ViewModelが先に初期化される
Framework->>View: new View(vm)
Framework->>View: initialize()
View->>UI: コンポーネント作成
View->>VM: イベントリスナー登録
Framework->>View: onEnter()
View->>UI: アニメーション開始
Note over Framework,UI: ユーザーが画面を操作
Framework->>View: onExit()
View->>UI: クリーンアップ
呼び出しタイミング:
initialize()より後に実行される主な用途:
addChild)async initialize(): Promise<void>
{
const { HomeBtnMolecule } = await import("@/ui/component/molecule/HomeBtnMolecule");
const { PointerEvent } = next2d.events;
const homeContent = new HomeBtnMolecule();
homeContent.x = 120;
homeContent.y = 120;
// イベントをViewModelに委譲
homeContent.addEventListener(
PointerEvent.POINTER_DOWN,
this.vm.homeContentPointerDownEvent
);
this.addChild(homeContent);
}
呼び出しタイミング:
initialize()の実行完了後主な用途:
async onEnter(): Promise<void>
{
const topBtn = this.getChildByName("topBtn") as TopBtnMolecule;
topBtn.playEntrance(() => {
console.log("アニメーション完了");
});
}
呼び出しタイミング:
主な用途:
async onExit(): Promise<void>
{
if (this.autoSlideTimer) {
clearInterval(this.autoSlideTimer);
this.autoSlideTimer = null;
}
}
ViewModelはViewとModelの橋渡しを行います。UseCaseを保持し、Viewからのイベントを処理してビジネスロジックを実行します。
import { ViewModel, app } from "@next2d/framework";
import { NavigateToViewUseCase } from "@/model/application/top/usecase/NavigateToViewUseCase";
export class TopViewModel extends ViewModel
{
private readonly navigateToViewUseCase: NavigateToViewUseCase;
private topText: string = "";
constructor()
{
super();
this.navigateToViewUseCase = new NavigateToViewUseCase();
}
async initialize(): Promise<void>
{
// routing.jsonのrequestsで取得したデータを受け取る
const response = app.getResponse();
this.topText = response.has("TopText")
? (response.get("TopText") as { word: string }).word
: "";
}
getTopText(): string
{
return this.topText;
}
async onClickStartButton(): Promise<void>
{
await this.navigateToViewUseCase.execute("home");
}
}
重要: ViewModelのinitialize()はViewのinitialize()より前に呼び出されます。
1. ViewModel のインスタンス生成
↓
2. ViewModel.initialize() ← ViewModelが先
↓
3. View のインスタンス生成(ViewModelを注入)
↓
4. View.initialize()
↓
5. View.onEnter()
これにより、Viewの初期化時にはViewModelのデータが既に準備されています。
// HomeViewModel.ts
export class HomeViewModel extends ViewModel
{
private homeText: string = "";
async initialize(): Promise<void>
{
// ViewModelのinitializeで事前にデータ取得
const data = await HomeTextRepository.get();
this.homeText = data.word;
}
getHomeText(): string
{
return this.homeText;
}
}
// HomeView.ts
export class HomeView extends View<HomeViewModel>
{
constructor(private readonly vm: HomeViewModel)
{
super();
}
async initialize(): Promise<void>
{
// この時点でvm.initialize()は既に完了している
const text = this.vm.getHomeText();
// 取得済みのデータを使ってUIを構築
const textField = new TextAtom(text);
this.addChild(textField);
}
}
画面遷移にはapp.gotoView()を使用します。
import { app } from "@next2d/framework";
// 指定のViewに遷移
await app.gotoView("home");
// パラメータ付きで遷移
await app.gotoView("user/detail?id=123");
import { app } from "@next2d/framework";
export class NavigateToViewUseCase
{
async execute(viewName: string): Promise<void>
{
await app.gotoView(viewName);
}
}
routing.jsonで設定したrequestsのデータはapp.getResponse()で取得できます。
import { app } from "@next2d/framework";
async initialize(): Promise<void>
{
const response = app.getResponse();
if (response.has("UserData")) {
const userData = response.get("UserData");
this.userName = userData.name;
}
}
cache: trueを設定したデータはapp.getCache()で取得できます。
import { app } from "@next2d/framework";
const cache = app.getCache();
if (cache.has("MasterData")) {
const masterData = cache.get("MasterData");
}
// 良い例: Viewは表示のみ、ViewModelはロジック
class HomeView extends View<HomeViewModel>
{
async initialize(): Promise<void>
{
const btn = new HomeBtnMolecule();
btn.addEventListener(PointerEvent.POINTER_DOWN, this.vm.onClick);
}
}
class HomeViewModel extends ViewModel
{
onClick(event: PointerEvent): void
{
this.someUseCase.execute();
}
}
ViewModelはインターフェースに依存し、具象クラスに依存しません。
// 良い例: インターフェースに依存
homeContentPointerDownEvent(event: PointerEvent): void
{
const target = event.currentTarget as unknown as IDraggable;
this.startDragUseCase.execute(target);
}
View内でイベント処理を完結させず、必ずViewModelに委譲します。
import type { YourViewModel } from "./YourViewModel";
import { View } from "@next2d/framework";
export class YourView extends View<YourViewModel>
{
constructor(vm: YourViewModel)
{
super(vm);
}
async initialize(): Promise<void>
{
// UIコンポーネントの作成と配置
}
async onEnter(): Promise<void>
{
// 画面表示時の処理
}
async onExit(): Promise<void>
{
// 画面非表示時の処理
}
}
import { ViewModel } from "@next2d/framework";
import { YourUseCase } from "@/model/application/your/usecase/YourUseCase";
export class YourViewModel extends ViewModel
{
private readonly yourUseCase: YourUseCase;
constructor()
{
super();
this.yourUseCase = new YourUseCase();
}
async initialize(): Promise<void>
{
return void 0;
}
yourEventHandler(event: Event): void
{
this.yourUseCase.execute();
}
}