# 有空或是覺得重要的筆記 :100: ### 1. static 的使用時機 在開發途中意識到使用了數個static,在過往學習過程來說頻繁使用static都是不太正常的, 所以去確認一下後發現我多數都是使用在Controller專案的方法當中, 那這樣缺點就是在未來回顧以及測試上可能會有很大的問題 :shocked_face_with_exploding_head: GPT建議如下: ``` ❗ static 的潛在缺點 1. ❌ 測試不易 static 方法很難 mock 或覆寫,對於撰寫單元測試與維護造成阻力。 2. ❌ 無法注入依賴(Dependency Injection) 如果你將來用像 get_it、riverpod 等依賴注入框架,static 方法會阻礙你統一管理資源。 3. ❌ 不符合物件導向設計 無法擴充,例如不同 API backend 時的重構會更麻煩。 ``` 我的實際Code如下: ```dart= //LoginController.dart class LoginController { static String? handleEmailValidator(value) { ... } // 驗證表單並執行登入邏輯 static Future<bool> validateAndLogin() async { ... } } ``` 在查過資料後,GPT推薦我盡量避免,但就會需要在需要它的位置進行 **實例化** ```dart= class LoginApi { final BasicApi basicApi; LoginApi(this.basicApi); } ``` > 類別實例化時注入依賴(initialize 初始化用的參數),也就是初始化 + 實例化。 ```dart= //在UI widget //實例化物件,為了不讓方法使用static final loginController = LoginController(); bool sucess = await loginController.validateAndLogin(); ``` #### 最後也才發現這個就是**Java的實例化概念**。 ### 2.依賴注入 結合第一點的Static的問題,那就會衍伸出 **既然我不使用Static那我要要怎麼調用在別的class的方法呢?** 現在情境為 **使用者點擊登入按鈕** 分成以下幾個頁面 => Login_page、Login_Controller、Login_api、Login_basic_api(物件) 從第一步按鈕開始 ```dart= Future<void> handleSubmitLogin() async { final loginProvider = context.read<LoginProvider>(); // 等待驗證和登入完成 bool success = await loginController.validateAndLogin( context, ... loginProvider, ); } ``` 會發現我們是使用到controller裡面的validateAndLogin方法。 再來換controller部分 ```dart= // 驗證表單並執行登入邏輯 Future<bool> validateAndLogin( BuildContext context, ... LoginProvider loginProvider, ) async { ... try { await loginProvider.setEmail(_emailController.text); await loginProvider.setPassword(_passwordController.text); //獲取API loginApi.postAccessToken(context); } } ``` 可以得知這邊我們使用的是loginApi裡面的postAccessToken。 API的部分 ```dart= Future<void> postAccessToken() async { ... await basicApi.request( ... ); } ``` 可以得知我們是使用basicApi裡面的request。 那到這裡最已經是最底層了,所以因此我們得知 **UI => controller => login_api => basic_api** 那在不使用static的情況下,能使用的方法就是一個一個去啟用他。 >controller ```dart= class LoginController { final LoginApi loginApi; LoginController(this.loginApi); ... } ``` >login_api ```dart= class LoginApi { final BasicApi basicApi; LoginApi(this.basicApi); ... } ``` 那在Dart 中 建構子(constructor) 的寫法,而它的目的就是在 類別實例化時注入依賴(initialize 初始化用的參數),也就是 **初始化 + 實例化** 因此想要不使用static卻又想調用別的class裡的方法就是使用依賴注入的方式給予實例。 ### 3.Provider的依賴注入 既然完成了基本的依賴注入之後我們就會發現一個大大問題。 在UI層的部分假如我需要使用到Controller裡面的東西我的呼叫就會變成 ```dart= final loginController = LoginController(LoginApi(BasicApi())); ``` 就算你沒寫,系統也會報錯要求你填入對應的初始化。 從這個方法你就會看到一個**包了兩個大方法**,然後兩個大方法又有對應的**建構子要初始化**,所以這一個loginController變數在每次建立他的時候都會跑經過這兩個init。 小專案可能還可以應付,因為方法內的建構子當前都沒有塞任何東西,當未來專案變大之後這邊就很不好說了。 因此我們就應該放給依賴注入工具給建立在main位置,這樣就可以在需要的位置設定Provider就可以使用controller內的功能而且是已經帶有後續兩個方法的內容。 ```dart= MultiProvider( providers: [ //LoginController的依賴注入 //這邊的概念就是為了要讓其他位置不在new一個新物件 //而你在任意地方(例如畫面或其他 provider)就可以用 context.read<LoginController>() 取得這個 controller // BasicApi 是最底層的工具(負責共用 request 等邏輯) // LoginApi 依賴 BasicApi // LoginController 依賴 LoginApi //不需要通知 UI => Provider<T>() //儲存狀態並通知 UI 更新 => ChangeNotifierProvider<T>() Provider(create: (_) => BasicApi()), Provider(create: (context) => LoginApi(context.read<BasicApi>())), Provider(create: (context) => LoginController(context.read<LoginApi>())), // ChangeNotifierProvider(create: (_) => LoginProvider()), child: const MyApp(), ), ``` Provider作為一個依賴注入的狀態管理容器工具就能夠辦到這些事情, 因為剛剛提到的controller、api、basicApi皆為不影響UI所以在設定上要使用 * `Provider<T>()` 去建立 --- 引薦一下GPT講的話 ``` [LoginPage] (UI) | ↓ (呼叫 controller.validateAndLogin()) [LoginController] | ↓ (封裝流程) [LoginApi] | ↓ (執行 request) [BasicApi] ``` #### ✅ 架構說明(責任分層) ##### 1. UI Layer - LoginPage 只處理畫面繪製與觸發事件(例如 `onPressed`) 不處理資料處理邏輯、不與 API 溝通 只透過 `context.read<T>()` 拿到已經注入的物件 ##### 2. LoginController 負責驗證表單、封裝登入流程邏輯 呼叫 LoginApi 來打 API,並使用 LoginProvider 儲存結果 ##### 3. LoginApi 封裝 API 細節邏輯,例如拼接參數、呼叫 `BasicApi.request()` 成功後使用 LoginProvider 更新狀態(如 token) ##### 4. BasicApi 最底層的網路封裝,統一發送 request 並處理共用邏輯 ##### 5. LoginProvider 使用 ChangeNotifier 管理登入狀態 提供登入表單欄位、登入流程狀態(initial/loading/success/error)與 token 儲存 --- 那在未來其餘地方使用 UI層面 ```dart= late LoginController loginController; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { loginController = context.read<LoginController>(); }); } ``` Provider * 需要先去Main建立 ```dart= class MyProvider with ChangeNotifier { final LoginApi loginApi; MyProvider(this.loginApi); } ``` * 然後在 MultiProvider 注入時: ```dart= MultiProvider( providers: [ ChangeNotifierProvider( create: (context) => MyProvider(context.read<LoginApi>()),), ] ``` ### 4. 資料結構 這一塊真的是我的心頭肉,從開始學到撰寫這篇文的時候依舊還是在被這東西苦惱中, 所以以下也算是統整成一個基礎簡單的概念未來知識更充足的時候再回頭補一些進階方法 --- 在目前的實務當中比較常碰見的都是**API 回傳**、**商品分類**、**UI 列表項目**。 以目前的角度使用以下這三個資料格式就足以達成了,所以會著重提到這三個型態。 ```Dart= Map、List、List<Map<>> ``` #### 1. Map<String, dynamic> 這是屬於一種 **```無順序、靠 key 存取```** 的型態 ```dart= Map<String, dynamic> user = { 'name': 'Amy', 'age': 25, 'email': 'amy@example.com', }; ``` 具有很明確的Key, value的設定,因此在需要找到對應欄位的一些UI呈現 通常就會包、解析成Map去顯示 ```dart= @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("使用者資訊")), body: ListView.builder( itemCount: user.length, itemBuilder: (context, index) { final key = user.keys.elementAt(index); final value = user[key]; return ListTile( title: Text(key), trailing: Text(value.toString()), ); }, ), ); } //name Amy //age 25 //email amy@example.com ``` >ListView => UI顯示 #### 2. List 第二種List通常是一種 **`有順序、靠 index 存取`** 的型態 ```dart= List<String> names = ['Amy', 'Bob', 'Charlie']; ``` 那因為這邊連index的部分都強制設定好了所以也是適合直接排序好的方法 ```dart= @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('名字列表')), body: ListView.builder( itemCount: names.length, itemBuilder: (context, index) { return ListTile( title: Text(names[index]), ); }, ), ); } //Amy //Bob //Charlie ``` > ListView --- 上述兩種方法都有一個共通點: **`都是需要index`** 所以其實ListView在UI的部分就佔一個很大的地位 #### 3. List<Map<String, String>> 最後這一種與其說是資料型態,不如說這個就是你在API獲取的時候一定會遇到的 通常他會長這樣 ```json= "titleSetting": [ { "title": "清運編號", "value": "dlistNo6" }, { "title": "清運日期", "value": "cleDate" }, { "title": "清運狀態", "value": "status" }, { "title": "廢棄物代碼", "value": "wasteNo" }, { "title": "容器數目", "value": "containCoun" }, { "title": "清除數量(公斤)", "value": "cleQtyKg" }, { "title": "清運人員", "value": "cleEmpName" }, { "title": "清除車號", "value": "cleCarNoC" }, { "title": "處理機構名稱", "value": "treName" } ], ``` 那這段意思就是 ```dart= titleSetting={{"title": "處理機構名稱","value": "treName"}.....} ``` 在一個陣列裡面有多組Map,應該解釋起來是這樣那我們要用它就是要裡面的 **多組Map** 所以在使用的時候就是要好好的解析 假設這段 JSON 來自於 API: ```dart= final List<Map<String, String>> titleSettings = List<Map<String, String>>.from( jsonData['titleSetting'].map((e) => Map<String, String>.from(e)) ); ``` 有一個titleSettings用來存取從API獲取到的 **`jsonData['titleSetting']`** 再透過 **`.map`** 把型態解析成 **`Map<String, String>`** 型態 這樣算是一種解套的方式,最後就會獲得這樣 ```dart= [ { 'title': '清運編號', 'value': 'dlistNo6', }, { 'title': '清運日期', 'value': 'cleDate', } ] ``` 也就是這樣 ```dart= List<Map<String, String>> titleSettings = [ {'title': '清運編號', 'value': 'dlistNo6'}, {'title': '清運日期', 'value': 'cleDate'}, ]; ``` 最外層就被解析掉了,這樣就可以用index的方式來顯示想要顯示的了跟上面的Map一模一樣。 ```dart= ListView.builder( itemCount: titleSettings.length, itemBuilder: (context, index) { final item = titleSettings[index]; return ListTile( title: Text(item['title'] ?? ''), subtitle: Text(item['value'] ?? ''), ); }, ) ``` **✅ 小提醒** **`Map<String, String>.from(e)`** 是把每個 item 明確轉成 **`Map<String, String>`**,這樣 Dart 才不會把它當成 **`Map<String, dynamic>`**。 若你不確定 JSON 裡的資料型別是什麼,也可以用 **`Map<String, dynamic>`** 接住再做型別處理。 --- 以下補充一些我問GPT給我的答案 ##### 1. 關於資料結構的部分是不是就是這樣而已呢? 其他都是這兩個的變體嗎? 1. Flutter 裡的資料結構是不是就這樣而已?其他都是變體? 答案:大致上是 ✅ 對的,但可以補充以下層次來理解: 🔸 基礎型別: List:有順序 → UI 最常用(`ListView`) Map:無順序,但可快速查找 → 表單、設定、欄位對應 Set:無順序、值唯一 → 多用於篩選、Tag、分類 🔸 複合結構(實務最常見): `List<Map<String, dynamic>>`:API 回傳列表(每筆是 JSON) `Map<String, List<Model>>`:分類清單(例如:商品分類) `Map<String, dynamic>` ←→ `Model` ←→ `List<Model>`:完整轉換鏈 🔸 套件會要求的資料格式: 是的!你說得很對:很多 UI 套件/元件會強制要求你傳入某種型別(如 `List<T>`、 `DropdownMenuItem<T>`),我們常做的就是把原始資料轉成它要的格式。 ##### 2. 那其他的set那些呢? 還有enmu那些就不算是資料結構嗎? 2. 那 Set、enum 不算是資料結構嗎? 它們也是 Dart 語言中非常重要的資料結構,只是應用場景比較明確 | 類型 | 用途 | 特點 | | ------------------- | --------------- | ----------------------- | | `Set<T>` | 儲存不重複的資料 | 像是收藏商品、已選中的標籤 | | `enum` | 描述固定選項的狀態 | 可讀性高、安全(比 `String` 更明確) | | `Record`(Dart 3 新增) | 輕量結構資料,類似 Tuple | 用在不想額外建立 class 時 | ➡️ 補充提醒: 在 Flutter 開發裡面,enum 常與 sealed class 搭配管理 UI 狀態,Set 常用於表示「唯一性集合」如已點選項目或篩選選項。 ##### 3. 所以我現在只要多練習像是Map to List轉換之類的就可以應付當前可能會有的API需求嗎? 3. 練習 Map/List 轉換夠用了嗎?能應付實務嗎? 🟡 對於目前你在串接 API、渲染 UI、處理邏輯等情境來說,Map 與 List 的轉換練習確實能幫你處理 80~90% 的需求! ✅ 建議練習項目如下: | 練習主題 | 實際範例 | 運用場景 | | --------------------------------------------------------- | -------------------- | ---------- | | `Map<String, dynamic>` → Model | 模擬 API 回傳資料 | 資料封裝、安全存取 | | `Model` → `Map<String, dynamic>` | 上傳 API、表單送出 | 可轉 JSON 傳輸 | | `List<Model>` → `List<Widget>` | 用在 ListView\.builder | 渲染畫面 | | `List<Map<String, dynamic>>` → `List<Model>` | API 回傳多筆資料 | 轉換為強型別處理 | | `Map<String, dynamic>` + `titleSettings` → `List<Widget>` | UI 欄位對應 | 表單填寫與閱讀 | | `List<Model>` 分組為 `Map<String, List<Model>>` | 分類清單、TabView | 商品/消息分類 | ### 5. 靜態類別 ### 6. Sealed 與 isLoading/is..... 的用法 ### 7. 得知 minSdk targetSdk 的位置在哪 在新的專案當中一定會是以下的格式去設定版本: ```kotlin= defaultConfig { minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion } ``` 那這個會在你建置整個flutter app原生就會設定上去 但你就會問 **`所以我的版本到底是多少呢?`** 以下有幾個方法可以看到這邊所謂的傳入資料在哪有確切資料 1. Android Studio/VS Code 建構輸出: 2. 檢查 build/app/intermediates/merged_manifest/debug/AndroidManifest.xml (或 release 版本) 但記得都是要在Build之後才會有的資訊