Wang Xuan(雅萱)
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 使用 flutter 建構 Expense Tracker app 原始碼:<https://github.com/WangShuan/flutter-03-expense_tracker> 該專案主要架構有一個 Appbar ,裏面顯示 app 標題以及 icon 按鈕,點擊 icon 按鈕後可開啟 modal 以新增消費紀錄; Appbar 下方則顯示圖表,分別列出每種類別的消費佔比;最後圖表下方則是列出所有消費紀錄的清單,其中有消費類別、標題、消費日期以及消費金額。 ## 定義 Expense 藍圖 `uuid` 套件可用來生成隨機的唯一 id,可通過指令 `flutter pub add uuid` 安裝套件,使用方式如下: ```dart // 引入套件 import 'package:uuid/uuid.dart'; class Expense { // 創建 Expense 藍圖,方便後續生成 Expense 列表 Expense({ required this.title, required this.price, required this.date, }) : id = Uuid().v4(); // 用 : id = xxx 設定初始值,並將初始值設為 Uuid().v4() final String id; final String title; final int price; final DateTime date; } ``` `enum` 用來定義一個類別以及該類別中可用的內容,舉例如下: ```dart // 建立一個類別 Category 並設置其可用的內容 enum Category { food, shopping, medical, learn, } // 指定 category 的類別為 Category // 所以 category 的值被限定為 food/shopping/medical/learn 四者其一 final Category category; ``` 定義與使用 category 的 icon : ```dart // 在 model/expense.dart 中定義 cateIcon 為各自類型所對應的 Icons const cateIcon = { Category.food: Icons.restaurant, Category.learn: Icons.menu_book, Category.medical: Icons.medical_services, Category.shopping: Icons.store }; // 使用 Icons CircleAvatar( child: Icon( cateIcon[expenseData.category], ), ) ``` 第三方套件 `intl` 用來格式化日期,可通過指令 `flutter pub add intl` 進行安裝,用法如下: ```dart // 通過 DateFormat() 設置想要顯示的格式,再用 format(date) 傳入 date 進行格式化 get formatterTime { return DateFormat('yyyy-MM-dd').format(date); } ``` ## 認識與使用新的小部件 不確定陣列長度時,可使用 `ListView.builder` 小部件,該小部件外層需具有寬高,如果外層為 `Row` 或 `Column` 時,需搭配 `Expanded` 或 `SizedBox` 小部件做使用: ```dart final List<Expense> expenseData; Expanded( child: ListView.builder( // 使用 ListView.builder 小部件 itemBuilder: (context, index) => Row( // 傳入 itemBuilder children: [ Text(expenseData[index].title), // 通過 index 獲取 expenseData 中的內容 ], ), itemCount: expenseData.length, // 設置 itemCount 為 expenseData 的長度 ), ) ``` 使用 `showModalBottomSheet` 方法從底部往上彈出 `modal` 以顯示表單新增消費紀錄: ```dart appBar: AppBar( // 在 Scaffold 小部件中設置 appbar title: const Text('Expenses Tracker'), // 設定 title actions: [ // 設定工具列的按鈕 IconButton( // 建立一個純 icon 的按鈕 onPressed: () { // 設定點擊後觸發的事件 showModalBottomSheet( // 這邊使用 showModalBottomSheet 開啟一個 modal context: context, // 傳入 context builder: (ctx) => const NewExpense(), // 設置 builder 回傳一個自定義的 NewExpense 表單小部件 ); }, icon: const Icon( // 設置 icon Icons.add, ), ), ], ), ``` 表單的輸入框要用 `TextField` 小部件,為了獲取輸入的內容,需設定小部件的 `controller` 屬性,並使用 `dispose()` 方法確保當關閉 `modal` 時同時銷毀控制器: ```dart final _titleController = TextEditingController(); // 建立輸入框控制器 @override void dispose() { // 使用 dispose() 方法 _titleController.dispose(); // 在不需要使用到 UI 時銷毀該控制器,以確保不會佔用到多餘的內存 super.dispose(); } // 綁定輸入框的控制器 TextField( controller: _titleController, // 綁定 controller decoration: const InputDecoration( // 設置樣式 label: Text('標題'), // 設置 label ), maxLength: 30, // 設置輸入內容的最大長度限制 ), // 獲取值 print(_titleController.text); ``` 另外在使用 `TextField` 時要注意,如果父部件為 `Row` 或 `Column`,需將 `TextField` 用 `Expanded` 小部件包住,否則會出現錯誤: ```dart Row( children: [ Expanded( child: TextField( controller: _priceController, decoration: const InputDecoration( labelText: '消費價格', prefixText: 'NT\$', ), maxLength: 6, keyboardType: const TextInputType.numberWithOptions(signed: true), // 設定鍵盤類型為數字+符號(因為純 TextInputType.number 鍵盤類型會沒送出按鈕) textInputAction: TextInputAction.done, // 設定點擊送出按鈕後執行的事件為完成,會關閉鍵盤並提交(如果是 TextInputAction.next 則會跳去下一個輸入框小部件並開啟其鍵盤類型) onSubmitted: (value) => _submitData(), // 設定送出時要執行的事件 ), ), const SizedBox( width: 30, ), // ... ], ), ``` 點擊按鈕關閉 `modal` 的方法: ```dart ElevatedButton( onPressed: () { Navigator.pop(context); // 設定這行用來關閉 modal }, child: const Text('CANCEL'), ) ``` 點擊按鈕開啟日期選擇器(使用 `showDatePicker` 方法)及顯示選中日期的做法: ```dart DateTime? _selectedDate_; // 聲明一個變量保存選中的日期,類型為 null 或 DateTime void _chooseDate() async { // 設定開啟日期選擇器的事件 final d = await showDatePicker( // 通過 await 方法獲取選中的日期,通過 showDatePicker 方法開啟日期選擇器 context: context, // 傳入 context initialDate: DateTime.now(), // 設置預設選中的日期 firstDate: DateTime(2000), // 設置最小日期 lastDate: DateTime.now(), // 設置最大日期 ); setState(() { // 通過 setState 更新日期並顯示在畫面上 _selectedDate_ = d; }); } // 使用 ElevatedButton.icon( // 建立一個帶 icon 的按鈕小部件 onPressed: _chooseDate, // 設置點擊後觸發的事件為 開啟日期選擇器的事件 label: _selectedDate_ != null // 判斷是否有選中日期,有就顯示選中的日期,沒有就顯示請選擇 ? Text(DateFormat('yyyy-MM-dd') // 用 intl 套件格式化日期 .format(_selectedDate_!) // 在可能為空的值後加上 ! 告訴 DART 這永遠不會是空值 .toString()) : const Text('請選擇日期'), icon: const Icon( // 設置 icon Icons.calendar_today, size: 15, ), ), ``` `DropdownButton` + `DropdownMenuItem` 小部件用來顯示類別的下拉選項: ```dart // 宣告變量 _selectCate 為第一個 Category Category? _selectCate = Category.values[0]; // 使用 Container( // 建立 Container 小部件當外框並設定樣式 padding: const EdgeInsets.symmetric( horizontal: 15, // 設定水平的 padding ), height: 40, // 設定高度 width: 120, // 設定寬度 decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), // 設定圓角弧度 color: const Color.fromARGB(255, 240, 230, 250), // 設定底色 ), child: DropdownButton<Category>( // 使用 DropdownButton 小部件生成下拉選單 underline: const SizedBox(), // 設置下拉選單的外觀樣式 isExpanded: true, // 設置擴展為 true(前提是父部件有設定寬度) items: Category.values // 設定下拉選項內容,這邊用 Category.values 遍歷 .map( (e) => DropdownMenuItem<Category>( // 回傳 DropdownMenuItem 小部件 value: e, // 設定選項的 value child: Row( // 設定 DropdownMenuItem 小部件的內容 children: [ Icon( cateIcon[e], size: 18, color: Theme.of(context).primaryColor, ), const SizedBox( width: 8, ), Text( cateZhName[e].toString(), style: textStyle, ), ], ), ), ) .toList(), // 最後用 toList() 轉為 [ DropdownMenuItem(), DropdownMenuItem()... ] value: _selectCate, // 設定選中的選項 icon: const Icon( Icons.arrow_drop_down_rounded, size: 15, ), onChanged: (value) { // 每次選中項目時觸發 onChanged,並會傳入 DropdownMenuItem 的 value setState(() { // 將選中的狀態設為 value _selectCate = value; }); }, ), ) ``` 撰寫提交表單的事件,如驗證無誤則添加消費紀錄、如驗證有誤則通過 `showDialog` 方法顯示 `AlertDialog` 小部件: ```dart void _submitData() { // 建立一個提交表單用的事件 if (_titleController.text.trim().isEmpty || // 判斷去除多餘空白後,是否有輸入標題 _priceController.text.trim().isEmpty || // 判斷去除多餘空白後,是否有輸入價格 int.parse(_priceController.text) <= 0 || // 判斷將文字轉為數字後是否有內容且小於等於零 _selectedDate == null) { // 判斷是否有選擇日期 showDialog( // 顯示對話框 context: context, builder: (context) => AlertDialog( // 顯示警告對話框小部件 title: const Text('格式錯誤'), // 設定標題 content: const Text('請確保您輸入了有效的標題、消費價格、消費日期與消費類別。'), // 設置警告內容 actions: [ TextButton( // 設置功能按鈕 onPressed: () { Navigator.pop(context); // 點擊後關閉對話框 }, child: const Text('確定'), ) ], ), ); return; // 返回,不執行下方程式碼 } // 如果上方驗證沒問題則觸發以下程式碼 widget.addData(Expense( // 這邊的 addData 是從 expenses.dart 傳入的,用來新增消費紀錄資料 title: _titleController.text.trim(), price: int.parse(_priceController.text), date: _selectedDate!, category: _selectCate!, )); Navigator.pop(context); // 新增後關閉 modal } ``` 優化 `modal` 讓鍵盤不會擋到內容的兩種方法: 第一種,讓 `modal` 佔據完整可用高度,程式碼如下: ```dart showModalBottomSheet( context: context, builder: (ctx) => NewExpense(_addData), isScrollControlled: true, // 新增這個設定,讓 modal 佔據完整的可用高度 ); ``` > 當 `modal` 佔據完整可用高度後,會發現部分內容將被手機本身的手機本身的功能遮蓋,比如相機鏡頭等等,所以需要讓 `modal` 的 `padding-top` 預留多一些空間,以確保能完整瀏覽 `modal` 的內容。 第二種,通過 `MediaQuery.of(context).viewInsets.bottom` 獲取鍵盤高度,並利用 `padding-bottom` 讓 `modal` 的內容往上推,程式碼如下: ```dart showModalBottomSheet( context: context, builder: (ctx) => Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, // 設置 paddint-bottom 為鍵盤高度 ), child: NewExpense(_addData), ), isScrollControlled: true, ); ``` 左/右滑動觸發事件可使用 `Dismissible` 小部件來實現,以本例來說,可通過滑掉單筆消費紀錄將該筆紀錄刪除: ```dart void removeExpenses(Expense expense) { setState(() { expensesData.remove(expense); }); } Dismissible( key: ValueKey(expenseData[i].id), // 設置唯一 key child: ExpenseItem(expenseData[i]), // 設定滑動的小部件,這邊是單筆消費紀錄 direction: DismissDirection.startToEnd, // 設定觸發事件的滑動方向,其他滑動方向則不會觸發任何事 onDismissed: (direction) => removeExpenses(expenseData[i]), // 設置滑動後要執行的事件 ) ``` 在滑動觸發刪除消費紀錄事件後,產生已刪除的提示訊息,可用 `SnackBar` 小部件: ```dart ScaffoldMessenger.of(context).clearSnackBars(); // 再產生新的 SnackBar 之前先清除舊的 ScaffoldMessenger.of(context).showSnackBar( // 顯示一個 SnackBar SnackBar( // 傳入 SnackBar 小部件 content: Text('已刪除 ${expense.title} 消費。'), // 設置訊息內容 duration: const Duration(seconds: 3), // 設置顯示時長,三秒後會自動消失 action: SnackBarAction( // 設置按鈕 label: '復原', // 按鈕名稱 onPressed: () { // 點擊後執行的事件 setState(() { expensesData.add(expense); }); }), ), ); ``` ## 自定義主題樣式 可設定使用 Material V3 版本的主題(默認是 V2 版本的主題,即主色為藍色的那種) ```dart MaterialApp( theme: ThemeData(useMaterial3: true), // 在 MaterialApp 裏面添加這行設定 home: const Expenses(), ), ``` 可設置種子顏色去生成一組相關聯的顏色,以便用於應用程式的主題設置: ```dart // 通過 ColorScheme.fromSeed 方法傳入 seedColor 為種子顏色進行生成 var kColorS = ColorScheme.fromSeed( seedColor: Colors.lightGreenAccent.shade100, ); ``` 可通過在 `MaterialApp` 小部件中設定 `theme` 來客製化主題樣式: ```dart MaterialApp( theme: ThemeData().copyWith( // 使用 ThemeData().copyWith 以保留預設的 Material 主題並加以改寫 useMaterial3: true, colorScheme: kColorS, // 指定應用程式主題中的各種顏色為我們用種子顏色生成的顏色 primaryColor: kColorS.primary, // 設置主題的主要顏色為種子顏色中的主要顏色 appBarTheme: const AppBarTheme().copyWith( // 設置 appBar 小部件的主題(通過 .copyWith 保留預設的樣式) foregroundColor: kColorS.onPrimary, // 更改前景色 backgroundColor: kColorS.primary, // 更改背景色 ), cardTheme: const CardTheme().copyWith( // 設置 Card 小部件的主題(通過 .copyWith 保留預設的樣式) elevation: 0.5, // 更改陰影程度 color: kColorS.secondaryContainer, // 設置背景色 ), filledButtonTheme: FilledButtonThemeData( // 設置 FilledButton 小部件的主題 style: FilledButton.styleFrom( // 通過 FilledButton.styleFrom 保留預設的樣式 elevation: 1, // 更改陰影程度 shadowColor: kColorS.background, // 更改陰影顏色 backgroundColor: kColorS.primary, // 更改 fill 的顏色 ), ), textTheme: ThemeData().textTheme.copyWith( // 設置所有文字相關小部件的主題(通過 .copyWith 保留預設的樣式) titleLarge: TextStyle( fontSize: 20, color: kColorS.primary, fontWeight: FontWeight.bold, height: 1, ), bodyLarge: TextStyle( fontSize: 16, color: kColorS.primary, height: 1, ), bodyMedium: TextStyle( color: kColorS.primary, height: 1, ), bodySmall: TextStyle( height: 1, color: kColorS.secondary, ), ), ), home: const Expenses(), ); ``` 如果需要特別針對某個文字小部件選用文字主題可以這樣做: ```dart Text( 'Records', style: Theme.of(context).textTheme.titleLarge, ), ``` 設置黑暗模式: ```dart var kDarkColorS = ColorScheme.fromSeed( // 新增一個給黑暗模式用的種子顏色 seedColor: const Color.fromARGB(255, 42, 96, 8), brightness: Brightness.dark, // 設定要生成深色主題用的顏色方案(默認是 Brightness.light) ); MaterialApp( themeMode: ThemeMode.system, // 設置主題的模式為手機系統的模式(以哀鳳來說可在手機的設定中點擊開發者切換深色外觀) darkTheme: ThemeData.dark().copyWith( // 使用 ThemeData().dark().copyWith 以保留預設的 Material 的深色主題並加以改寫 useMaterial3: true, colorScheme: kDarkColorS, // 指定應用程式主題中的各種顏色為我們用種子顏色生成的顏色 primaryColor: kDarkColorS.primary, // 設置主題的主要顏色為種子顏色中的主要顏色 scaffoldBackgroundColor: kDarkColorS.background, // 設置 Scaffold 小部件的背景色 appBarTheme: const AppBarTheme().copyWith( // 設置 appBar 小部件的主題(通過 .copyWith 保留預設的樣式) foregroundColor: kDarkColorS.primary, // 更改前景色 backgroundColor: kDarkColorS.onPrimary, // 更改背景色 ), // ...後略 ), home: const Expenses(), ) ``` 在小部件中判斷當前的主題模式以顯示不同的顏色: ```dart TextStyle( fontSize: 14, color: Theme.of(context).brightness == Brightness.dark ? kDarkColorS.onPrimaryContainer : const Color.fromARGB(255, 55, 55, 55), ); ``` ## 顯示統計圖表的部分 在 `models/expense.dart` 檔案中添加設定 ExpenseBucket 的藍圖,用來獲取每個類別的數據: ```dart class ExpenseBucket { ExpenseBucket(this.category, this.expenses); final Category category; // 傳入類別 final List<Expense> expenses; // 傳入 Expense 的列表 // 添加方法用來針對單個類別獲取數據 ExpenseBucket.fromCategory(List<Expense> allExpense, this.category) : expenses = allExpense .where((element) => element.category == category) .toList(); // 添加獲取總金額的方法 int get totalExpenses { int sum = 0; for (final e in expenses) { sum += e.price; } return sum; } } ``` ### 水平圖表作法 建立 `chart.dart` 檔案用來顯示統計圖表: ```dart final List<Expense> expenses; // 傳入所有的 Expense 數據 List<ExpenseBucket> get allCategory { // 通過 get 方法獲取各個類別的 ExpenseBucket 數據並整合為 List return [ ExpenseBucket.fromCategory(expenses, Category.food), ExpenseBucket.fromCategory(expenses, Category.learn), ExpenseBucket.fromCategory(expenses, Category.medical), ExpenseBucket.fromCategory(expenses, Category.shopping), ]; } int total = 0; // 宣告一個變量用來存放總金額 for (var item in allCategory) { // 使用 for in 方法計算總金額 total += item.totalExpenses; // 使用 ExpenseBucket 中的 totalExpenses 方法得到每個類別的總金額並進行加總 } // 用 `Container` 小部件設置外觀 Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Theme.of(context).brightness == Brightness.dark ? kDarkColorS.onPrimary : kColorS.secondaryContainer, border: Border.all( width: 1, color: Theme.of(context).brightness == Brightness.dark ? kDarkColorS.primary : kColorS.primary, ), ), padding: const EdgeInsets.symmetric( vertical: 8, ), child: Column( // 用 Column 小部件生成由上到下的排列 children: [ ...allCategory.map((e) => ChartBar(e, total, allCategory)), // 通過 map 方法產生各個分類的圖表小部件 ], ), ); ``` 新增自訂義的圖表小部件 `chart_bat.dart` : ```dart final ExpenseBucket item; // 各個分類的數據 final int total; // 總金額 final List<ExpenseBucket> allCategory; // 所有分類的 ExpenseBucket 列表 Row( children: [ Column( // 用 Column 小部件顯示分類的 icon 與標題 children: [ Icon( cateIcon[item.category], size: 20, color: Theme.of(context).brightness == Brightness.dark ? kDarkColorS.primary : kColorS.onPrimaryContainer, ), const SizedBox( height: 4, ), Text( cateZhName[item.category]!, style: TextStyle( color: Theme.of(context).brightness == Brightness.dark ? kDarkColorS.primary : kColorS.onPrimaryContainer, fontSize: 12, ), ) ], ), const SizedBox( width: 8, ), Expanded( // 用 Expanded 小部件確保圖表佔據最大可用寬度 child: ClipRRect( // 用 ClipRRect 小部件設置圓角外觀 borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( // 用 LinearProgressIndicator 小部件生成百分比水平條狀圖 value: (item.totalExpenses.toDouble() / total), // 設置填色區塊為 類別總金額➗總金額 minHeight: 16, // 設置水平條狀圖的高度 color: Theme.of(context).brightness == Brightness.dark // 設置填色區塊的顏色 ? kDarkColorS.primary : kColorS.primary, backgroundColor: // 設置為填色區塊的背景色 Theme.of(context).brightness == Brightness.dark ? kDarkColorS.primaryContainer : kColorS.primary.withOpacity(0.3), ), ), ), ], ); ``` 最後可用 `Column` 小部件將整個 `Row` 小部件包起來,並在每個類別的 `Row` 下方新增分隔線小部件 `Divider`: ```dart Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, ), child: Row( children: [ Column( // 顯示類別的 icon 與名稱 ), const SizedBox( width: 8, ), Expanded( // 顯示水平條狀圖 ), ], ), ), if (allCategory.last.category != item.category) // 判斷是否為最後一個分類,不是才顯示分隔線 const Divider(), // 分隔線小部件 ], ); ``` ## 垂直圖表做法 將 `Chart.dart` 的 `Column` 改為 `Row`: ```dart Row( // 用 Row 小部件生成由左到右的排列 crossAxisAlignment: CrossAxisAlignment.end, // 設置內容對齊方式為垂直貼底 children: [ ...allCategory.map( (e) => Expanded( // 將 ChartBar 小部件用 Expanded 包住,以佔據所有空間 child: ChartBar(e, total, maxTotal, allCategory), ), ), ], ), ``` 將 `chart_bat.dart` 最外層的 `Row` 改為 `Column` ,讓類別名稱與填色區塊垂直排列,接著將原本用來設置 `LinearProgressIndicator` 小部件外觀圓角的 `ClipRRect` 小部件刪除,並將 `LinearProgressIndicator` 改為 `FractionallySizedBox` 小部件,整體內容如下: ```dart final ExpenseBucket item; // 各個分類的數據 final int total; // 總金額 final int maxTotal; // 分類當中最高的金額 Column( mainAxisAlignment: MainAxisAlignment.end, // 設置讓條狀圖統一靠下 children: [ Column( // 用來在填色條狀上方顯示類別名稱/圖標/所佔據百分比 children: [ Icon( // 顯示類別 icon cateIcon[item.category], size: 20, color: isDarkMode ? kDarkColorS.primary : kColorS.onPrimaryContainer, ), const SizedBox( height: 4, ), Text( // 顯示類別名稱 item.category.name.toUpperCase(), style: TextStyle( color: isDarkMode ? kDarkColorS.primary : kColorS.onPrimaryContainer, fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox( height: 4, ), Text( // 用總金額下去計算,顯示類別佔據的百分比 '${(item.totalExpenses.toDouble() / total * 100).floor()}%', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(fontWeight: FontWeight.bold), ) ], ), const SizedBox( height: 4, ), Flexible( // 使用 Flexible 將 FractionallySizedBox 小部件包住 child: FractionallySizedBox( // 使用 FractionallySizedBox 小部件顯示填色條狀圖 heightFactor: item.totalExpenses.toDouble() / maxTotal, // 要填色的高度 child: Container( // 用 Container 小部件進行填色 width: 24, margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( borderRadius: const BorderRadius.vertical( top: Radius.circular(4), bottom: Radius.circular(0), ), color: isDarkMode ? kDarkColorS.onPrimaryContainer : kColorS.primary, ), ), ), ), ], ); ``` ## 響應式設計 獲取設備寬高的方式如下: ```dart // 比如可判斷 width > 400 時顯示 Row 小部件、 width < 400 時顯示 Column 小部件 final width = MediaQuery.of(context).size.width; // 比如可判斷 height > 400 時顯示 Column 小部件、 height < 400 時顯示 Row 小部件 final height = MediaQuery.of(context).size.height; ``` 獲取小部件寬高的方式: ```dart LayoutBuilder( // 使用 LayoutBuilder 小部件包住欲計算的小部件 builder: (BuildContext context, BoxConstraints constraints) { return Padding( padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 8, ), child: SizedBox( width: constraints.maxWidth * 0.55, // 通過 constraints.maxWidth 獲取寬度 // constraints 共有 maxWidth/minWidth/maxHeight/minHeight 四種屬性值 child: Column( children: [ Text( 'Records', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox( height: 8, ), mainContent, ], ), ), ), }, ); ``` 判斷設備當前方向為直立或橫向: ```dart final orientation = MediaQuery.of(context).orientation; if (orientation == Orientation.portrait) // 直立 Row( children: [ Expanded(child: priceWidget), // 把消費價格的輸入框小部件設為 local variable 方便復用 const SizedBox(width: 16), const Spacer() ], ) else // 橫向 Row( children: [ Expanded(child: titleWidget), // 把標題的輸入框小部件設為 local variable 方便復用 const SizedBox(width: 16), SizedBox(width: 200, child: priceWidget), // 把消費價格的輸入框小部件設為 local variable 方便復用 ], ), ``` ## 自適應 安卓設備使用 `Material` 主題,蘋果設備使用 `Cupertino` 主題,比如 `showDialog` vs `showCupertinoDialog` ,可通過以下方式判斷設備: ```dart if (Platform.isIOS) { // 判斷是否為 IOS 系統 showCupertinoDialog( context: context, builder: (context) => CupertinoAlertDialog( title: const Text('格式錯誤'), content: const Text('請確保您輸入了有效的標題、消費價格、消費日期與消費類別。'), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('確定'), ) ], ), ); } else { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('格式錯誤'), content: const Text('請確保您輸入了有效的標題、消費價格、消費日期與消費類別。'), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('確定'), ) ], ), ); } ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully