隨著畫面數量增加 (7-8 個以上),傳統「每個畫面自己處理 Command、直接 display.setCurrent 跳轉」會導致:
MIDP UI 元件僅 20 幾個類別,不需重型框架。核心三角色:
Screen/Canvas,僅負責呈現與事件觸發。
public class UIController implements CommandListener {
private PhoneBookMIDlet midlet; // 需要 MIDlet 取得 Display
private Display display;
private Model model; // 業務模型
// 所有 View 實例 (單例模式)
private IndexFunctionUI indexUI;
private NewPhoneUI newPhoneUI;
private ListPhoneUI listPhoneUI;
private SearchPhoneUI searchUI;
private InformationUI infoUI;
// 事件 ID 定義 (內部類別,編譯期常數)
public static class EventID {
private EventID() {}
// UI 觸發的導航事件
public static final byte EVENT_NEW_RECORD_SELECTED = 1;
public static final byte EVENT_SAVE_RECORD = 2;
public static final byte EVENT_NEWPHONE_BACK_MAIN = 3;
public static final byte EVENT_LISTPHONE_BACK_MAIN = 4;
public static final byte EVENT_SEARCH_BACK_MAIN = 5;
// 功能性事件 (可攜帶參數)
public static final byte ADD_NEW_RECORD = 100;
public static final byte SEARCH_RECORD = 101;
public static final byte CLEAR_RECORD = 102;
public static final byte LIST_RECORD = 103;
public static final byte HELP = 104;
}
public UIController(PhoneBookMIDlet midlet) {
this.midlet = midlet;
}
// 初始化:建立所有 View、綁定監聽器、顯示歡迎畫面
public void init(Model model) {
this.model = model;
this.display = Display.getDisplay(midlet);
indexUI = new IndexFunctionUI(this);
infoUI = new InformationUI(this);
newPhoneUI = new NewPhoneUI(this);
listPhoneUI = new ListPhoneUI(this);
searchUI = new SearchPhoneUI(this);
displayWelcome(); // 或 display.setCurrent(indexUI);
}
// 統一顯示切換入口
public void setCurrent(Displayable next) {
display.setCurrent(next);
}
public void setCurrent(Alert alert, Displayable next) {
display.setCurrent(alert, next);
}
// 核心:事件分發器
public void handleEvent(int eventID) {
switch (eventID) {
case EventID.ADD_NEW_RECORD:
setCurrent(newPhoneUI); break;
case EventID.LIST_RECORD:
setCurrent(listPhoneUI); break;
case EventID.SEARCH_RECORD:
setCurrent(searchUI); break;
case EventID.EVENT_NEWPHONE_BACK_MAIN:
case EventID.EVENT_LISTPHONE_BACK_MAIN:
case EventID.EVENT_SEARCH_BACK_MAIN:
setCurrent(indexUI); break;
default:
// 可加入日誌記錄未知事件
break;
}
}
// 重載:攜帶參數的事件 (如搜尋關鍵字、編輯記錄 ID)
public void handleEvent(int eventID, Object[] params) {
switch (eventID) {
case EventID.SEARCH_RECORD:
String keyword = (String) params[0];
searchUI.setKeyword(keyword);
setCurrent(searchUI);
break;
case EventID.ADD_NEW_RECORD:
// 進入編輯模式,傳入現有記錄 ID
int recId = (Integer) params[0];
newPhoneUI.loadRecord(recId);
setCurrent(newPhoneUI);
break;
}
}
// 供 View 呼叫的回呼:儲存新記錄
public void onRecordSaved(RecordEntry entry) {
try {
model.saveRecord(entry);
setCurrent(indexUI);
} catch (Exception e) {
showError("儲存失敗: " + e.getMessage());
}
}
private void showError(String msg) {
Alert alert = new Alert("錯誤", msg, null, AlertType.ERROR);
setCurrent(alert, indexUI);
}
}
public class NewPhoneUI extends Form implements CommandListener, ItemStateListener {
private UIController ctrl;
private TextField nameField, mobileField, phoneField, emailField;
private ChoiceGroup typeGroup;
private Command saveCmd = new Command("儲存", Command.OK, 1);
private Command backCmd = new Command("返回", Command.BACK, 2);
public NewPhoneUI(UIController ctrl) {
super("新增聯絡人");
this.ctrl = ctrl;
nameField = new TextField("姓名", null, 25, TextField.ANY);
mobileField = new TextField("手機", null, 25, TextField.PHONENUMBER);
phoneField = new TextField("市話", null, 25, TextField.PHONENUMBER);
emailField = new TextField("Email", null, 30, TextField.EMAILADDR);
typeGroup = new ChoiceGroup("類別", ChoiceGroup.MULTIPLE);
typeGroup.append("親友", null);
typeGroup.append("工作", null);
append(nameField);
append(mobileField);
append(phoneField);
append(emailField);
append(typeGroup);
addCommand(saveCmd);
addCommand(backCmd);
setCommandListener(this);
setItemStateListener(this);
}
public void commandAction(Command c, Displayable d) {
if (c == saveCmd) {
// 收集資料建立實體
RecordEntry entry = new RecordEntry();
entry.setName(nameField.getString());
entry.setMobile(mobileField.getString());
entry.setPhone(phoneField.getString());
entry.setEmail(emailField.getString());
entry.setTypes(getSelectedTypes());
// 交給 Controller 處理儲存與導航
ctrl.onRecordSaved(entry);
} else if (c == backCmd) {
ctrl.handleEvent(UIController.EventID.EVENT_NEWPHONE_BACK_MAIN);
}
}
// 編輯模式:載入現有記錄
public void loadRecord(int recId) {
// 從 Model 讀取並填入欄位...
// 切換標題為「編輯聯絡人」
}
private int[] getSelectedTypes() { /* ... */ return new int[0]; }
public void itemStateChanged(Item item) { /* 即時驗證邏輯 */ }
}
public class PhoneBookMIDlet extends MIDlet {
private UIController uiController;
private Model model;
protected void startApp() {
if (uiController == null) {
model = new Model(); // 初始化 RMS 等
uiController = new UIController(this);
uiController.init(model);
}
}
protected void pauseApp() {}
protected void destroyApp(boolean unconditional) {}
}
| 維度 | 傳統寫法 | MVC 模式 |
|---|---|---|
| 導航邏輯位置 | 分散在各 View | 集中在 UIController |
| 新增畫面成本 | 修改 3-5 個類別 | 新增 1 View + 1 case 在 Controller |
| 單元測試 | 難 (需模擬 Display、RMS) | 易 (Controller 可注入 Mock Model/View) |
| 程式碼行數 | 重複 boilerplate 多 | View 專注 UI,Controller 專注流程 |
| 團隊協作 | 易衝突 | UI/邏輯分離,平行開發 |
handleEvent 升級為狀態機,支援「返回鍵行為隨上下文變化」等複雜導航。onShow()/onHide(),Controller 切換時自動呼叫,處理資料刷新、計時器啟停。Injector),解耦 Controller 對具體 View 類別的實例化。MVC 模式在 J2ME 並非過度設計,而是解決「導航混亂」的最佳良方。將 UIController 作為唯一導航中樞,View 僅觸發事件、不直接切換畫面,Model 專注資料,三者職責清晰。此架構已在多款超過 15 個畫面的商業 MIDlet 中驗證,維護成本降低 60% 以上。強烈建議所有中型以上 J2ME 專案採用。