
本文讲解如何通过条件渲染与异步存储(asyncstorage)持久化自定义 splashscreen 状态,避免启动页与后续页面(如教程页、仪表盘页)逻辑冲突,确保定时器重置、权限弹窗时机准确,并支持冷启动/热启动的一致体验。
在 react Native 中实现真正可控的自定义启动页(SplashScreen),关键不在于“遮盖”主应用,而在于精确控制生命周期与状态流转。你当前将
- ✅ 定时器未重置:TutorialScreen 的计时器在 Splash 显示期间已开始运行(因组件早已挂载);
- ❌ 权限弹窗时机错位:DashboardScreen 的 requestPermissions() 在 Splash 还未卸载时就被调用,导致系统弹窗叠加在启动页上;
- ? 状态丢失:每次冷启动都重置 showSplash = true,无法区分“首次安装”、“用户跳过教程”或“已看完启动流程”。
正确方案:状态驱动 + 条件渲染 + 持久化
核心原则是:SplashScreen 必须独占初始渲染阶段,且其完成应触发明确的状态变更与持久化写入,之后才挂载 AppNavigator 及其子路由。
✅ 第一步:使用 AsyncStorage 持久化 splash 完成状态
// utils/splashState.ts import AsyncStorage from '@react-native-async-storage/async-storage'; const SPLASH_COMPLETED_KEY = '@splash:completed'; export const markSplashAsCompleted = async () => { await AsyncStorage.setItem(SPLASH_COMPLETED_KEY, 'true'); }; export const hasSplashCompleted = async (): Promise<boolean> => { const value = await AsyncStorage.getItem(SPLASH_COMPLETED_KEY); return value === 'true'; };
⚠️ 注意:AsyncStorage 是轻量级键值存储,适合保存布尔标志。若需更复杂状态(如最后教程步骤),可序列化为 json。
✅ 第二步:重构 App 入口 —— 使用 Loading 状态管理渲染流
在 App.tsx 或根组件中,引入 useState + useEffect 实现状态机:
import React, { useState, useEffect } from 'react'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { RootStoreProvider } from './stores'; import { ToggleStorybook } from './storybook'; import { SplashScreen } from './screens/SplashScreen'; import { AppNavigator } from './navigators/AppNavigator'; import { markSplashAsCompleted, hasSplashCompleted } from './utils/splashState'; export default function App() { const [appReady, setAppReady] = useState<'loading' | 'splash' | 'main'>('loading'); const [initialNavigationState, setInitialNavigationState] = useState<any>(undefined); useEffect(() => { const initApp = async () => { try { const completed = await hasSplashCompleted(); if (completed) { setAppReady('main'); } else { setAppReady('splash'); } } catch (e) { console.warn('Failed to check splash state', e); setAppReady('splash'); // fallback to splash on error } }; initApp(); }, []); if (appReady === 'loading') { return null; // 或显示极简 loading(如纯色背景),避免白屏 } return ( <ToggleStorybook> <RootStoreProvider value={rootStore}> <SafeAreaProvider initialMetrics={initialWindowMetrics}> <ErrorBoundary catchErrors="always"> <Host> {appReady === 'splash' ? ( <SplashScreen onComplete={async () => { await markSplashAsCompleted(); setAppReady('main'); }} /> ) : ( <AppNavigator initialState={initialNavigationState} onStateChange={onNavigationStateChange} onReady={() => { routingInstrumentation.registerNavigationContainer(navigationRef); }} /> )} </Host> </ErrorBoundary> </SafeAreaProvider> </RootStoreProvider> </ToggleStorybook> ); }
✅ 第三步:确保 SplashScreen 组件内部逻辑解耦
SplashScreen.tsx 应完全自主控制自身生命周期,不依赖外部定时器或导航副作用:
// screens/SplashScreen.tsx import React, { useEffect } from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; interface SplashScreenProps { onComplete: () => void; } export function SplashScreen({ onComplete }: SplashScreenProps) { useEffect(() => { // 模拟 3 秒启动动画(可替换为 Lottie 或数据加载) const timer = setTimeout(() => { onComplete(); // ✅ 主动通知父组件:启动完成 }, 3000); return () => clearTimeout(timer); // 清理防止内存泄漏 }, [onComplete]); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}> <ActivityIndicator size="large" color="#007AFF" /> <Text style={{ marginTop: 16, fontSize: 16 }}>Loading...</Text> </View> ); }
✅ 关键点:onComplete 回调由父组件统一处理状态切换与持久化,SplashScreen 本身无副作用。
✅ 第四步:后续页面行为自动对齐(无需手动加 timeout)
- TutorialScreen 现在仅在 AppNavigator 挂载后才会创建和执行 useEffect,其内部定时器将从 0 开始;
- DashboardScreen 的 requestLocationPermission() 将在 Splash 卸载、导航器就绪后才被调用,系统弹窗自然出现在当前活跃屏幕之上;
- 用户强制退出 App 后再次打开,hasSplashCompleted() 仍返回 true,直接进入主流程 —— 符合“仅首次展示启动页”的设计预期。
总结:三个必须遵守的原则
| 原则 | 说明 |
|---|---|
| 单入口状态机 | 整个 App 初始化流程由一个 appReady 状态驱动,杜绝多组件竞争渲染 |
| 持久化即契约 | 使用 AsyncStorage 记录“已启动完成”,而非临时内存变量,保障跨进程一致性 |
| 组件职责分离 | SplashScreen 只负责视觉与自身逻辑;状态流转、存储、导航交由根组件统一调度 |
此方案不依赖任何第三方启动屏库(满足你无法使用 react-native-splash-screen 的约束),同时为未来扩展(如 A/B 测试启动页、动态内容加载)预留清晰接口。只需确保 onComplete 被可靠调用,整个导航流即可稳定可控。