當 optional SDK 像 core infrastructure 一樣行事
Vibe-coded mobile app 裡一個很常見的 launch failure 是:analytics、attribution、crash reporting 和真正 critical 的 service 被放在同一條初始化路徑上。如果這些 optional SDK 裡有一個在某個 device 或某種 network condition 下出問題,整個 app 可能會在第一個 screen 出現之前就直接死掉。
會變成這樣,是因為 AI 生成的 setup code 很容易把每一個 dependency 都當成同等重要。沒有明確的 tier system,must-have infrastructure 和 nice-to-have observer 之間就沒有架構層面的區分。結果就是所有東西都會意外變成 must-have。
依賴的三個 tier
你的 app 裡每一個 dependency,實際上都只會落在三個 bucket 的其中一個。把這三條線明確畫出來,就是一個能撐過成長期的 app,和一個被無數 production crash 慢慢拖死的 app 之間的差別:
- Hard dependencies:這些一旦失敗,你的 app 就無法 render。config parsing、storage 建立、core service 建構。這些是地基。
- Soft dependencies:這些可以 graceful fail。auth、依賴 network 的 feature、依賴 permission 的 flow。app shell 仍然會 render,使用者看到的是降級體驗,而不是 crash。
- Optional dependencies:這些可以 silent fail。analytics、crash reporting、A/B testing。它們絕不能阻擋 app。它們是 observer,不是 participant。
大多數 Vibe-coded app 犯的錯,就是把所有東西都當成 hard dependency。每一個 SDK 都在 startup 階段初始化。每一次 failure 都直接變成 startup crash。但你的 analytics SDK 根本不該和 storage layer 擁有同樣的地位。
Hard Dependency Boundary
hard boundary 負責驗證 config 並建構 services。如果它失敗,app 應該顯示一個帶 retry button 的 error screen,而不是一片讓使用者去 App Store 留一星的空白白屏。
// src/context/hard-dependency-boundary.tsx
// This is the gatekeeper. If it fails, nothing else renders.
import * as SplashScreen from 'expo-splash-screen'
void SplashScreen.preventAutoHideAsync()
export function HardDependencyBoundary({
children,
}: {
children: (services: Services) => ReactNode
}) {
const { services, ready, error, retry } =
useHardDependencies()
useEffect(() => {
if (ready || error) void SplashScreen.hideAsync()
}, [ready, error])
if (error) {
return (
<ErrorScreen
title='Startup failed'
message={error.message}
onRetry={retry}
/>
)
}
if (!ready || !services) return null
return <>{children(services)}</>
}
注意這裡的 render prop pattern。boundary 會把初始化完成的 services 傳給子節點。子節點不負責建構 service;它們只負責接收。建構只發生一次,任何失敗也都在 boundary 被接住,而不是一路漏成 production crash report。
Optional Boundary
再對照一下 optional dependency。這些東西絕對不應該讓你的 app crash。假如 Mixpanel 掛了,你的 app 連抖都不該抖一下:
// src/context/optional-dependency-boundary.tsx
// If analytics fails, we log a warning and keep going.
export function OptionalDependencyBoundary({
children,
}: PropsWithChildren) {
const { analytics, crashReporting } = useServices()
useEffect(() => {
try {
analytics.track('app_started')
} catch (error) {
if (__DEV__) console.warn('Analytics failed:', error)
}
try {
crashReporting.setContext('app', { started: true })
} catch (error) {
if (__DEV__) console.warn('Crash reporting failed:', error)
}
}, [analytics, crashReporting])
return <>{children}</>
}
兩者的姿態差異非常明顯。Hard boundary:失敗就顯示 error screen。Optional boundary:失敗就記一個 warning,然後繼續往前走。兩者都很明確。兩者都不會假裝自己是另一種東西。
大多數 Vibe-coded app 的 production crash,都是同一個錯誤導致的:把 soft 或 optional dependency 當成 hard dependency。Autotomy Expo Starter Pack 從第一天就把這些線畫清楚,所以你不用在凌晨 2 點才學到這一課。