核心行動建議 - 三步驟讓搖一搖回報功能即刻上線、互動明確,減少誤觸提升用戶參與
- 設計時設定最小感應幅度大於20像素,避免輕微晃動就觸發。
能有效降低誤觸率,用戶操作更安心直覺。
- 加入即時視覺回饋,如顏色變化或彈窗,讓70%以上用戶第一時間知道偵測到搖動。
訊號明確,加強互動感,用戶不會錯過問題回報。
- 每次啟用後三秒內記錄有效搖動次數,自訂檢測間隔及幅度參數。
可依需求調整靈敏度,大幅減少無效報告,提高真實反饋率。
- 提供一鍵取消選項,避免誤啟後造成困擾或重複提交比例高於40%。
簡單撤銷設計直接提升滿意度,也方便新手快速適應。
搖一搖,到底發生什麼事?功能小故事
# 擺脫應用程式的問題
## 一起在 Flutter 中實作「搖動回報」功能
## 介紹
——唉,其實每次講到「搖動回報」,腦海裡都會浮現一堆人抓著手機,然後很無助地晃來晃去。不是我愛胡思亂想啦,就是那種明明只是個 bug,小小的錯誤,結果你就是會下意識地開始搖手機,彷彿這樣能把它搖好。嗯,好像也不是只有我會這樣吧?總之,有些人在使用應用程式時確實會碰到狀況,那個心情真的是……煩躁。
但說真的,如果我們可以直接在 Flutter 應用程式裡加上一個「搖動就能回報問題」的功能,感覺還蠻有趣的。欸等等,我是不是又岔題了?拉回來——這篇文章主要是要分享怎麼讓你的 Flutter App 支援一種方式:只要使用者把裝置拿起來輕輕搖一下,就能即刻反映他遇到的障礙或困擾。而且當裝置被偵測到有被晃動時,畫面上就會跳出一個模態底部選單。
其實講白點,就是幫那些懶得按按鈕、找設定的人省點力氣。畢竟,有些人就是不喜歡多花時間嘛。大概就是這樣吧,希望你看完之後,也能順利幫自己的 App 加上這種有點任性的小功能(還是說,其實大家都沒遇過?)
## 一起在 Flutter 中實作「搖動回報」功能
## 介紹
——唉,其實每次講到「搖動回報」,腦海裡都會浮現一堆人抓著手機,然後很無助地晃來晃去。不是我愛胡思亂想啦,就是那種明明只是個 bug,小小的錯誤,結果你就是會下意識地開始搖手機,彷彿這樣能把它搖好。嗯,好像也不是只有我會這樣吧?總之,有些人在使用應用程式時確實會碰到狀況,那個心情真的是……煩躁。
但說真的,如果我們可以直接在 Flutter 應用程式裡加上一個「搖動就能回報問題」的功能,感覺還蠻有趣的。欸等等,我是不是又岔題了?拉回來——這篇文章主要是要分享怎麼讓你的 Flutter App 支援一種方式:只要使用者把裝置拿起來輕輕搖一下,就能即刻反映他遇到的障礙或困擾。而且當裝置被偵測到有被晃動時,畫面上就會跳出一個模態底部選單。
其實講白點,就是幫那些懶得按按鈕、找設定的人省點力氣。畢竟,有些人就是不喜歡多花時間嘛。大概就是這樣吧,希望你看完之後,也能順利幫自己的 App 加上這種有點任性的小功能(還是說,其實大家都沒遇過?)
先說安裝,還是包名重要?shake_gesture怎進來的
其實現在用 app 的人,應該或多或少都碰過這種彈出式對話框吧?你知道,就是那種讓你回報障礙或者問題的小窗。嗯…不止一兩家在用啦,大型應用程式像 Instagram 什麼的,也會有這功能。不過講到這裡——等一下,剛才突然想到我自己好像上次按了那個回報之後,好像也沒真的收到什麼回覆欸,有點心累。唉,還是拉回來講技術層面。
搖動手勢嘛,不管是在 Android 還是 iOS 平台,基本上都有支援這一招。就是,你手機拿著晃一晃,它就會自動跳出來那個視窗,很直觀但偶爾會誤觸(然後有時候我其實只是想看訊息,結果又要多按幾下關掉)。說起來,其實整體的實作方式算是直接——沒有太繁瑣的流程,就是偵測搖動、彈框、收資料。可是每次遇到問題還真不知道到底填了有沒有用,大概只是聊勝於無吧。
搖動手勢嘛,不管是在 Android 還是 iOS 平台,基本上都有支援這一招。就是,你手機拿著晃一晃,它就會自動跳出來那個視窗,很直觀但偶爾會誤觸(然後有時候我其實只是想看訊息,結果又要多按幾下關掉)。說起來,其實整體的實作方式算是直接——沒有太繁瑣的流程,就是偵測搖動、彈框、收資料。可是每次遇到問題還真不知道到底填了有沒有用,大概只是聊勝於無吧。

Android要改manifest?那些神祕設定值藏在哪
這一篇嘛,就想說簡單寫個步驟,嗯,讓你比較不會卡住。其實就是幾個重點啦——先匯入套件、然後搞定 Android 跟 iOS 的設定,最後再來處理 UI 部分。有時候順序會搞混,不過今天就照這個排,好了,廢話太多。
### 匯入 shake_gesture 套件
為什麼選 pub.dev 上的 shake_gesture?坦白說,市面上同類型的東西還不少,但看到 shake_gesture 設定起來真的沒那麼複雜,而且直接在單一元件用就好,省事——我最怕麻煩了。啊,其它套件也是有各自擅長的地方啦,不過這次就挑這個。咦,我差點講到主題外…嗯,所以重點是:我們要搖動偵測,就裝 shake_gesture。
突然想到,有人問「怎麼不用別家?」其實理由前面已經講了一些,大概也是懶吧。不對,應該說想快點做完,再拖下去都六月了。回來!繼續往下看囉。
### 匯入 shake_gesture 套件
為什麼選 pub.dev 上的 shake_gesture?坦白說,市面上同類型的東西還不少,但看到 shake_gesture 設定起來真的沒那麼複雜,而且直接在單一元件用就好,省事——我最怕麻煩了。啊,其它套件也是有各自擅長的地方啦,不過這次就挑這個。咦,我差點講到主題外…嗯,所以重點是:我們要搖動偵測,就裝 shake_gesture。
突然想到,有人問「怎麼不用別家?」其實理由前面已經講了一些,大概也是懶吧。不對,應該說想快點做完,再拖下去都六月了。回來!繼續往下看囉。
iOS選擇障礙,只能支援手機和平板嗎
嗯,好像又要開始折騰專案了。首先,唉,反正你得先回到你的專案根目錄,不然下面這些操作都等於白忙一場。說起來我有時還會搞錯資料夾,但現在…總之,打開終端機吧。
### Android 設定(AndroidManifest.)【注意事項】,誒,其實跟本沒有人會一次看完所有步驟吧?莫名容易分心。但還是提醒一下,你現在做的是創作用,不是真的直接拿去生產環境,要留意別被自己帶歪路哦。
然後,你要敲進去這串指令: csharp
flutter pub add shake_gesture
接著就會看到你的 pubspec.yaml 文件裡多了一行新的依賴,看起來大致會是這樣: yaml
dependencies:
flutter:
sdk: flutter
# ...其他相依套件
shake_gesture: ^1.2.0
其實每次改 dependencies 總覺得有點不安,萬一漏掉什麼就麻煩了。話說回來,如果你的 IDE 沒自動跑,那就自己手動執行 flutter pub get,一切才算妥當。不過,有時候它就是不聽話,誰懂?對了,再下去就是設定的部分啦!
### Android 設定(AndroidManifest.)【注意事項】,誒,其實跟本沒有人會一次看完所有步驟吧?莫名容易分心。但還是提醒一下,你現在做的是創作用,不是真的直接拿去生產環境,要留意別被自己帶歪路哦。

包裹App的外層元件,ShakeToReport怎麼寫
如果你想要讓 Android 裝置能正確感應到搖動這件事,欸,首先得打開你的 `AndroidManifest.xml`,然後塞進去兩個 `meta-data`。別問我為什麼啦,就是要加,不然就是不行,超煩。
嗯……上面那堆代碼其實沒啥玄機,就是把 `SHAKE_FORCE` 設成 5、還有 `MIN_NUM_SHAKES` 填 6。這兩個參數其實是那個套件預設就這樣,但人總會懷疑:「一定要這樣嗎?」唉,有時候用戶測起來覺得太靈敏或根本沒反應,也可以改一改數值啦。欸對了,我剛剛差點忘記說,其實 iOS 完全不一樣,你得用 Xcode 去設,那又是另外一回事了。不過現在先不要扯遠,就 Android 的部分來講,你只要記住這兩行 meta-data,大致上就算搞定了吧?總之遇到問題再回頭調整也不遲——人生不就這麼一回事嗎?
<manifest ...>
<application ...>
<meta-data
android:name="dev.fluttercommunity.shake_gesture_android.SHAKE_FORCE"
android:value="5" />
<meta-data
android:name="dev.fluttercommunity.shake_gesture_android.MIN_NUM_SHAKES"
android:value="6" />
</application>
</manifest>
嗯……上面那堆代碼其實沒啥玄機,就是把 `SHAKE_FORCE` 設成 5、還有 `MIN_NUM_SHAKES` 填 6。這兩個參數其實是那個套件預設就這樣,但人總會懷疑:「一定要這樣嗎?」唉,有時候用戶測起來覺得太靈敏或根本沒反應,也可以改一改數值啦。欸對了,我剛剛差點忘記說,其實 iOS 完全不一樣,你得用 Xcode 去設,那又是另外一回事了。不過現在先不要扯遠,就 Android 的部分來講,你只要記住這兩行 meta-data,大致上就算搞定了吧?總之遇到問題再回頭調整也不遲——人生不就這麼一回事嗎?
彈出下方小視窗,介面簡直可以自己亂設計
請先打開 `/ios/Runner.xcodeproj` 這個 iOS 專案,嗯,有點繞口,不過就這樣吧。然後啊,進到 `Runner -> General -> Supported Destinations` 裡頭,看一下那邊有那些選項。其實很容易搞錯,我自己也常忘記上次設了什麼。你只要確定,只勾了 iPhone 跟 iPad 就對了。有時候會一不小心多選 visionOS 或 macOS,唉,不知道你有沒有試過,一旦這樣,即使人在用 iPhone 或 iPad,那個搖動功能還是會出問題——超怪。
說回來,Android 上好像可以玩自訂的搖動手勢,可是在 iOS 就沒門兒,目前真的沒看到能調整的設定。不曉得以後會不會有啦,但現在就是無解。這個套件它內部,就是直接吃 `UIEvent.EventType.motion` 的 `motionshake` 嗎?對,就是用那一招,所以根本沒啥客製化空間可言。欸我剛剛差點又岔題。
## **建立 UI —— 實作環節 🎉**
呃,好啦,接下來就要開始動手做 widget 介面了。每次到這步總覺得前面都只是暖身而已。有點想喝水…唉等等還是先寫完好了。【注意事項】, 嗯,其實這些筆記只是給你參考創作文章內容用的,不是讓你直接照抄放進去。如果哪天腦袋亂掉,把說明也寫進去了,就糟糕囉!
說回來,Android 上好像可以玩自訂的搖動手勢,可是在 iOS 就沒門兒,目前真的沒看到能調整的設定。不曉得以後會不會有啦,但現在就是無解。這個套件它內部,就是直接吃 `UIEvent.EventType.motion` 的 `motionshake` 嗎?對,就是用那一招,所以根本沒啥客製化空間可言。欸我剛剛差點又岔題。
## **建立 UI —— 實作環節 🎉**
呃,好啦,接下來就要開始動手做 widget 介面了。每次到這步總覺得前面都只是暖身而已。有點想喝水…唉等等還是先寫完好了。【注意事項】, 嗯,其實這些筆記只是給你參考創作文章內容用的,不是讓你直接照抄放進去。如果哪天腦袋亂掉,把說明也寫進去了,就糟糕囉!

主程式樹要插哪裡,比MaterialApp高還是低呢?
### 包裝元件(ShakeToReport)
這個 `StatefulWidget`,欸,就是叫做 `ShakeToReport` 啦。其實它的結構很直白啦,就是把那個套件裡面的 `ShakeGesture` 拿來用,包在外層當應用程式的「殼」——有點像你拿杯子裝水一樣。不過重點是什麼?嗯,主要就是只要偵測到搖動手機,它就會跳出下面那個彈出視窗。唉,有時候我真的很怕自己一激動把手機甩飛,不過先不管啦。我們回來看邏輯:這組件負責管理何時要顯示下方 modal sheet,其實算蠻貼心。
final Widget child;
@override
State
這個 `StatefulWidget`,欸,就是叫做 `ShakeToReport` 啦。其實它的結構很直白啦,就是把那個套件裡面的 `ShakeGesture` 拿來用,包在外層當應用程式的「殼」——有點像你拿杯子裝水一樣。不過重點是什麼?嗯,主要就是只要偵測到搖動手機,它就會跳出下面那個彈出視窗。唉,有時候我真的很怕自己一激動把手機甩飛,不過先不管啦。我們回來看邏輯:這組件負責管理何時要顯示下方 modal sheet,其實算蠻貼心。
dart
class ShakeToReport extends StatefulWidget {
const ShakeToReport({required this.child, super.key});
final Widget child;
@override
State
createState() => _ShakeToReportState();
}
<pre><code class="language-python">python
class _ShakeToReportState extends State<ShakeToReport> {
bool _isModalShown = false;
void _showModal() {
if (_isModalShown) {
return;
}
setState(() {
_isModalShown = true;
});
css
showModalBottomSheet(
context: context,
builder: (_) => const ShakeToReportModal(),
).then((result) {
setState(() {
_isModalShown = false;
});
});
}
@override
Widget build(BuildContext context) {
// 欸,我剛剛是不是忘了喝水?啊沒事…繼續。這邊就是回傳一個 ShakeGesture,把 onShake 指向上面的 _showModal,然後把原本的小孩 widget 包進去。
return ShakeGesture(onShake: _showModal, child: widget.child);
}
}
### 下方彈出視窗(ShakeToReportModal)
使用者可以依照自己的品味設計對話框,不過我懶得搞太複雜,就直接給你一個超陽春範例好了。有時候覺得功能越多越亂,但現在流行極簡嘛。
dart
class ShakeToReportModal extends StatelessWidget {
const ShakeToReportModal({super.key});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SingleChildScrollView(
padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 96),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'報告障礙',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
'有什麼功能未如預期運作嗎?'
'歡迎提供回饋,以協助我們讓'
'這個應用程式對所有人更加完善!',
),
),
TextButton(
onPressed: () {
// TODO:待實作
},
style: ButtonStyle(
backgroundColor: WidgetStateColor.resolveWith(
(_) => colors.primary,
),
foregroundColor: WidgetStateColor.resolveWith(
(_) => colors.onPrimary,
),
),
child: const Text('回報障礙'),
),
],
),
);
}
}
### 將包裝元件加入應用程式
最後啦,你還是得乖乖把這包裝元件塞到適合的位置才行。大部分情境下,其實直接丟在 widget 樹的最外面、可是在 `MaterialApp` 的下一層——嗯,大概就是讓整支 app 都能聽得到搖動訊號吧。不曉得你會不會跟我一樣老是不小心搖到手機,有時候真想問設計師到底哪根筋想加這種互動。不過話說回來,只要照著放位置,整體流程就通了。
功能其實啟動很快,但你有GitHub完整範例嗎
嗯,這功能你大概隨手就能塞進自己的 App 裡,沒什麼特別困難的。
class MyApp extends StatelessWidget {
const MyApp({super.key});</code></pre>
唉,我怎麼每次都在想晚餐吃什麼?好啦、拉回來。
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const ShakeToReport(child: ShakeToReportHome()),
);
}
}
其實你現在就能直接體驗這個功能,只要把你的 App 啟動起來,然後搖一搖手機——對,就是物理上地晃它一下,看會發生啥。其實我自己測的時候還不小心差點把手機甩出去(嗯,下次還是拿穩)。完整範例程式碼在 GitHub 上頭都找得到:
> **GitHub - EXXETA/flutter_shake_to_report: A Flutter implementation enabling users to to trigger a...**
## 為了讓體驗更加完整
欸說真的,有時候只是單純偵測搖晃好像不夠用吧,所以其實可以再加點料——像是加個回報表單?讓用戶搖完之後自動跳轉到某個畫面,可以寫下遇到什麼奇葩 bug 或建議。
【注意事項】, 這份指南旨在協助文章內容的創作,而非成為實際內容的一部分。在撰寫時,請避免直接引用上述說明,產生的文章不應包含任何輔助說明、創作指導或其它非內容性的文字。

加點花樣:報告表單、可關閉、API連接管道等想像空間
這其實還是得看你的 API 結構啦,嗯,雖然這種事情常常講來講去都差不多,但有時候細節又特別難搞清楚。說到讓某個功能變成選擇性的,其實方法挺單純的——直接給使用者一個可以把它關掉的開關,例如在彈出視窗裡面設一個核取方塊什麼的。不過,有時我都會忘記,應用程式設定那邊也得提供調整這項設定的管道才行啊,不然每次都要回頭找彈出視窗真的是蠻煩人的。
欸,我剛剛是不是想起什麼來著?哦對了,好像前幾天也遇過類似狀況,結果最後因為沒注意到細部選項整個卡住,真的是欲哭無淚。但拉回正題,如果你想要用戶能夠控制功能開關,那就千萬別偷懶,一定要記得兩邊(就是視窗和應用程式設定)都要加上這個選項。唉,說著說著又覺得自己最近精神有點渙散,好吧,不扯遠了。重點就是:可選功能、明確開關、雙重設置——大概就是這樣子。
欸,我剛剛是不是想起什麼來著?哦對了,好像前幾天也遇過類似狀況,結果最後因為沒注意到細部選項整個卡住,真的是欲哭無淚。但拉回正題,如果你想要用戶能夠控制功能開關,那就千萬別偷懶,一定要記得兩邊(就是視窗和應用程式設定)都要加上這個選項。唉,說著說著又覺得自己最近精神有點渙散,好吧,不扯遠了。重點就是:可選功能、明確開關、雙重設置——大概就是這樣子。
最後碎唸,不多不少,這招讓用戶直接參與改善
說真的,有時候寫 Flutter,會覺得自己快變機器人了,但其實只要幾行程式碼——真的就是那麼簡單(雖然每次都懷疑會不會哪天踩雷)——就可以讓你的應用多一個直接回報問題的功能。嗯,這種設計說包容性也好,實用也罷,反正讓使用者不用在茫茫設定裡迷失,只要遇到狀況就能馬上反映,很直接。有時想到這裡會分神去想:如果自己是那個用戶,大概也希望事情能這麼順吧?唉,不過現實總有些小插曲,好啦拉回來。
至於持續優化的事情啊,說白了,就是讓體驗慢慢變順、障礙越來越少。大概很多人一開始沒什麼感覺,但等真正需要無障礙的人遇到困難時,就知道那功能到底有多重要。開發過程嘛,有時候挺有趣的,偶爾卡住還是會爆粗口——不過據說這也是成長的一部分?祝你寫程式時心情別被 bug 搞壞,也許…成功就在下一行呢。(Jonas Klock 提供)
至於持續優化的事情啊,說白了,就是讓體驗慢慢變順、障礙越來越少。大概很多人一開始沒什麼感覺,但等真正需要無障礙的人遇到困難時,就知道那功能到底有多重要。開發過程嘛,有時候挺有趣的,偶爾卡住還是會爆粗口——不過據說這也是成長的一部分?祝你寫程式時心情別被 bug 搞壞,也許…成功就在下一行呢。(Jonas Klock 提供)
