Flutter 音量控制:App 自己的音量 vs 系統音量
做 App 內的「音量設定」時,常會冒出一個問題:App 到底能不能控制自己的音量?需不需要裝套件? 關鍵在於先分清楚「音量」其實有兩個不同層次。
兩種音量
1. 播放器自己的音量(per-player volume)
每個媒體播放套件都提供自己的音量 API,範圍 0.0–1.0:
1// video_player
2controller.setVolume(0.5);
3
4// audioplayers / just_audio 也都有對應的 setVolume這是 App 控制「自己播放的聲音」 的方式——不需要任何額外套件,播放套件本身就帶。想調哪個音源就對哪個播放器設音量,範圍精準、互不影響。
2. 系統媒體音量(device / stream volume)
就是 OS 的音量滑桿、實體音量鍵所控制的那個值(Android 的 STREAM_MUSIC 等音訊串流)。它是整台裝置、所有 App 共用的。
Flutter 核心沒有暴露讀寫系統音量的 API,要從 App 動它得裝 plugin(例如 volume_controller、flutter_volume_controller),它們底層去戳平台的 AudioManager。
沒有「每個 App 的音量」這種 OS 概念
一個容易誤解的點:標準 Android 沒有「這個 App 的音量」這種 OS 層級的單一旋鈕。音量只有兩端:
- 每個播放器自己的音量(App 說了算)。
- 系統串流音量(全域,需 plugin 才能從 App 改)。
所以「App 主音量」的體驗,做法是App 自己維護一個值、套用到它所有的播放器上——這個「主音量」是應用層自己合成出來的,不是 OS 給的。
兩者是相乘的
實際聽到的響度大致是:
1實際響度 ≈ 播放器音量 (0–1) × 系統串流音量兩個是獨立、可相乘、可共存的旋鈕。這也代表:就算 App 把播放器音量設成最大 1.0,若裝置系統音量被轉到很小或靜音,還是不會大聲——App 的設定是相對衰減,不是絕對音量。
相乘只是心智模型:系統音量刻度到實際增益是非線性的(多為對數 / dB 級距),也沒計入 audio focus、其他 App 搶焦點、輸出裝置增益等因素。但「兩個旋鈕獨立、App 只能相對衰減」這個結論不受影響。
為什麼多數情況不建議從 App 去改系統音量
如果需求只是「控制 App 自己發出的聲音」,用 per-player setVolume 就夠了。去操作全域系統音量通常弊大於利:
- 全域副作用:系統媒體音量是整台裝置共用的,動它會影響裝置上所有 App 的媒體輸出,範圍遠大於你想控制的目標。
- 跟使用者爭同一個控制點:實體音量鍵與 OS 滑桿是使用者的控制介面。App 也去寫系統音量,就變成同一個值有兩個主人——使用者調大、App 稍後又改回去,互相拉扯、行為難預測。
- 無法表達「只調自己、不動其他」:很多時候我們要的是「相對地」把某個音源調小或靜音(例如把廣告靜音、但其他媒體正常)。系統音量一動就是全部一起動,做不到這種區隔;per-player 音量剛好就是精準範圍。
- 為小需求引入平台依賴:per-player 音量本來就能完全解決,為此引入一個呼叫平台 API 的 plugin,只是多了依賴、權限與邊界情況。
- 相容性風險:系統音量 plugin 碰平台特定 API,跨 Android 版本/OEM(各手機廠牌客製 ROM)/模擬器行為常有差異;播放器音量由媒體框架處理,一致得多。
- 語意會被洗掉:你想存的是「某功能的音量」這個持久化設定;改成驅動系統音量,那個值本質是「裝置的音量」,OS 和其他 App 隨時會蓋掉它。
什麼時候才真的需要動系統音量
- 需求是「控制整台裝置的輸出」,包含非本 App 的聲音。
- 部署在 kiosk/POS,實體音量鍵被鎖死或無法操作,而店員需要一個 App 內入口去調整台裝置的音量。
即使是這些情境,也要清楚你是在動「裝置的音量」而非「App 自己的音量」,並接受它的全域性。
小結
- 控制 App 自己的聲音 → 用播放器的
setVolume,免 plugin、範圍精準。 - 想要「App 主音量」→ 自己維護一個值套到所有播放器,應用層合成。
- 控制整台裝置的媒體音量 → 才需要 plugin,但那是全域副作用,先確認需求真的是「裝置音量」再用。