課程:RN 跨平台開發基礎 第 10 堂:原生功能整合
權限策略與語音整合
想像一下,你剛下載一個新的閱讀器 APP,還沒看到任何一行字,畫面就連續彈出「要求存取位置」、「要求存取聯絡人」與「要求傳送推播通知」三個視窗。你的直覺反應是什麼?通常是憤怒地按下「拒絕」,甚至直接卸載。
在行動裝置的世界裡,「權限」不僅是技術上的門檻,更是使用者信任的關鍵。當我們開發如 AuraFlow 這種需要推播通知,或閱讀器 APP 需要處理語音朗讀(TTS)的功能時,如何優雅地處理原生權限與底層硬體整合,是決定 APP 專業程度的分水嶺。
本節課將拆解兩個核心主題:第一,如何建立系統性的權限請求策略,避免被作業系統「永久封殺」;第二,如何深度整合 expo-speech,實作一個具備背景播放與音頻中斷處理能力的專業語音朗讀功能。
原生權限請求的系統性策略
在 React Native 中處理權限,不能只靠 await request() 一行程式碼。你必須理解作業系統是如何保護使用者的。
宣告與請求:iOS 與 Android 的底層機制
所有的原生功能存取都分為兩個階段:靜態宣告與動態請求。
- 靜態宣告 (Declaration): 這是告訴作業系統:「我的 APP 可能會用到這個功能。」
- iOS: 所有的權限必須寫在
Info.plist檔案中。除了勾選權限外,你還必須提供一段「使用說明文字」(Purpose String),這段文字會出現在系統彈窗中,解釋你為什麼需要這個權限。如果沒寫,APP 在請求權限時會直接崩潰。 - Android: 必須寫在
AndroidManifest.xml中。雖然 Android 6.0 之後引入了動態權限,但如果你沒有先在 Manifest 中宣告,動態請求一樣會失效。
- 動態請求 (Request):
這是發生在執行期(Runtime),當程式碼執行到某處時,彈出視窗詢問使用者是否同意。在 Expo 生態系中,我們通常使用模組內建的
requestPermissionsAsync()方法。
情境式請求 (Contextual Permission) 原則
這是 UX 設計中最重要的一環:不要在 APP 啟動時請求所有權限。
我們應該採取「情境式請求」策略,即「在使用者即將受益於該功能的前一秒才請求」。
- 錯誤範例: AuraFlow 一打開就要求推播權限。使用者還不知道這 APP 有什麼價值,通常會點拒絕。
- 正確範例: 當使用者在 AuraFlow 中點擊「訂閱站方公告」或「設定提醒」時,才跳出權限請求。此時使用者有明確的心理預期,同意機率大幅提升。
iOS 的「只能請求一次」限制與對策
這開發者最常踩的雷。在 iOS 上,針對同一個權限,系統層級的彈窗一輩子只會出現一次。
- 如果使用者第一次點了「拒絕」,之後你的程式碼再怎麼呼叫
requestPermissionsAsync,系統都不會彈窗,而是直接回傳denied。 - 這時,你唯一的機會是引導使用者手動開啟設定。
權限請求決策框架
為了處理這種複雜情況,建議建立如下的邏輯流程:
- 檢查狀態 (Check Status): 呼叫
getPermissionsAsync()。 - 判斷是否為「未決定」(Undetermined):
- 如果是,這代表使用者還沒看過系統彈窗。此時執行「動態請求」。
- 判斷是否為「已拒絕」(Denied):
- 這代表使用者之前點過拒絕,或是在設定中關閉了。
- 對策: 彈出一個自定義的對話框(Alert),解釋:「我們需要通知權限來發送重要更新,請前往設定開啟。」
- 行動: 提供按鈕,利用
Linking.openSettings()直接跳轉到系統設定頁面。
- 判斷是否為「已授權」(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 強大且易用,但作為開發者,你必須清楚它的邊界:
- 音質受限: 它不是像 OpenAI TTS 或 Google Cloud TTS 那樣的高品質合成聲音,而是呼叫手機本地的引擎。音質取決於使用者手機裡裝的是什麼聲音。
- 語言支援: 如果使用者的系統語言沒下載對應的語音包,朗讀可能會變成「機械式」的單字拼讀甚至無聲。
- 細節控制: 它無法精確控制每個字的重音或情感。如果你需要像 Podcast 一樣自然的效果,未來可能需要轉向雲端 TTS 服務並改用
expo-av播放音訊檔案。
藉由結合「權限請求策略」與「生命週期管理」,你已經能構建出一個行為穩定、對使用者友好的原生功能整合方案。這不僅解決了「功能能不能跑」的問題,更解決了「好不好用」的體驗問題。
關鍵回顧與橋接
在本節中,我們探討了跨平台開發中最具挑戰性的「原生溝通」部分。你學會了:
- 權限決策路徑: 理解了 iOS 彈窗的唯一性,以及如何透過檢查狀態引導使用者進行二次授權。
- 情境化請求: 建立了「價值先行、權限隨後」的 UX 心智模型。
- 語音整合深度: 掌握了
expo-speech的關鍵參數,並學會如何處理繁簡語系差異與背景播放配置。 - 生命週期連動: 運用
useFocusEffect的清理機制,解決了跨頁面時的原生資源殘留問題。
至此,我們已經完整覆蓋了從渲染、樣式、導覽到原生功能整合的所有核心課題。你的 APP 現在不僅在視覺上符合跨平台規範,在行為上也具備了與原生應用程式同等的專業度。
接下來,我們將進入本課程的最後一個部分:Review。我們將透過一系列的核心問題,幫你串聯起這堂課所有的知識點,並檢視你如何將這些底層邏輯應用到你現有的 APP 優化計畫中。