簡介

網頁連結: 機殼篩選

本專案是我使用Flutter開發的第一個正式專案,功能是讓使用者可以篩選出符合自己需求的機殼,可以使用的功能包括了搜索、排序以及篩選,希望對正在挑選機殼的人們有幫助,如果在使用過程中有遇到問題或有其他建議都歡迎向我回報。

機殼的資料來自 原價屋CASE機殼(+電源)總覽 網站,此專案所使用到的技術以及環境如下:

  • Flutter 3.19.0 (channel stable)
  • Dart 3.3.0
  • Python 3.12.2
  • Firebase Realtime Database
  • Github Actions
  • Github Pages

開發過程

蒐集機殼資料

在原價屋網站上的機殼資料主要包含以下項目:

  • 機殼
  • 品牌
  • 容量
  • 顯卡長
  • CPU高
  • 主板
  • 電供
  • 內附風扇數
  • 風扇 (前、後、上、下、側)
  • 水冷 (120、140、240、280、360、420)
  • I/O (U3、U2、TYPE-C、HDMI、SD讀卡機)
  • 硬碟 (2.5、3.5)
  • 光碟機
  • 側板類型
  • 控制器&集線器
  • 顯卡垂直安裝
  • 圖片

而我所做的就是利用 Python 的 BeautifulSoup 函示庫,蒐集網頁上的資料並且整理成一個一個陣列,之後上傳到建立好的 Firebase 資料庫中,讓 Flutter 建立的網頁可以讀取我們整理好的資料,同時為了方便管理,我也讓這個 Python 檔案可以產生 log 以及儲存機殼資料的 csv 檔。

部分程式內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#控制器&集線器
fanHub = []
for case in allCase:
if (len(case.find_all(string=re.compile("控制器|集線器")))!=0):
fanHub.append(1)
else:
fanHub.append(0)

#顯卡垂直安裝
verticalGPU = []
for case in allCase:
if (len(case.find_all(string=re.compile("顯卡垂直安裝")))!=0):
verticalGPU.append(1)
else:
verticalGPU.append(0)

Flutter 建立網頁

Fltter 的部分首先先從 Firebase 資料庫儲存每個機殼的資料到 class 中,根據使用者設定的條件進行排序、篩選後呈現到網頁上,每個機殼的資料是放在 Card 類型的 widget 中,而布局方式不是使用內建的 GridView ,而是使用 responsive_grid ,因為每個機殼的敘述文字長度可能不同,使用此布局方式能使每個 Card 的高度自適應內容,不用像 GridView 一樣統一高度,不過也是因為使用了此方法,導致網頁在滾動時的效能會降低。

部分程式內容:

1
2
3
4
5
6
7
8
9
10
11
12
body: Align(
child: Container(
decoration: BoxDecoration(
color: primaryColor,
),
child: ResponsiveGridList(//布局
controller: controller,
desiredItemWidth: 500,
children: initCaseCard(cardColor, textColor, buttonColor, filterCase),//Card 物件
),
),
),

開發心得

在開發的過程中我也遇到過不少問題,因此在這邊把一些開發時可能遇到的困難、解決方法以及一些實用的小技巧記錄下來。

Quicktype

網頁連結: quicktype

Quicktype是一個可以將 JSON 轉為各種語言的 Class 或 Struct 的網站,可以減少自己轉換程式碼的時間。進入網站之後,點擊 OPEN QUICKTYPE 就會進入轉換網頁,只要在左側輸入 JSON 源碼,右側選擇想要的程式語言,就會在中間自動生成程式碼,以下是使用範例:

Gitgub 專案 Secrets

如果要把專案上傳到 Gitgub 的公開專案上,並使用 Github Actions 執行的話,為了避免 Passowrd, API Key 等等重要資料外洩,就必須要把這些資料寫到該專案的 Secrets 之中,而我在建立 Python 和 Flutter 的專案時都有使用到 Secrets 功能,底下將簡介兩種程式語言引入 Secrets 的方法。

Python

  1. 打開專案的 Settings > Secrets and variables > Actions ,新建一個 Repository secret ,將資料輸入,範例名稱為{SECRET_LINK}。
  1. 在 workflows 中的 yml 檔案中要加入以下代碼:
1
2
3
env:
SECRET_LINK: ${{secrets.SECRET_LINK}}
#{SECRET_LINK} 可以改為剛剛設定的變數名稱
  1. 在 Python 引入 os 函式庫,就可以使用環境中的 secrets
1
2
import os
url = os.environ['SECRET_LINK'] #{SECRET_LINK} 可以改為剛剛設定的變數名稱

Flutter

這一個方法的原理是將想要的檔案轉為 base64 編碼,並設定為 secrets ,執行 Actions 時將 secrets 轉換回 dart 檔案讓其他程式碼可以使用。

  1. 建立一個 secrets.dart 檔案,將資料放入其中(記得將檔案上傳時不要連 secrets.dart 一起上傳了),secrets.dart 範例:
1
2
3
4
5
6
7
8
9
10
11
12
import 'package:firebase_core/firebase_core.dart';

var myFirebaseOptions = const FirebaseOptions(
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "...");

  1. 在 secrets.dart 檔案所在文件夾打開終端機,輸入以下指令,產生 base64 編碼文件 secrets。
1
certutil -encode secrets.dart secrets
  1. 用記事本打開 secrets 就會看到 secrets.dart 檔案轉換成的 base64 編碼,在專案中新建一個 Repository secret ,將剛剛的編碼輸入(不包含上下分隔線),範例名稱為{SECRETS_FILE_CONTENT}。

  2. 在 workflows 中的 yml 檔案中要加入以下代碼:

1
2
3
4
5
name: Decode base64 secrets #將 base64 編碼還原
run: echo $SECRETS_FILE_CONTENT | base64 -di > lib/secrets.dart #lib 路徑
env:
SECRETS_FILE_CONTENT: ${{ secrets.SECRETS_FILE_CONTENT }}
#{SECRETS_FILE_CONTENT} 可以改為剛剛設定的變數名稱
  1. 在 Flutter 引入 secrets.dart ,就可以使用裡面的內容
1
2
3
4
5
6
7
8
9
import 'package:pc_case_filter/secrets.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: myFirebaseOptions /* secrets.dart 內的變數 */
);
runApp(const MyApp());
}

白天 & 黑夜模式切換

本專案中可以透過按鍵切換樣式,有三種模式,分別為自動、黑夜、白天,其中的自動模式是根據裝置的樣式自動切換,為了達成這個功能,我建立了兩個變數:

  • isDarkMode 判斷當時該顯示什麼樣式 (0:白天、1:黑夜)
  • colorMode 當時的色彩模式 (0:自動、1:黑夜、2:白天)

配合 MediaQuery class 取得裝置的色彩模式:

1
2
3
brightness = MediaQuery.of(context).platformBrightness; /* 裝置模式 */
isDarkMode = (brightness == Brightness.dark && colorMode == 0) || (colorMode == 1);
/* 裝置為黑夜模式且網頁為自動模式 or 網頁為黑夜模式 */

為了在其他檔案中也能調整 colorMode ,建立 setColorMode 函數,並將其作為參數傳遞給其他檔案,這樣就能在按下按鍵時調整色彩模式。

1
2
3
4
5
void setColorMode(){
setState(() {
colorMode = (colorMode + 1) % 3;
});
}

按鍵調整模式:

1
2
3
4
5
6
7
8
9
10
11
12
Builder(
builder: (context) => IconButton(
/* 根據模式調整 icon */
icon: Icon((colorMode == 0? Icons.auto_mode:(colorMode == 1? Icons.dark_mode:Icons.light_mode)), color: textColor),
onPressed: (){
setState(() {
widget.setColorMode();
});
},
tooltip: "顏色模式",
),
)

參考資料