App 問題排查步驟:如何快速定位與解決應用程式常見錯誤

Published on: | Last updated:

最近在想一個功能,就是很多大 App 都有的那個… App 卡住或哪裡怪怪的時候,你就搖一下手機,然後它就跳出一個回報問題的視窗。像 Instagram 就有,對吧?老實說這功能真的蠻讚的,使用者不用大海撈針去找那個「聯絡我們」或「回報問題」的按鈕,當下遇到問題,搖一搖就能講,體驗好很多。

然後我就去查了一下,這東西在 Flutter 上面要自己做會不會很麻煩。結果發現… 欸,其實不會耶,甚至可以說蠻簡單的。今天就來邊想邊記錄一下,怎麼把這個「搖手機回報」功能加到自己的 Flutter App 裡。

重點一句話

簡單講,就是用一個現成的套件 `shake_gesture` 來偵測手機晃動,然後觸發一個 `showModalBottomSheet` 把回報視窗叫出來。整個過程不用寫什麼原生程式碼,大概幾十分鐘就能搞定。

搖晃手機後,從底部彈出的回報視窗示意圖
搖晃手機後,從底部彈出的回報視窗示意圖

這功能,真的有比較好嗎?

在直接衝進去寫 code 之前,先停下來想一下。這東西真的有比較好嗎?還是只是為了酷炫?我自己是覺得,在某些情境下,它真的很有用。

你可以想成,它是一種「即時情境回饋」的管道。特別是在 App 的 Beta 測試階段,或是給內部 QA 團隊用,這個超方便。測試人員不用再截圖、開另一個通訊軟體、然後描述老半天「我在哪個畫面的哪個按鈕下面看到怪怪的東西」。他只要當下搖一搖,系統甚至可以順便把當前的頁面資訊、使用者 ID 什麼的一起打包送出。

不過呢,當然也有缺點。如果你的 App 是個遊戲,或是一個需要使用者頻繁晃動手機操作的應用… 那可能就會一直誤觸,反而造成困擾。所以,也不是萬靈丹啦。

說到這個,市面上也不是只有一個套件能做這件事,我順手整理了一下幾個選項的感覺:

不同實作方式的快速比較
方法 優點 缺點(或說…要考慮的點) 適合誰
用 `shake_gesture` 套件 設定超簡單,幾乎是無腦安裝。文件也寫得很清楚。 客製化選項比較少,特別是 iOS 那邊幾乎沒得調。 大部分的 App、想快速驗證功能、不想搞太複雜的人。就是我們今天要用的方法。
用 `sensors_plus` 自己監聽 自由度超高!你可以自己決定加速度的閾值、偵測頻率等等。 你要自己處理演算法,判斷怎樣才算「一次有效的搖晃」,而不是使用者走路或搭車的晃動。要花點時間調校。 對偵測精準度有特殊要求,或是想做得很細緻的專案。
自己寫原生整合 (MethodChannel) 終極完全體。效能最好,可以呼叫到最底層的系統 API。 開發地獄。你要同時懂 Dart、Kotlin/Java、Swift/Objective-C。時間成本非常非常高。 除非你的 App 有幾百萬用戶,對效能跟體驗的要求到頂了,不然真的沒必要自虐。

怎麼做

好,廢話不多說。我們就選最簡單的路,用 `shake_gesture` 來做。流程大概是:裝套件 -> 搞定 Android 設定 -> 搞定 iOS 設定 -> 寫 UI。超直覺。

第一步:把套件加進來

這應該不用多說了。打開你的終端機,在專案底下跑這個指令:

flutter pub add shake_gesture

跑完之後,你的 `pubspec.yaml` 檔案裡應該會多一行 `shake_gesture` 的依賴。如果你的 VS Code 或 Android Studio 沒有自動幫你跑 `flutter pub get`,記得自己手動跑一下。

第二步:Android 這邊要加幾行字

Android 這邊需要手動設定一下。你要去打開 `android/app/src/main/AndroidManifest.xml` 這個檔案。對,就是那個看起來很複雜的 XML 檔。

然後在 `` 標籤裡面,加上這兩段 `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" />
        <!-- 其他的 activity 設定 -->
    </application> 
</manifest> 

這兩個值是幹嘛的?

  • SHAKE_FORCE: 簡單講就是搖晃的力道。數字越小,表示輕輕搖就會觸發。數字越大,你就要像在搖飲料一樣用力。
  • MIN_NUM_SHAKES: 就是要連續搖幾下才會觸發。

原文提供的 `5` 跟 `6` 是預設值。我自己是覺得可以先用這個,然後找幾個同事或朋友試用看看,如果他們覺得太難觸發或太容易誤觸,再來調整就好。

第三步:iOS 這邊… 比較特別

輪到 iOS 了。說真的,每次搞 Flutter 遇到 iOS 都會有點…嗯,你知道的。蘋果有它自己的規矩。

首先,你不用在程式碼裡加什麼設定,但你要打開 Xcode。在你的專案資料夾裡找到 `/ios/Runner.xcodeproj`,用 Xcode 打開它。然後照著 `Runner -> General -> Supported Destinations` 這個路徑點進去。

重點來了:**確保你只勾選了 iPhone 跟 iPad**。如果你勾了 `macOS` 或新的 `visionOS`,這個搖晃偵測功能… 就會直接罷工,連在你的 iPhone 上都沒用。很怪,但就是這樣。

然後,前面有提到,iOS 這邊是沒辦法讓你調整力道或次數的。因為這個套件在 iOS 底層用的是蘋果內建的 `UIEvent.EventType.motion` 事件,也就是 `motionshake`。蘋果並沒有開放參數給開發者調整。這點跟我們在台灣看到很多 App 功能的彈性很不一樣,台灣的 App 開發文化比較偏向快速迭代、給使用者最多選項,但蘋果的 [Apple Human Interface Guidelines] 就非常強調一致性跟簡潔,不希望開發者濫用會干擾主要操作的感測器功能。反過來說,Google 的 [Material Design] 指引就相對開放,鼓勵各種創新的互動方式。所以… 沒辦法,在 iOS 上就是得接受它預設的靈敏度,好不好用就看使用者回饋了。

第四步:把 UI 兜起來

設定都搞定了,終於可以來寫 Dart code 了。我們要寫兩個 Widget:一個是包在 App 外面的「監聽器」,另一個是跳出來的「回報視窗」。

Widget 結構示意:用 ShakeToReport 包住你的主畫面
Widget 結構示意:用 ShakeToReport 包住你的主畫面

首先是監聽器,我叫它 `ShakeToReport`。它是一個 `StatefulWidget`,因為它需要記錄「回報視窗是不是已經打開了」。

class ShakeToReport extends StatefulWidget {
  const ShakeToReport({required this.child, super.key});

  final Widget child;

  @override
  State<ShakeToReport> createState() => _ShakeToReportState();
}

class _ShakeToReportState extends State<ShakeToReport> {
  // 這個 flag 很重要,用來防止重複觸發
  bool _isModalShown = false;

  void _showModal() {
    // 如果視窗已經開了,就直接 return,不做事
    if (_isModalShown) {
      return;
    }

    // 先把 flag 設為 true,鎖起來
    setState(() {
      _isModalShown = true;
    });

    // 叫出回報視窗
    showModalBottomSheet(
      context: context,
      isScrollControlled: true, // 這行蠻好用的,可以讓你的 modal 高度自適應
      builder: (_) => const ShakeToReportModal(),
    ).then((_) {
      // then 會在視窗關閉後才執行
      // 這時候再把 flag 設回 false,解除鎖定
      setState(() {
        _isModalShown = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    // 核心就是這個 ShakeGesture Widget
    return ShakeGesture(
      onShake: _showModal, // 搖了就呼叫 _showModal
      child: widget.child, // 把我們的 App 內容放進來
    );
  }
}

那個 `_isModalShown` 的處理我覺得是個小細節但很重要。不然使用者只要持續搖晃,你的 App 可能會一直瘋狂想跳出新視窗,畫面會爛掉。

再來是回報視窗本身 `ShakeToReportModal`。這個就超簡單了,就是一個 `StatelessWidget`,裡面排版看你高興怎麼做。下面是個極簡範例:

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

  @override
  Widget build(BuildContext context) {
    // 拿一下 App 的主題顏色,這樣看起來比較有一致性
    final colors = Theme.of(context).colorScheme;

    return Padding(
      padding: const EdgeInsets.all(24),
      // 用 SingleChildScrollView 包起來,萬一內容太多手機太小才不會爆掉
      child: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch, // 讓按鈕可以滿版
          children: [
            const Text(
              '遇到問題了嗎?',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 16),
            const Text(
              '請告訴我們發生了什麼事,您的回饋能幫助我們把 App 變得更好!',
            ),
            const SizedBox(height: 24),
            // 這邊可以放文字輸入框、選項等等
            // TODO: 加上一個 TextField 讓使用者輸入文字

            const SizedBox(height: 24),
            TextButton(
              onPressed: () {
                // TODO: 實作回報的邏輯,例如把資料送到後端 API
                Navigator.of(context).pop(); // 按完就關掉視窗
              },
              style: TextButton.styleFrom(
                backgroundColor: colors.primary,
                foregroundColor: colors.onPrimary,
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text('送出回饋'),
            ),
          ],
        ),
      ),
    );
  }
}

最後一步,就是在你 App 的最外層,把剛剛寫的 `ShakeToReport` 包上去。通常會包在 `MaterialApp` 的 `home` 屬性外面,這樣整個 App 都能偵測到搖晃。

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // 就是這裡!
      home: const ShakeToReport(
        child: MyHomePage(title: '我的 App 首頁'),
      ),
    );
  }
}

這樣就大功告成了!跑起來你的 App,搖搖看手機,應該就能看到效果了。

還可以怎麼玩?讓體驗更完整

當然,上面只是最基礎的版本。如果真的要放到正式產品上,還有很多可以優化的細節。

  • 自動截圖:搖晃的當下,自動截一張目前的畫面。這樣回報問題時,開發者就能立刻知道是哪個頁面出錯,省去大量溝通成本。這需要用到像是 `screenshot` 之類的套件。
  • 整合後端服務:送出的回饋總要有地方去吧?你可以把它們送到 Firebase、Sentry,或是自己公司的後端 API。更簡單一點,甚至可以透過 `url_launcher` 直接打開 Email 或其他通訊軟體,把資訊帶過去。
  • 讓使用者可以關掉它:在回報視窗或 App 設定裡,加一個開關「關閉搖晃回報功能」。畢竟不是每個使用者都喜歡這個功能,給他們選擇的權利很重要。
更進階的回饋流程概念圖
更進階的回饋流程概念圖

我自己是覺得,加上自動截圖的功能,投資報酬率最高。對開發團隊來說,一張圖勝過千言萬語啊。

所以,總的來說,這是一個花費不多,但卻能有效改善特定情境(特別是測試期)下使用者體驗的小功能。有時候 App 的質感,就是靠這些小細節堆出來的。有空的話,不妨在你的專案上試試看吧!


換你說說看:

你覺得「搖晃回報」這個功能,最適合用在哪種 App 上?或是有沒有哪個 App 的回饋機制讓你印象深刻?在下面留言分享你的看法吧!

Related to this topic:

Comments

  1. Guest 2025-11-13 Reply
    每天處理 App 的 bug,這種事我早就習慣了。從矽谷到新加坡再回台北,好像日常差不多都圍繞著這個跑。不過最記得還是當時在倫敦的那次,真的很頭痛。團隊剛把一個社群 App 上架,結果隔天開始,一堆用戶就說閃退。國際市場壓力老實說很煩,一失誤,全世界投訴信馬上塞爆 inbox,那幾天真沒怎麼睡好。 每個工程師急著去抓 log。我那時候反正先直接把 crash 報告跟 stack trace 都弄出來,比較簡單的第一步其實就是看它發生在什麼平台、哪個作業系統,不然 Android 跟 iOS 問題有時候差很多,有沒有?後來一查,才知道根本是某個語言版本—西班牙文特別會暴走—字串格式化寫太隨便,就掛掉了。 跨區小問題,看似小但麻煩,其實你一定要站在其他語言用戶的角度檢查一下。不僅僅只是程式碼,有些細節只有當下那些人會遇到,你平常根本想不到。嗯對,那次修完之後,我們順手把 Crashlytics 的通知流程調整過,更好用了。
  2. Guest 2025-11-11 Reply
    嗯,其實我一直在想這個。唔,怎麼說……有時候一開始就急著下結論,好像會不小心跳過了某些細節。之前我弄錯誤的時候,不是立刻去看程式哪裡寫壞掉,結果東查西查才發現,啊原來根本就是資料來源怪怪的,並不是哪一行程式碼出包。你會有這種經驗嗎?還滿讓人頭痛的。
撥打專線 LINE免費通話