跳至主要内容

課程:RN 跨平台開發基礎 第 10 堂:原生功能整合

權限策略與語音整合

想像一下,你剛下載一個新的閱讀器 APP,還沒看到任何一行字,畫面就連續彈出「要求存取位置」、「要求存取聯絡人」與「要求傳送推播通知」三個視窗。你的直覺反應是什麼?通常是憤怒地按下「拒絕」,甚至直接卸載。

在行動裝置的世界裡,「權限」不僅是技術上的門檻,更是使用者信任的關鍵。當我們開發如 AuraFlow 這種需要推播通知,或閱讀器 APP 需要處理語音朗讀(TTS)的功能時,如何優雅地處理原生權限與底層硬體整合,是決定 APP 專業程度的分水嶺。

本節課將拆解兩個核心主題:第一,如何建立系統性的權限請求策略,避免被作業系統「永久封殺」;第二,如何深度整合 expo-speech,實作一個具備背景播放與音頻中斷處理能力的專業語音朗讀功能。


原生權限請求的系統性策略

在 React Native 中處理權限,不能只靠 await request() 一行程式碼。你必須理解作業系統是如何保護使用者的。

宣告與請求:iOS 與 Android 的底層機制

所有的原生功能存取都分為兩個階段:靜態宣告動態請求

  1. 靜態宣告 (Declaration): 這是告訴作業系統:「我的 APP 可能會用到這個功能。」
  • iOS: 所有的權限必須寫在 Info.plist 檔案中。除了勾選權限外,你還必須提供一段「使用說明文字」(Purpose String),這段文字會出現在系統彈窗中,解釋你為什麼需要這個權限。如果沒寫,APP 在請求權限時會直接崩潰。
  • Android: 必須寫在 AndroidManifest.xml 中。雖然 Android 6.0 之後引入了動態權限,但如果你沒有先在 Manifest 中宣告,動態請求一樣會失效。
  1. 動態請求 (Request): 這是發生在執行期(Runtime),當程式碼執行到某處時,彈出視窗詢問使用者是否同意。在 Expo 生態系中,我們通常使用模組內建的 requestPermissionsAsync() 方法。

情境式請求 (Contextual Permission) 原則

這是 UX 設計中最重要的一環:不要在 APP 啟動時請求所有權限。

我們應該採取「情境式請求」策略,即「在使用者即將受益於該功能的前一秒才請求」。

  • 錯誤範例: AuraFlow 一打開就要求推播權限。使用者還不知道這 APP 有什麼價值,通常會點拒絕。
  • 正確範例: 當使用者在 AuraFlow 中點擊「訂閱站方公告」或「設定提醒」時,才跳出權限請求。此時使用者有明確的心理預期,同意機率大幅提升。

iOS 的「只能請求一次」限制與對策

這開發者最常踩的雷。在 iOS 上,針對同一個權限,系統層級的彈窗一輩子只會出現一次

  • 如果使用者第一次點了「拒絕」,之後你的程式碼再怎麼呼叫 requestPermissionsAsync,系統都不會彈窗,而是直接回傳 denied
  • 這時,你唯一的機會是引導使用者手動開啟設定。

權限請求決策框架

為了處理這種複雜情況,建議建立如下的邏輯流程:

  1. 檢查狀態 (Check Status): 呼叫 getPermissionsAsync()
  2. 判斷是否為「未決定」(Undetermined):
  • 如果是,這代表使用者還沒看過系統彈窗。此時執行「動態請求」。
  1. 判斷是否為「已拒絕」(Denied):
  • 這代表使用者之前點過拒絕,或是在設定中關閉了。
  • 對策: 彈出一個自定義的對話框(Alert),解釋:「我們需要通知權限來發送重要更新,請前往設定開啟。」
  • 行動: 提供按鈕,利用 Linking.openSettings() 直接跳轉到系統設定頁面。
  1. 判斷是否為「已授權」(Granted):
  • 直接執行目標功能。

expo-speech 語音朗讀完整整合

在閱讀器 APP 中,語音朗讀(Text-to-Speech, TTS)是核心功能。我們使用的是 expo-speech,它本質上是 iOS AVSpeechSynthesizer 與 Android Android TTS 的橋接器。

核心 API 與參數:打造自然的朗讀體驗

一個好的朗讀體驗,取決於你如何微調 Speech.speak() 的 options 參數:

  • **rate**** (語速):** 預設通常是 1.0。在閱讀器中,建議提供 0.8 到 1.5 的調整區間。
  • **pitch**** (音調):** 影響聲音的高低。
  • **language**** (語言):** 這是最重要的部分。
    • 繁體中文: 應指定為 zh-TW(iOS 通常能精準辨識為台灣女聲)。
  • 簡體中文: 應指定為 zh-CN
  • 自動偵測: 如果你的 APP 支援繁簡轉換,記得在呼叫語音前,根據當前內容的語系切換這個參數,否則會出現用大陸口音唸台灣習慣用語的違和感。

程式碼實作範例

以下是一個簡化後的朗讀控制器邏輯,展示了如何處理連續朗讀(當一段唸完後自動跳下一段):

import * as Speech from 'expo-speech';

const SpeechController = () => {
const [isPaused, setIsPaused] = useState(false);

const startReading = (text: string) => {
Speech.speak(text, {
language: 'zh-TW',
rate: 1.0,
pitch: 1.0,
onStart: () => console.log('開始朗讀'),
onDone: () => {
console.log('朗讀完畢,準備翻頁');
// 這裡可以觸發自動翻頁或讀下一段的邏輯
handleNextChapter();
},
onError: (error) => console.error('語音出錯', error),
});
};

const handlePause = async () => {
// 注意:pause 在部分 Android 裝置支援度不一
await Speech.pause();
setIsPaused(true);
};

const handleResume = async () => {
await Speech.resume();
setIsPaused(false);
};

const handleStop = () => {
Speech.stop();
};

// ... 渲染 UI
};

背景播放與音頻中斷處理

如果使用者在聽書時,螢幕熄滅了或跳去回訊息,語音不應該中斷。這涉及到原生層級的「音頻會話管理」。

1. 背景播放配置 (Background Audio)

預設情況下,當 APP 進入背景,作業系統會為了省電而掛起(Suspend)所有進程,語音也會隨之停止。你必須在 app.json 中宣告你的 APP 是一個音頻播放器:

{
"expo": {
"ios": {
"infoPlist": {
"UIBackgroundModes": ["audio"]
}
},
"android": {
"permissions": ["FOREGROUND_SERVICE", "FOREGROUND_SERVICE_MEDIA_PLAYBACK"]
}
}
}

這僅是宣告,實際運行時仍需確保 JS Thread 沒有被完全凍結。

2. 音頻中斷 (Audio Interruption)

這是「專業感」的來源。當使用者正在聽書,突然電話響了,或者拔掉耳機,APP 應該怎麼做?

  • 來電: 系統會自動降低音量(Ducking)或暫停。你的 APP 應該監聽中斷事件,在電話結束後自動恢復(Resume)朗讀。
  • 限制提示: expo-speech 的限制在於它依賴系統內建 TTS。如果使用者的 Android 手機沒有下載中文語音包,朗讀就會失敗。在開發時,建議在進入朗讀模式前先用 Speech.isSpeakingAsync() 檢查狀態。

生命週期管理:避免「靈異語音」

在導覽系統中,我們學過頁面切換時元件不一定會銷毀(Unmount)。如果你在「章節頁面」開啟了朗讀,然後點擊「返回」回到書架,若沒有處理停止邏輯,你會發現語音還在繼續唸——這就是所謂的「靈異語音」現象。

我們必須結合 Topic 3.4 學過的 useFocusEffect 來進行清理:

import { useFocusEffect } from '@react-navigation/native';
import * as Speech from 'expo-speech';
import React from 'react';

function ChapterScreen() {
useFocusEffect(
React.useCallback(() => {
// 進入頁面時的操作 (可選)

return () => {
// 當頁面失去焦點 (Blur) 或銷毀時
console.log('離開章節,強制停止語音');
Speech.stop();
};
}, [])
);

return (
// ... 頁面內容
);
}

這確保了資源的精確控制,避免多個語音執行緒重疊導致的效能問題或奇怪的重音。


總結與技術邊界

雖然 expo-speech 強大且易用,但作為開發者,你必須清楚它的邊界:

  1. 音質受限: 它不是像 OpenAI TTS 或 Google Cloud TTS 那樣的高品質合成聲音,而是呼叫手機本地的引擎。音質取決於使用者手機裡裝的是什麼聲音。
  2. 語言支援: 如果使用者的系統語言沒下載對應的語音包,朗讀可能會變成「機械式」的單字拼讀甚至無聲。
  3. 細節控制: 它無法精確控制每個字的重音或情感。如果你需要像 Podcast 一樣自然的效果,未來可能需要轉向雲端 TTS 服務並改用 expo-av 播放音訊檔案。

藉由結合「權限請求策略」與「生命週期管理」,你已經能構建出一個行為穩定、對使用者友好的原生功能整合方案。這不僅解決了「功能能不能跑」的問題,更解決了「好不好用」的體驗問題。

關鍵回顧與橋接

在本節中,我們探討了跨平台開發中最具挑戰性的「原生溝通」部分。你學會了:

  • 權限決策路徑: 理解了 iOS 彈窗的唯一性,以及如何透過檢查狀態引導使用者進行二次授權。
  • 情境化請求: 建立了「價值先行、權限隨後」的 UX 心智模型。
  • 語音整合深度: 掌握了 expo-speech 的關鍵參數,並學會如何處理繁簡語系差異與背景播放配置。
  • 生命週期連動: 運用 useFocusEffect 的清理機制,解決了跨頁面時的原生資源殘留問題。

至此,我們已經完整覆蓋了從渲染、樣式、導覽到原生功能整合的所有核心課題。你的 APP 現在不僅在視覺上符合跨平台規範,在行為上也具備了與原生應用程式同等的專業度。

接下來,我們將進入本課程的最後一個部分:Review。我們將透過一系列的核心問題,幫你串聯起這堂課所有的知識點,並檢視你如何將這些底層邏輯應用到你現有的 APP 優化計畫中。