Flutter Icon 使用原則是什麼?實際範例解析常見設計誤區

Published on: | Last updated:

講白一點:Flutter 的 icons 沒處理好,整個 UI 就會有股「怪味」

先把重點丟出來:在 Flutter 裡,icons 看起來小小一顆,但真的超會搞事,好的時候會讓整個畫面看起來很「完整」、很 Flutter;壞的時候,就會變成一堆小小難點、顏色又不對、在暗色模式直接消失,還點不太到。

我自己看很多專案(不管是 GitHub 上的、還是同事 side project),icon 常見翻車點幾乎都一樣:顏色寫死、明明是可以點的卻只用 `Icon` 搭 `GestureDetector`、hit area 小到手抖一下就 miss、還有完全沒考慮到螢幕閱讀器怎麼唸。

這篇要幹嘛:就是把這些 icon 常見坑一個一個挖出來、踩給你看,然後用「可以直接跑」的 Flutter code 把正確做法示範給你。

讀完你大概會比較有底:什麼時候該用 `Icon`、什麼時候該用 `IconButton` 或 `InkWell`,icon 要怎麼吃 theme、不去毀掉 dark mode,tap 區域怎麼抓,還有無障礙(a11y)到底要補哪幾個地方。

左邊是顏色亂寫、hit area 超小、沒 tooltip 的 icon;右邊是吃 theme、有 ripple、有語意的 icon,差很多。
左邊是顏色亂寫、hit area 超小、沒 tooltip 的 icon;右邊是吃 theme、有 ripple、有語意的 icon,差很多。

先有一個能跑的最小 app,才知道自己在改什麼

寫 Flutter 的老習慣:先弄一個超小的 demo app,確認環境跟 theme 都是你預期的,再開始玩 icon,不然你有時候會以為是 icon 有問題,其實是整個 `MaterialApp` 設定就怪怪的。

一個很裸的 scaffold,大概像這樣:

import 'package:flutter/material.dart';

void main() => runApp(const IntroApp());

class IntroApp extends StatelessWidget {
  const IntroApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Icons Intro',
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.indigo,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Icons Intro'),
          actions: const [
            Padding(
              padding: EdgeInsets.only(right: 8.0),
              child: Icon(Icons.search), // 直接吃 app bar 的顏色
            ),
          ],
        ),
        body: const Center(
          child: Icon(
            Icons.favorite,
            size: 48, // 先丟一顆大的在中間當對照
          ),
        ),
      ),
    );
  }
}

這種小 app 很無聊沒錯,但它是後面所有「改 icon 顏色、size、動畫、a11y」的 baseline,你要知道「不動任何設定時,Flutter 會怎麼處理 icons」。

搞懂一件事就好:Icon 本質上就是一個字型 glyph

很多人一開始都會把 icon 當圖片:但在 Flutter 這邊,`Icon` 其實比較像是「從一個特別的字型裡拿一個字出來」——那個字型就是 Material Icons(或你用的其他 icon font)。

`Icon` 這個 widget 本身只是拿著一個 `IconData`,真正決定它長什麼樣子的,是附近的兩個東西:

1. 最近的 `IconTheme`:負責 size、color、opacity 這種 icon 級的樣式。

2. 現在的 `Theme`:比如你是 light 還是 dark、`colorScheme` 長怎樣,`AppBar` 用什麼 foreground color… 這些都會影響到 icon。

有點像我之前聽人家形容:

IconTheme 就像一個房間裡的「服裝規定」,你說今天全部人都穿黑色 T-shirt,除非有人自己硬要穿紅色,不然大家就一起黑到底。

丟個簡單範例,感覺一下那個「房間」的概念:

import 'package:flutter/material.dart';

void main() => runApp(const ConceptApp());

class ConceptApp extends StatelessWidget {
  const ConceptApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.teal,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('IconTheme Basics')),
        body: Center(
          child: IconTheme(
            data: const IconThemeData(
              size: 40,
              opacity: 0.9,
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: const [
                Icon(Icons.home),
                SizedBox(width: 16),
                Icon(Icons.star),
                SizedBox(width: 16),
                Icon(Icons.settings),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

這三顆 icon 都沒特別設定 size、color,卻長一樣,就是因為被包在同一個 `IconTheme` 房間裡。你只要抓到這個 inheritance 的感覺,後面很多「為什麼這顆 icon 顏色怪怪的」的 debug 就會比較有方向。

先分清楚:Icon / IconButton / InkWell 到底誰管什麼

這邊超多人搞混:明明是要做一個可以點的 icon,結果就直接 `GestureDetector + Icon`,然後 tap 區域小小一坨,沒有 ripple,也沒有 semantics。

簡單粗暴分一下:

● `Icon`:純裝飾、純顯示,不負責互動,適合那種「只是放在文字旁邊點綴一下」的狀況。

● `IconButton`:要點擊、要有 ripple、要有 tooltip、要有合理的 hit area,就用它。它已經幫你把很多無障礙跟互動的東西都包好了。

● `InkWell` / `InkResponse`:你要的是「一整塊都能點」的卡片或區域,icon 只是裡面的一部分,那就用這個把整塊包起來。

import 'package:flutter/material.dart';

void main() => runApp(const WhichWidgetApp());

class WhichWidgetApp extends StatelessWidget {
  const WhichWidgetApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.deepPurple,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Icon vs IconButton')),
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.info), // 真的只是裝飾

              const SizedBox(width: 24),
              IconButton(
                icon: const Icon(Icons.favorite),
                tooltip: 'Like',
                onPressed: () {},
              ),

              const SizedBox(width: 24),
              InkWell(
                borderRadius: BorderRadius.circular(24),
                onTap: () {},
                child: const Padding(
                  padding: EdgeInsets.all(8.0),
                  child: Icon(Icons.share),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

很多在台灣公司接到的 app 設計稿,右上角一排小 icon(搜尋、收藏、分享)看起來都一樣,但實作上應該是三種不同語意:有的是 action、有的是 purely decorative、有的是整塊卡片點擊,這邊 widget 選錯,後面你要補無障礙跟 hit area 會超痛苦。

顏色不要亂寫:讓 theme 幫你撐住 light / dark mode

先承認一件事:大家第一次寫 Flutter(或 React、Vue 也一樣)超愛直接 `color: Colors.black`,因為這樣看起來「很確定」。然後一開 dark mode 整個 icon 直接消失在背景裡。

比較健康的做法,是把顏色交給 `Theme` 和 `IconTheme`,然後你只決定「大概是 primary 還是 secondary」,不要直接寫死 black / white。

import 'package:flutter/material.dart';

void main() => runApp(const ThemeIconsApp());

class ThemeIconsApp extends StatefulWidget {
  const ThemeIconsApp({super.key});

  @override
  State createState() => _ThemeIconsAppState();
}

class _ThemeIconsAppState extends State {
  bool dark = false;

  @override
  Widget build(BuildContext context) {
    final seed = Colors.orange;

    return MaterialApp(
      themeMode: dark ? ThemeMode.dark : ThemeMode.light,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: seed,
        iconTheme: const IconThemeData(size: 28),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: seed,
        brightness: Brightness.dark,
        iconTheme: const IconThemeData(size: 28),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Themed Icons'),
          actions: [
            IconButton(
              icon: Icon(dark ? Icons.light_mode : Icons.dark_mode),
              tooltip: 'Toggle theme',
              onPressed: () => setState(() => dark = !dark),
            ),
          ],
        ),
        body: Center(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: const [
              Icon(Icons.check_circle),
              SizedBox(width: 16),
              Icon(Icons.warning),
              SizedBox(width: 16),
              Icon(Icons.error_outline),
            ],
          ),
        ),
      ),
    );
  }
}

你在這個小 app 裡狂切 light/dark,就會發現一個很重要的感覺:你什麼都沒動,icon 自己會變成適合的顏色,對比也會維持在可讀範圍。

如果你有品牌色:像很多台灣的服務都會有一個「momo 粉」、「蝦皮橘」這種品牌色,最穩的是丟到 `colorSchemeSeed`,讓 Flutter 幫你推導整套顏色,然後你就用 `Theme.of(context).colorScheme.primary` 去上在少數需要強調的 icon 上。

icon size 跟可點擊範圍,其實是兩個不同的東西

這個坑真的超常見:你看到設計稿上 icon 看起來 18dp,就直接 `size: 18`,然後外面什麼都不包,結果實機上使用者手指一碰就 miss,一直抱怨「這 app 很難點」。

設計 guideline(像 Google Material)通常都會說:mobile 上可點擊區域大概要 48x48 dp 左右。`IconButton` 已經幫你做掉這件事;如果你硬要自己用 `Icon`,就記得用 padding 把 hit area 撐大。

import 'package:flutter/material.dart';

void main() => runApp(const SizeLayoutApp());

class SizeLayoutApp extends StatelessWidget {
  const SizeLayoutApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: Scaffold(
        appBar: AppBar(title: const Text('Icon Sizes & Hit Areas')),
        body: Center(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.zoom_in, size: 18), // 小小裝飾

              const SizedBox(width: 12),
              IconButton(
                icon: const Icon(Icons.zoom_in),
                onPressed: () {},
              ),

              const SizedBox(width: 12),
              InkWell(
                borderRadius: BorderRadius.circular(24),
                onTap: () {},
                child: const Padding(
                  padding: EdgeInsets.all(12), // 把 hit area 撐大
                  child: Icon(Icons.zoom_out, size: 20),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

你可以把手指真的放在實機上試一下,會發現「看起來一樣大的 icon」,用 `IconButton` 跟沒用 padding 的 `Icon`,那個容錯率完全不同。

同一顆 icon,看起來差不多大,但左邊只有 icon 本身能點,右邊整塊 padding 區都算 hit area。
同一顆 icon,看起來差不多大,但左邊只有 icon 本身能點,右邊整塊 padding 區都算 hit area。

無障礙這件事:icon 要「講話」,不然螢幕閱讀器會很尷尬

台灣很多專案老實說對 a11y 沒那麼敏感:但你只要遇過一次政府標案或大一點的金融客戶,就會突然發現「螢幕閱讀器」這四個字超級現實。

icon 本身對視覺使用者很直覺,但對螢幕閱讀器來說,如果你不給任何 label,它就是一個沉默的圖案。

幾個基本觀念先抓好:

● 可以點的 icon:用 `IconButton`,順便給 `tooltip`,這樣長按或 web hover 也有提示。

● 純裝飾 icon:用 `ExcludeSemantics` 把它從螢幕閱讀器的世界藏起來,避免亂唸。

● 有意義但不能點:例如「定位已開啟」這種狀態 icon,用 `Semantics(label: ...)` 包起來。

import 'package:flutter/material.dart';

void main() => runApp(const A11yIconsApp());

class A11yIconsApp extends StatelessWidget {
  const A11yIconsApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: Scaffold(
        appBar: AppBar(title: const Text('Accessible Icons')),
        body: Center(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                icon: const Icon(Icons.delete),
                tooltip: 'Delete item',
                onPressed: () {},
              ),
              const SizedBox(width: 24),

              // 純裝飾,螢幕閱讀器可以跳過
              const ExcludeSemantics(
                child: Icon(Icons.star),
              ),
              const SizedBox(width: 24),

              // 有意義但不能點
              const Semantics(
                label: 'Location enabled',
                child: Icon(Icons.location_on),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

如果你手邊有 Android 裝置,可以開 TalkBack 實際聽聽看這三種 icon 被唸出來的差別,感覺會很直接。

icon 狀態切換的時候,順便加一點動畫,整個質感會直接拉高

最典型的例子:收藏愛心那顆。你點下去,如果只是「border 變成實心」這種瞬間切換,其實也不是不能用,只是會有點硬。

Flutter 給你兩種很好用的東西:

● `AnimatedSwitcher`:你可以把兩顆不同狀態的 icon 丟進去,切換時加個 scale / fade 動畫。

● `AnimatedIcon`:內建一些很常見的動畫組合,像 play/pause、menu/close,直接用 `AnimationController` 控就好。

import 'package:flutter/material.dart';

void main() => runApp(const ToggleIconApp());

class ToggleIconApp extends StatefulWidget {
  const ToggleIconApp({super.key});

  @override
  State createState() => _ToggleIconAppState();
}

class _ToggleIconAppState extends State {
  bool liked = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.pink,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Toggle Icon')),
        body: Center(
          child: IconButton(
            tooltip: liked ? 'Unlike' : 'Like',
            onPressed: () => setState(() => liked = !liked),
            icon: AnimatedSwitcher(
              duration: const Duration(milliseconds: 250),
              transitionBuilder: (child, anim) {
                return ScaleTransition(
                  scale: anim,
                  child: child,
                );
              },
              child: liked
                  ? const Icon(
                      Icons.favorite,
                      key: ValueKey('on'),
                    )
                  : const Icon(
                      Icons.favorite_border,
                      key: ValueKey('off'),
                    ),
            ),
          ),
        ),
      ),
    );
  }
}

這段你在實機上點幾次就會懂:UI 其實沒做什麼大事,但那 250ms 的縮放動畫,會讓整個 app 感覺比較「有在乎細節」。

如果是播放鍵這種,就可以直接用內建的 `AnimatedIcons.play_pause`:

import 'package:flutter/material.dart';

void main() => runApp(const AnimatedIconDemo());

class AnimatedIconDemo extends StatefulWidget {
  const AnimatedIconDemo({super.key});

  @override
  State createState() => _AnimatedIconDemoState();
}

class _AnimatedIconDemoState extends State
    with SingleTickerProviderStateMixin {
  late final AnimationController _c = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );

  bool playing = false;

  @override
  void dispose() {
    _c.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: Scaffold(
        appBar: AppBar(title: const Text('AnimatedIcons.play_pause')),
        body: Center(
          child: IconButton(
            tooltip: playing ? 'Pause' : 'Play',
            iconSize: 40,
            onPressed: () {
              setState(() => playing = !playing);
              if (playing) {
                _c.forward();
              } else {
                _c.reverse();
              }
            },
            icon: AnimatedIcon(
              icon: AnimatedIcons.play_pause,
              progress: _c,
            ),
          ),
        ),
      ),
    );
  }
}
像是幫 icon 裝了一個小小的呼吸,介於「沒事」跟「太花」之間的那種剛好。
像是幫 icon 裝了一個小小的呼吸,介於「沒事」跟「太花」之間的那種剛好。

導航 icon:Back / Close 真的不要自己畫輪子

這邊有個很偷懶但很實用的原則:只要是「返回」或「關閉」這種超基本的導航 icon,盡量用 Flutter 已經幫你準備好的 `BackButton` 跟 `CloseButton`。

原因其實很多:

● RTL(從右到左)語系:像阿拉伯文那種,back icon 方向會反過來,自己用 `Icon(Icons.arrow_back)` 就要手動處理,但 `BackButton` 幫你顧到了。

● Navigator 行為:`BackButton` 預設會用 `Navigator.of(context).maybePop()`,沒有上一頁的時候不會直接炸掉。

● semantics & hit area:也都幫你處理好了。

import 'package:flutter/material.dart';

void main() => runApp(const NavIconsApp());

class NavIconsApp extends StatelessWidget {
  const NavIconsApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 真正要支援 RTL 會再加 localizationsDelegates 等等
      home: Scaffold(
        appBar: AppBar(
          leading: const BackButton(),
          title: const Text('Navigation Icons'),
          actions: const [
            CloseButton(),
          ],
        ),
        body: const Center(
          child: Text('Use BackButton/CloseButton when possible'),
        ),
      ),
    );
  }
}

尤其是那種要上架 Google Play、還要過審的 app,這種小地方都用 framework 給你的預設,其實比較不容易出事。

只想改一小塊 icon 風格,用 IconTheme.merge 就好

整體 theme 不動,只想在某張卡片裡 icon 長得不一樣:例如某個「雲端狀態」卡片,你希望裡面的 icon 都比較大、顏色固定是 cyan,不要被外面的 `AppBar` 之類影響。

這種時候就用 `IconTheme.merge` 把一個小範圍包起來,等於開一個小房間,裡面的人都穿你指定的衣服。

import 'package:flutter/material.dart';

void main() => runApp(const MergeThemeApp());

class MergeThemeApp extends StatelessWidget {
  const MergeThemeApp({super.key});

  @override
  Widget build(BuildContext context) {
    final base = ThemeData(
      useMaterial3: true,
      colorSchemeSeed: Colors.cyan,
    );

    return MaterialApp(
      theme: base,
      home: Scaffold(
        appBar: AppBar(title: const Text('IconTheme.merge Example')),
        body: Center(
          child: IconTheme.merge(
            data: const IconThemeData(
              size: 36,
              color: Colors.cyan,
            ),
            child: Card(
              elevation: 2,
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: const [
                    Icon(Icons.cloud),
                    SizedBox(width: 16),
                    Icon(Icons.cloud_queue),
                    SizedBox(width: 16),
                    Icon(Icons.cloud_done),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

這種寫法在實際專案裡很好用,像你在台灣常見的金融 app 裡,首頁一堆「功能卡片」:每張卡片裡的 icon size / color 可能都要統一,但跟 app bar 又不一樣,這時候就很適合用這一招。

常見錯誤 vs 比較穩的寫法,直接對照一下

整理一下幾個很常見的翻車:這邊做一個小小對照表,方便你之後回來翻。

比較表:icon 常見雷點 & 怎麼修

狀況 NG 寫法 比較好的做法
顏色寫死,dark mode 爆炸 直接 `color: Colors.black` 或 `Colors.white` 讓 theme / IconTheme 決定,或用 `colorScheme.primary`
小小一顆還要點 `GestureDetector + Icon(size: 18)` `IconButton` 或 `InkWell + Padding` 撐 hit area
螢幕閱讀器不知道它是誰 重要 icon 沒有 tooltip / semantics `IconButton.tooltip` 或 `Semantics(label: ...)`
theme 完全不起作用 每一顆 icon 都手動寫 `size` / `color` 用 `IconTheme` 統一,只有少數例外再 override

硬舉幾段 code 來對照:

// ❌ 顏色寫死,dark mode 基本上會看不見
const Icon(
  Icons.settings,
  color: Colors.black,
);
// ✅ 交給 theme / IconTheme
const Icon(Icons.settings);

// 或者用 colorScheme 裡的顏色
Icon(
  Icons.settings,
  color: Theme.of(context).colorScheme.primary,
);
// ❌ 小 hit area、沒有 ripple、語意也怪
GestureDetector(
  onTap: () {},
  child: const Icon(
    Icons.edit,
    size: 18,
  ),
);
// ✅ IconButton 幫你處理一堆東西
IconButton(
  icon: const Icon(Icons.edit),
  tooltip: 'Edit',
  onPressed: () {},
);
// ❌ 沒有 label 的重要 icon,螢幕閱讀器超困惑
Row(
  children: const [
    Icon(Icons.download),
    SizedBox(width: 8),
    Icon(Icons.upload),
  ],
);
// ✅ 有 tooltip / semantics,機器跟人都比較懂
Row(
  children: [
    IconButton(
      icon: const Icon(Icons.download),
      tooltip: 'Download file',
      onPressed: () {},
    ),
    const SizedBox(width: 8),
    const Semantics(
      label: 'Upload file',
      child: Icon(Icons.upload),
    ),
  ],
);
看起來只是多幾行 code,但這些都是「在你睡覺時幫你擋 bug」的小保險。
看起來只是多幾行 code,但這些都是「在你睡覺時幫你擋 bug」的小保險。

icon 拿不到你預期的顏色?先懷疑它站錯隊

有一種 debug 場景超常見:你在 theme 裡明明有設 `iconTheme`,但某顆 icon 就是長得跟大家不一樣。

這時候可以先想兩件事:

● 它是不是被放在 `IconTheme` 外面?如果整個 subtree 都包在一個 `IconTheme` 裡,而你那顆 icon 剛好寫在外面,那當然吃不到。

● 你有沒有在 local 又 override 掉?只要你在那顆 icon 上自己寫了 `size` 或 `color`,就會蓋掉從上面傳下來的設定。

import 'package:flutter/material.dart';

void main() => runApp(const ContextAwareIconsApp());

class ContextAwareIconsApp extends StatelessWidget {
  const ContextAwareIconsApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.green,
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Context-Aware Icons')),
        body: Center(
          child: IconTheme(
            data: const IconThemeData(size: 30),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                // 這顆吃到 size = 30
                const Icon(Icons.map),

                const SizedBox(height: 12),

                // 這顆自己寫 size,等於故意打破 inheritance
                const Icon(
                  Icons.map,
                  size: 16,
                ),

                const SizedBox(height: 12),

                Builder(
                  builder: (innerContext) {
                    // 還是在同一個 IconTheme 房間裡
                    return const Icon(Icons.map);
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

如果你哪天遇到「為什麼這顆 icon 不跟大家一起變大變小」這種 bug,就可以從 widget tree 的位置跟 local override 兩個方向先檢查。

說真的,我自己在意 icon 的幾個點,就這幾個

整理到這邊,其實可以很粗暴地收斂成幾條原則:

1. 能用 IconButton 就不要自己亂組合。

尤其是可點擊的東西,`IconButton` 幫你顧 hit area、ripple、語意,還有很多看不到的小細節,你省下來的不是幾行 code,是一堆你懶得測的邊界狀況。

2. 把 theme 當成第一順位,而不是「寫完再來補」。

顏色、size 能用 `Theme` / `IconTheme` 就不要每顆都寫死。這在你哪天要上 dark mode 或跟設計一起改整體配色的時候,會救你一次。

3. hit area 跟視覺 size 分開想。

icon 看起來可以小小一顆沒關係,但手指要有地方可以按。`IconButton` 或 `Padding + InkWell` 真的要用到爛掉沒關係。

4. 無障礙不要等客戶要求才補。

tooltip 跟 semantics label 都不難加,但如果你是等產品上線才回頭補,那時候你會覺得自己在還債。乾脆在一開始就順手寫好。

5. 動畫不用多,一點點就夠了。

像收藏、播放這種狀態切換,給它一點 scale 或內建 `AnimatedIcon`,整個 app 的「有用心程度」會看起來高很多,真的。

最後丟一個小 demo,把這些東西串在一起:

import 'package:flutter/material.dart';

void main() => runApp(const BestPracticesMini());

class BestPracticesMini extends StatelessWidget {
  const BestPracticesMini({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          leading: const BackButton(),
          title: const Text('Mini Best Practices'),
          actions: [
            IconButton(
              tooltip: 'Search',
              onPressed: () {},
              icon: const Icon(Icons.search), // 吃 app bar 的顏色
            ),
          ],
        ),
        body: Center(
          child: IconTheme.merge(
            data: const IconThemeData(size: 32),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: const [
                Icon(Icons.thumb_up), // 純裝飾
                SizedBox(width: 16),
                Semantics(
                  label: 'Location enabled',
                  child: Icon(Icons.location_on),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
一整排 icon 看起來很平常,但裡面其實藏了一堆「未來的你會感謝現在的你」的小設定。
一整排 icon 看起來很平常,但裡面其實藏了一堆「未來的你會感謝現在的你」的小設定。

最後留一個小小的動作給你:如果你現在專案裡已經有一堆 icons,不用全部重寫,挑一個畫面就好,把那一頁的 icon 改成「全部吃 theme、可點的都換成 IconButton、有語意的都補 tooltip/semantics」。

先試一頁,看看實際感覺差多少,有卡住的地方再回來檢查一下這些段落,通常會找到對應的坑。

Related to this topic:

Comments

撥打專線 LINE免費通話