4. MVP架构入门:Model-View-Presenter在TouchGFX中的实现
MVP架构,说白了就是让界面和逻辑各司其职。很多初学者写TouchGFX代码,喜欢把业务逻辑直接塞进View里——按钮回调里直接读写数据、处理网络请求。一开始觉得挺爽,代码全在一个文件里。等界面改版或者要加新功能,那叫一个痛苦。
我最早做嵌入式界面时也踩过这个坑。一个温控器项目,界面逻辑和数据操作全混在一起,客户要换屏幕尺寸,我几乎重写了所有代码。后来接触了MVP,才明白什么叫「解耦」。今天我们就来拆解一下,MVP在TouchGFX里到底怎么落地。
4.1 什么是MVP?三个角色的分工
MVP把界面程序拆成三块:Model、View、Presenter。它们各管一摊,互不越界。
| 角色 | 职责 | TouchGFX中的对应 |
|---|---|---|
| Model | 数据层,负责业务数据和逻辑 | 自定义数据类、硬件驱动封装 |
| View | 界面层,只负责显示和用户交互 | Screen / View 类(.hpp / .cpp) |
| Presenter | 中介层,协调View和Model | Presenter 类(自动生成) |
你想想看,View不知道数据从哪来,Model不知道界面长什么样。Presenter夹在中间,像翻译官一样传递消息。这样改界面不影响数据,换数据源也不动界面。
核心原则:View只做两件事——把用户操作告诉Presenter,把Presenter给的数据显示出来。Model只做一件事——管理数据,通知变化。Presenter负责所有决策和调度。
4.2 TouchGFX中的MVP骨架
当你用TouchGFX Designer新建一个Screen时,它会自动生成三个文件:
ScreenView.hpp / ScreenView.cpp—— View层ScreenPresenter.hpp / ScreenPresenter.cpp—— Presenter层Screen.hpp—— 接口定义
嗯,这里要注意:TouchGFX生成的Presenter默认是空的,Model需要你自己建。我习惯在项目里单独建一个 model/ 目录,放所有数据模型类。
举个例子,一个简单的温度显示界面:
// Model —— 温度数据类
class TemperatureModel {
public:
int32_t getCurrentTemp() { return currentTemp; }
void updateTemp(int32_t newTemp) { currentTemp = newTemp; }
private:
int32_t currentTemp = 25;
};
// Presenter —— 中介逻辑
void ScreenPresenter::updateTemperature() {
int32_t temp = model->getCurrentTemp();
view->showTemperature(temp);
}
// View —— 界面显示
void ScreenView::showTemperature(int32_t temp) {
Unicode::snprintf(textAreaBuffer, 10, "%d°C", temp);
textArea.invalidate();
}
看到没?View里没有 model->getCurrentTemp() 的调用,它只负责把数字显示出来。Presenter拿到数据后,调用View的方法去刷新界面。
4.3 数据流:从用户点击到界面刷新
我们走一遍完整的数据流,你就明白解耦是怎么做到的了。
- 用户点击按钮 —— View收到点击事件
- View通知Presenter —— 调用
presenter->onButtonClicked() - Presenter处理逻辑 —— 可能读写Model、做计算、调硬件
- Presenter更新View —— 调用
view->showResult(...) - View刷新界面 —— 更新控件、调用
invalidate()
这个流程是单向的,不会出现View直接改Model的情况。我曾经在一个项目中看到有人直接在View里调Model的setter,结果后来要加数据校验,到处找调用点,改得头皮发麻。
我的习惯:所有从View到Model的数据修改,都必须经过Presenter。哪怕只是改一个标志位,也要在Presenter里加一个接口。这样以后加日志、加校验、加权限,都只需要改Presenter一个地方。
4.4 界面更新的解耦机制
TouchGFX的界面刷新靠 invalidate() 机制。当数据变化需要更新界面时,Presenter调用View的方法,View内部再调控件的 invalidate() 触发重绘。
这里有个关键点:Presenter不能直接持有控件的指针。它只能通过View暴露的接口来操作界面。比如:
// 错误做法 —— Presenter直接操作控件
void ScreenPresenter::updateTemp() {
view->textArea.setValue(temp); // 不推荐
}
// 正确做法 —— Presenter调用View的接口
void ScreenPresenter::updateTemp() {
view->showTemperature(temp);
}
// View内部实现
void ScreenView::showTemperature(int32_t temp) {
textArea.setValue(temp);
textArea.invalidate();
}
为什么非要绕一圈?因为如果你让Presenter直接操作控件,哪天你要把 textArea 换成 textAreaWithBorder,所有调了 view->textArea 的地方都得改。而通过接口,你只需要改View内部实现,Presenter完全不用动。
避坑指南:我曾经在一个项目中,为了省事,让Presenter直接访问View的public成员变量。结果UI设计师换了三版界面,我改了三次Presenter。后来老老实实加接口,反而一劳永逸。
4.5 MVP架构图:数据流一目了然
下面这张图展示了MVP三个角色之间的调用关系和数据流向。我建议你把它打印出来贴在工位上。
4.6 实战中的MVP注意事项
讲几个我在项目中实际遇到的问题,你遇到了可以少走弯路。
- Presenter的生命周期:TouchGFX的Presenter和View同生共死。Screen切换时,旧的Presenter会被销毁。如果你在Presenter里开了定时器或注册了回调,记得在
deactivate()里清理掉,否则会野指针崩溃。 - Model的共享问题:多个Screen共享同一个Model时,我建议用单例或者依赖注入。不要在每个Presenter里各自new一个Model,数据不同步你会疯掉。
- 不要过度设计:如果只是一个简单的LED开关,View直接调Model也不是不行。MVP是为了应对复杂业务,别为了模式而模式。
我的经验:判断要不要用MVP,就看这个界面有没有「跨界面共享的数据」或者「可能变化的业务逻辑」。有,就用MVP;没有,简单点也行。我一般从第二个版本开始重构,第一个版本先跑通功能,第二个版本再抽离MVP结构。
4.7 小结
MVP的核心就一句话:View只管显示,Model只管数据,Presenter管所有决策。TouchGFX已经帮你搭好了View和Presenter的架子,你只需要把Model填进去,然后遵守单向数据流的原则。