使用 TDD + Clean Architecture 開發 Flutter 專案課程筆記 13 ~ 14

前言

邏輯、流程的部分都已經處理掉了,接下來應該是把畫面跟定義好的程式串起來就會動了;課程也即將進入尾聲。

筆記

Flutter TDD Clean Architecture Course [13] – Dependency Injection

這集主要是要教學 dependency injection,在 Android 專案我習慣用 Koin 來處理這個需求,因為建構子實在需要太多東西了,假設 10 個地方需要 NumberTriviaBloc,那我就必需手動準備好 30 個參數。有了 DI 可以加速我們開發,只要宣告一次 NumberTriviaBloc,就可以到處使用。

影片開頭有個註解上色的 plugin,JetBrain 版本也有,但格式不太一樣:

https://plugins.jetbrains.com/plugin/index?xmlId=org.igu.plugins.bettercomments

作者在註冊 DI 時有提到:因為 Bloc 有 Stream 的關係,不應該使用 Singleton,避免停在 Stream 的中間,導致狀態錯誤。至於其他的都用 LazySingleton,好處是用到時才會初始化 instance。

這集要注意的地方應該就是:SharedPreferences 的 return type 是 Future,所以整個 DI 都要為它修改成 async / await 的形式。

Flutter TDD Clean Architecture Course [14] – User Interface

最後一集,也就是刻畫面了,原以為是單純的一集,但我太天真:作者影片開始沒多久就開了模擬器,展示程式碼修改後的畫面;當我要把 App 灌進手機,卻顯示了下列錯誤:

E/flutter (24786): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Null check operator used on a null value
E/flutter (24786): #0      MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:142:86)
E/flutter (24786): #1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:148:36)
E/flutter (24786): #2      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:331:12)
E/flutter (24786): #3      MethodChannel.invokeMapMethod (package:flutter/src/services/platform_channel.dart:358:49)
E/flutter (24786): #4      MethodChannelSharedPreferencesStore.getAll (package:shared_preferences_platform_interface/method_channel_shared_preferences.dart:54:22)
E/flutter (24786): #5      SharedPreferences._getSharedPreferencesMap (package:shared_preferences/shared_preferences.dart:191:57)

第一個直覺:我的 Dart 版本大於支援 null-safety 的 2.12,會不會是要升級 package 到支援 null-safety 的版本才可以正常運作?所以就拉了一條 branch 去處理升級後的各種 migration。但很不幸地,修到最後會出現 7 個單元測試的 error,而且是某個必定有值的地方變成 null,完全無法理解,後來就放棄了這個修改。

接著朝第一行的錯誤去找,剛好有一篇也有人是炸在 SharedPreferences,就看到:如果要在 main() 處理 await SharedPreferences.getInstance(),必須要先初始化WidgetFlutterBinding 。

https://stackoverflow.com/a/68178721

後來又查到一篇在講解 WidgetFlutterBinding,篇幅較長,我自己的理解是:今天要初始化 SharedPreferences 必須讓它接到雙平台的 native code,而 WidgetFlutterBinding 提供了與雙平台互動的通道,因此會有順序之分,先有通道才能接觸到 native code。

https://stackoverflow.com/a/63873689

這個問題處理掉後,理所當然地繼續刻畫面,但使用到 bloc 時,出現了下述錯誤:

../../Documents/flutter/.pub-cache/hosted/pub.dartlang.org/provider-3.2.0/lib/src/delegate_widget.dart:194:18: Error: Superclass has no method named 'inheritFromElement'.
    return super.inheritFromElement(ancestor, aspect: aspect);

查了一下,發現是因為我的 Flutter 版本 >= 2.0.0,有個叫做 provider 的最低版本至少要 5.0.0,而此時我的 bloc 依賴的 provider 版本是 3.0.0,只好心一橫,將 bloc 升到最新。接下來就是無止盡地各種 migrate 跟測試 dependencies 版本的相容,最後還是無法修好壞掉的 bloc 單元測試(又回到一開始第一個直覺的輪迴),為了繼續上課,就直接抄別人的 clone 來修改了:

https://github.com/svarunid/flutter-tdd-clean-architecture-course

因為上述問題我卡了近兩天,之後正式開發一定先把所有 dependencies 升好升滿(雖然根據經驗升到最新也不是一件好事),否則為了版本問題去修一堆東西真的太血尿了。

回到正題,這集大多時間都在刻畫面,其中比較重要的是:利用 BlocProvider 來送出 event 以及用 BlocBuilder 來給予 state,只要這兩個串起來就可以兜起所有東西。作者使用的 RaisedButton 已經 deprecated,所以我改用 ElevatedButton 來取代:

            Expanded(
              // Search concrete button
              child: ElevatedButton(
                child: Text('Random'),
                style: ElevatedButton.styleFrom(
                  primary: Theme.of(context).accentColor,
                ),
                onPressed: dispatchRandom,
              ),
            ),

在完成這個課程後,我還另外補充了 Github Actions 來處理自動單元測試,也用 codecov 補上測試覆蓋率的 badge,但奇怪的是:本地跑測試是 100% 通過,但 CI 會錯兩個,讓人滿困擾的。

專案連結

想看我實作的完整專案程式內容請瀏覽我的 github:

Summary
Article Name
使用 TDD + Clean Architecture 開發 Flutter 專案課程筆記 13 ~ 14
Description
邏輯、流程的部分都已經處理掉了,接下來應該是把畫面跟定義好的程式串起來就會動了;課程也即將進入尾聲。
Author
Publisher Name
jarvislin.com

有什麼想法嗎?快來跟大家分享你的看法。