最近改開發的時候,碰到一段很難懂的 Code,讓自己看了非常久,明明程式邏輯沒多複雜,但是整體讓我覺得很不舒服,總覺得有太多東西混雜在裡面。
這段程式主要的功能是做電話號碼 OTP 驗證,功能沒有太複雜,也就是拿著使用者輸入的 OTP 驗證碼去打一隻 API 做驗證。
這段 Code 來回看了幾天,請 AI 重構了無數回,也 Rollback 無數次。
最後終於想通了到底問題在哪邊,到底是什麼讓整段程式難以理解。
舉個例子
先來看看下面這段簡化過的程式碼例子:
class OtpPage extends StatefulWidget {
@override
_OtpPageState createState() => _OtpPageState();
}
class _OtpPageState extends State<OtpPage> {
String? errorMessage;
String otpCode = "";
Future<void> _verifyOtp(String otp) async {
try {
await otpService.verify(otp);
} catch (e) {
setState(() {
if (e.code == 'INVALID_OTP') {
errorMessage = "OTP 驗證碼錯誤,請重新輸入";
} else if (e.code == 'EXPIRED_OTP') {
errorMessage = "OTP 驗證碼已過期";
}
});
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => otpCode = value,
decoration: InputDecoration(
hintText: "請輸入 OTP 驗證碼",
),
),
ElevatedButton(
onPressed: () => _verifyOtp(otpCode),
child: Text("驗證"),
),
if (errorMessage != null)
Text(
errorMessage!,
style: TextStyle(color: Colors.red),
),
],
);
}
} 這段程式挺單純的,使用者輸入 OTP 驗證碼,按下驗證按鈕後就打 API 去驗證。
在驗證的時候,程式會用 try/catch 來攔截 API 回的 400/500 錯誤,並且將指定錯誤訊息存到狀態中,以供畫面顯示。
這功能做起來也沒什麼問題,也挺好理解的,錯誤訊息就是要顯示給使用者看的,放在 _OtpPageState 中也是合情合理。
但是如果故事只到此處,那也沒什麼好說的,難就難在需求總是會改,我們總是要回來重新閱讀這段程式碼。
需求異動
有天客人抱怨錯誤訊息不夠清楚,害他沒看到驗證碼錯誤的訊息,覺得怎麼系統一直沒反應,然後就很不高興的刪除 App 了。
既然客戶抱怨了,那我們只好想想辦法來解決客戶的問題。
經過 PO 與設計師討論,最終決定強化錯誤提示,讓客人可以更清楚了解現在是發什麼狀況。
這邊有個小細節,就是只有在 OTP 驗證碼錯誤的情況下才需要顯示紅框,其餘像是過期就不顯示紅框。
當開發人員看到設計之後,肯定覺得這題我會,不過就是一個 if 能解決的事,IDE 一開,手指一動,測試一跑,輕輕鬆鬆就完成了。
class _OtpPageState extends State<OtpPage> {
String? errorMessage;
String otpCode = "";
String phoneNumber = "0912345678";
Future<void> _verifyOtp(String otp) async {
try {
await otpService.verify(otp);
} catch (e) {
setState(() {
if (e.code == 'INVALID_OTP') {
errorMessage = "OTP 驗證碼錯誤,請重新輸入";
} else if (e.code == 'EXPIRED_OTP') {
errorMessage = "OTP 驗證碼已過期";
}
});
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => otpCode = value,
decoration: InputDecoration(
hintText: "請輸入 OTP 驗證碼",
),
border: OutlineInputBorder(
borderSide: BorderSide(。
color: errorMessage == "OTP 驗證碼錯誤,請重新輸入" ?
Colors.red :
Colors.black,
),
),
),
ElevatedButton(
onPressed: () => _verifyOtp(otpCode),
child: Text("驗證"),
),
if (errorMessage != null)
Text(
errorMessage!,
style: TextStyle(color: Colors.red),
),
if (errorMessage == "OTP 驗證碼錯誤,請重新輸入")
Row(
children: [
Icon(Icons.phone, color: Colors.orange),
Text("請檢查您的電話號碼:"),
Text(
phoneNumber,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
],
);
}
} 結果 Review 的時候一看,就出現了 errorMessage == “OTP 驗證碼錯誤,請重新輸入” 的 if 判斷。
看到這裡,你肯定會想,功能完成是完成了,但是總感覺哪裡怪怪的。
這裡就得回到我們的主題,把細節當狀態了。
由於前面錯誤訊息與錯誤狀態是一對一,所以把訊息當狀態好像也沒什麼毛病。
但當今天錯誤狀態與錯誤畫面細節不是一對一的時候,問題就出現了。
直接把畫面細節當狀態來存的時候,當未來需要根據狀態來判斷時,就會變得很麻煩。
尤其是當一個錯誤可能又會出現多個提示的時候,錯綜復雜的關係,讓閱讀的人根本不可能從當前的提示組合中理解當前是處於什麼錯誤狀態。
所以我們應該儲存的是錯誤狀態,而不是畫面細節。
更好的做法
了解問題之後,其實解法也就顯而易見,我們在 _OtpPageState 中應該存的是錯誤狀態,而不是錯誤訊息。
enum OtpError {
invalidOtp,
expiredOtp,
}
class _OtpPageState extends State<OtpPage> {
OtpError? otpError;
String otpCode = "";
String phoneNumber = "0912345678";
Future<void> _verifyOtp(String otp) async {
try {
await otpService.verify(otp);
} catch (e) {
setState(() {
if (e.code == 'INVALID_OTP') {
otpError = OtpError.invalidOtp;
} else if (e.code == 'EXPIRED_OTP') {
otpError = OtpError.expiredOtp;
}
});
}
}
String? get errorMessage {
switch (otpError) {
case OtpError.invalidOtp:
return "OTP 驗證碼錯誤,請重新輸入";
case OtpError.expiredOtp:
return "OTP 驗證碼已過期";
default:
return null;
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => otpCode = value,
decoration: InputDecoration(
hintText: "請輸入 OTP 驗證碼",
border: OutlineInputBorder(
borderSide: BorderSide(。
color: otpError == OtpError.invalidOtp ?
Colors.red :
Colors.black,
),
),
),
),
ElevatedButton(
onPressed: () => _verifyOtp(otpCode),
child: Text("驗證"),
),
if (errorMessage != null)
Text(
errorMessage!,
style: TextStyle(color: Colors.red),
),
],
);
}
} 使用 enum 來儲存錯誤狀態之後,我們要拿這個錯誤狀態來衍生更多不同的顯示,就更容易的。
當然錯誤不一定得用 enum 來表示,這部分就得視需求來決定。
但重點是儲存錯誤狀態,能讓看的人更容易理解程式碼的邏輯。
結語
避免把細節當狀態,是寫出易讀、易維護程式碼的重要概念。
當你發現自己在用字串比對、或是用畫面顯示的內容做邏輯判斷時,就要停下來想想:我儲存的是系統狀態,還是畫面細節?
存正確的狀態不只能讓讀的人更容易理解,也增加未來修改彈性。
記住:狀態管理的核心,是管理系統的真實狀況,而不是管理畫面的呈現細節。
在 Flutter 開發中,合理運用 enum、class 和狀態管理工具,可以讓你的程式碼更清晰、更容易測試和維護。