4、MVP架构入门:Model-View-Presenter模式在TouchGFX中的实现
说实话,刚接触TouchGFX那会儿,我也有点懵。代码自动生成了那么多文件,到底哪个该写逻辑,哪个只管显示?后来我才明白,这一切都围绕着一种叫MVP的架构模式在转。
MVP,全称Model-View-Presenter。说白了,就是把界面、数据和业务逻辑拆成三块,各干各的活。这样做的好处,我在项目里体会太深了——改个UI不用动底层逻辑,换数据源也不用重画界面。
4.1 MVP三兄弟:各司其职
咱们先看看这三个角色分别负责什么:
| 角色 | 职责 | 在TouchGFX中的对应 |
|---|---|---|
| Model | 数据层,负责业务逻辑和数据存取 | Model类、数据源、硬件抽象层 |
| View | 视图层,只负责界面显示和用户交互 | ScreenView类、控件、画布 |
| Presenter | 协调层,连接Model和View,处理事件 | ScreenPresenter类 |
嗯,这里要注意:View不直接访问Model。所有数据请求都通过Presenter中转。这样做的好处是,哪天你想把LCD屏换成OLED,或者把数据从本地文件改成云端接口,只需要改对应的那一层就行。
核心原则:View只负责"画什么",Model只负责"存什么",Presenter负责"怎么处理"。三者之间通过接口通信,不直接依赖具体实现。
4.2 TouchGFX中的MVP骨架
当你用TouchGFX Designer新建一个Screen时,它会自动生成三个文件:
ScreenView.hpp / ScreenView.cpp—— View层ScreenPresenter.hpp / ScreenPresenter.cpp—— Presenter层Screen.hpp—— 接口定义
Model层呢?通常需要你自己建。我习惯在项目根目录下建一个model文件夹,放所有数据相关的类。
举个例子,假设我们要做一个温湿度监控界面:
// Model层:SensorDataModel.hpp
class SensorDataModel {
public:
float getTemperature();
float getHumidity();
void updateSensorData();
private:
float temperature;
float humidity;
};
// View层:TempHumidView.hpp
class TempHumidView : public TempHumidViewBase {
public:
virtual void setupScreen();
virtual void tearDownScreen();
void updateTemperature(float temp);
void updateHumidity(float hum);
};
// Presenter层:TempHumidPresenter.hpp
class TempHumidPresenter : public TempHumidPresenterBase {
public:
TempHumidPresenter(TempHumidView& v);
virtual void activate();
virtual void deactivate();
void onButtonRefreshClicked();
private:
SensorDataModel model;
};
我的习惯:Presenter里不写具体的业务算法,只做调度。比如"用户点了刷新按钮 → Presenter通知Model更新数据 → Model返回新数据 → Presenter把数据传给View显示"。这样每个类的职责都很纯粹,调试起来特别爽。
4.3 数据流:从按钮到屏幕的完整旅程
用户按下一个刷新按钮,数据是怎么从传感器一路跑到屏幕上的?我画了张图,你看完就明白了:
你看,整个流程是单向的,不会出现View直接调用Model的情况。这样做的好处是:
- 可测试性:你可以单独测试Presenter的逻辑,不用依赖真实的UI和硬件
- 可替换性:想换UI库?只要View的接口不变,其他代码不用动
- 可维护性:每个类的代码量都不大,出了问题很快能定位
4.4 事件传递机制:回调与监听
TouchGFX里的事件传递,主要靠两种方式:
- 回调函数:按钮点击、滑动等用户交互事件
- 监听器模式:Model数据变化后通知Presenter
先看回调。在TouchGFX Designer里给按钮添加点击事件时,它会自动生成类似这样的代码:
// View层
void TempHumidView::setupScreen() {
buttonRefresh.setAction(buttonRefreshCallback);
}
void TempHumidView::buttonRefreshCallback(const AbstractButton& source) {
// 通知Presenter
presenter->onButtonRefreshClicked();
}
再看监听器。当Model的数据发生变化时,怎么通知Presenter?我常用的做法是定义一个接口:
// ModelListener接口
class ModelListener {
public:
virtual ~ModelListener() {}
virtual void onSensorDataUpdated(float temp, float hum) = 0;
};
// Presenter实现这个接口
class TempHumidPresenter : public TempHumidPresenterBase, public ModelListener {
public:
virtual void onSensorDataUpdated(float temp, float hum) override {
view.updateTemperature(temp);
view.updateHumidity(hum);
}
};
我曾经踩过的坑:在Presenter的构造函数里直接调用Model的初始化方法,结果View还没准备好,导致空指针崩溃。正确的做法是在activate()方法里做初始化,这时候View已经创建完毕了。
4.5 实战中的MVP最佳实践
做了几个项目后,我总结了几条经验:
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 界面切换 | 在Presenter的deactivate()中释放资源 |
在View的析构函数中处理业务逻辑 |
| 数据更新 | Model通过回调通知Presenter | Presenter轮询Model状态 |
| UI动画 | 在View层处理,Presenter只触发开始 | 在Presenter里控制动画帧 |
| 硬件访问 | 封装在Model层,通过接口暴露 | 在View或Presenter里直接操作寄存器 |
嗯,还有一点我想强调:不要为了MVP而MVP。如果你的界面只有一两个控件,逻辑也很简单,硬拆成三层反而增加复杂度。我一般会在项目有3个以上Screen时才开始用MVP,这时候它的优势就体现出来了。
我的小技巧:在TouchGFX Designer里,给每个Screen的Presenter命名时加上"Presenter"后缀,比如MainPresenter、SettingsPresenter。这样在代码里一眼就能看出哪个是Presenter,哪个是View,团队协作时特别有用。
MVP架构说白了就是"分而治之"的思想。把界面、数据和逻辑拆开,各管各的一亩三分地。刚开始可能会觉得多写了好多代码,但等你需要改需求、修bug的时候,就会感谢当初的自己了。