起因

项目最早的写法是典型的“胖 Controller”:View(FXML 对应的 Controller)里既管 UI 交互,又直接调 Service 加载数据、刷新表格、维护选中状态。功能一多,单个 Controller 几千行,加载数据的逻辑和 UI 操作揉在一起,几乎没法单元测试,改一处怕动全身。

拆分

按 MVVM 把职责切开:

  • View:只管 UI 绑定和用户交互,从 UI 组件里提取纯数据(比如表格当前选中的行),不含业务。
  • ViewModel:持有状态(Property / ObservableList),负责数据加载的调度和参数构建,对 View 暴露可绑定的属性。
  • Service:纯逻辑,不碰 JavaFX,可在任意线程跑、可单测。

核心动作是把数据加载从 View 剥离到 ViewModel。原来 Controller 里 service.load() 之后跟一堆 UI 刷新,现在变成 ViewModel 加载完更新自己的 ObservableList,View 通过绑定自动刷新:

class RadarViewModel {
val files: ObservableList<RadarFileVo> = observableListOf()

fun load(projectId: String) = viewModelScope.launch {
files.setAll(service.loadFiles(projectId)) // 加载在 ViewModel
}
}

View 只需要绑定 files,在合适时机调 vm.load(id),完全不关心数据怎么来的。

收益

  • View 大幅变薄,Controller 回归“只做 UI”。
  • ViewModel 和 Service 不依赖 JavaFX UI 组件,能脱离界面写单测(尤其渲染参数构建、数据转换这种纯逻辑)。
  • 后续做“截图改 Java2D 纯数据渲染”时,正是因为渲染参数在 ViewModel 层就组装好了,Service 层可以完全不接触 UI 直接绘图。

小结

MVVM 的关键不是套个名字,而是真正把“状态 + 数据调度”从 View 里抽出来。一旦抽出来,可测性和可复用性立刻上一个台阶,后面所有模块的改造都顺了。