refactor(init): 优化应用初始化流程,将权限请求延迟到引导完成后

- 将服务初始化拆分为基础服务和权限相关服务两个阶段
- 基础服务(用户数据、HealthKit初始化、快捷动作等)在应用启动时立即执行
- 权限相关服务(通知、HealthKit权限请求)仅在用户完成引导流程后才执行
- 在Redux store中添加onboardingCompleted状态管理
- 引导页面完成时通过Redux更新状态而非直接操作AsyncStorage
- 启动页面从预加载数据中读取引导完成状态,避免重复读取存储
- 使用ref防止权限服务重复初始化
This commit is contained in:
richarjiang
2025-11-14 14:10:52 +08:00
parent 7bd0b5fc52
commit 8cffbb990a
4 changed files with 91 additions and 72 deletions

View File

@@ -51,11 +51,12 @@ if (__DEV__) {
function Bootstrapper({ children }: { children: React.ReactNode }) { function Bootstrapper({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { profile } = useAppSelector((state) => state.user); const { profile, onboardingCompleted } = useAppSelector((state) => state.user);
const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule); const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule);
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false); const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const { isLoggedIn } = useAuthGuard() const { isLoggedIn } = useAuthGuard()
const fastingHydrationRequestedRef = React.useRef(false); const fastingHydrationRequestedRef = React.useRef(false);
const permissionInitializedRef = React.useRef(false);
// 初始化快捷动作处理 // 初始化快捷动作处理
useQuickActions(); useQuickActions();
@@ -94,11 +95,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
} }
}, [isLoggedIn]); }, [isLoggedIn]);
// ==================== 基础服务初始化(不需要权限,总是执行)====================
React.useEffect(() => { React.useEffect(() => {
// ==================== 第一优先级立即执行关键路径0-500ms==================== const initializeBasicServices = async () => {
const initializeCriticalServices = async () => {
try { try {
logger.info('🚀 开始初始化关键服务...'); logger.info('🚀 开始初始化基础服务(不需要权限)...');
// 1. 加载用户数据(首屏展示需要) // 1. 加载用户数据(首屏展示需要)
await dispatch(fetchMyProfile()); await dispatch(fetchMyProfile());
@@ -108,57 +109,65 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
initializeHealthPermissions(); initializeHealthPermissions();
logger.info('✅ HealthKit 权限系统初始化完成'); logger.info('✅ HealthKit 权限系统初始化完成');
// 3. 初始化通知服务基础功能 // 3. 初始化快捷动作(用户可能立即使用)
await notificationService.initialize();
logger.info('✅ 通知服务初始化完成');
// 4. 初始化快捷动作(用户可能立即使用)
await setupQuickActions(); await setupQuickActions();
logger.info('✅ 快捷动作初始化完成'); logger.info('✅ 快捷动作初始化完成');
// 5. 清空 AI 教练会话缓存(轻量操作) // 4. 清空 AI 教练会话缓存(轻量操作)
clearAiCoachSessionCache(); clearAiCoachSessionCache();
logger.info('✅ AI 教练缓存清理完成'); logger.info('✅ AI 教练缓存清理完成');
logger.info('🎉 关键服务初始化完成'); // 5. 初始化喝水记录 Bridge
initializeWaterRecordBridge();
logger.info('✅ 喝水记录 Bridge 初始化完成');
logger.info('🎉 基础服务初始化完成');
} catch (error) { } catch (error) {
logger.error('❌ 关键服务初始化失败:', error); logger.error('❌ 基础服务初始化失败:', error);
} }
}; };
// ==================== 第二优先级短延迟1-2秒后执行==================== initializeBasicServices();
const initializeSecondaryServices = () => { }, [dispatch]);
setTimeout(async () => {
try {
logger.info('⏰ 开始初始化次要服务...');
// 1. 请求 HealthKit 权限(延迟到 3 秒,避免打断用户浏览) // ==================== 权限相关服务初始化(仅在 onboarding 完成后执行)====================
setTimeout(async () => { React.useEffect(() => {
try { // 如果还没完成 onboarding或已经初始化过权限则跳过
await ensureHealthPermissions(); if (!onboardingCompleted || permissionInitializedRef.current) {
logger.info('✅ HealthKit 权限请求完成'); return;
} catch (error) { }
logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error);
}
}, 3000);
// 2. 初始化喝水记录 Bridge permissionInitializedRef.current = true;
initializeWaterRecordBridge();
logger.info('✅ 喝水记录 Bridge 初始化完成');
// 3. 异步同步 Widget 数据(不阻塞主流程) const initializePermissionServices = async () => {
syncWidgetDataInBackground(); try {
logger.info('🔐 开始初始化需要权限的服务onboarding 已完成)...');
logger.info('🎉 次要服务初始化完成'); // 1. 初始化通知服务(包含权限请求)
} catch (error) { await notificationService.initialize();
logger.error('❌ 次要服务初始化失败:', error); logger.info('✅ 通知服务初始化完成');
}
}, 2000); // 2. 延迟请求 HealthKit 权限(避免立即弹窗打断用户)
setTimeout(async () => {
try {
await ensureHealthPermissions();
logger.info('✅ HealthKit 权限请求完成');
} catch (error) {
logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error);
}
}, 2000);
// 3. 异步同步 Widget 数据
syncWidgetDataInBackground();
logger.info('🎉 权限相关服务初始化完成');
} catch (error) {
logger.error('❌ 权限相关服务初始化失败:', error);
}
}; };
// ==================== 第三优先级中延迟3-5秒后或交互完成后执行)==================== // ==================== 后台服务初始化(延迟执行)====================
const initializeBackgroundServices = () => { const initializeBackgroundServices = () => {
// 使用 InteractionManager 确保在所有交互和动画完成后再执行
const { InteractionManager } = require('react-native'); const { InteractionManager } = require('react-native');
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
@@ -183,7 +192,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
}); });
}; };
// ==================== 第四优先级:空闲时执行(不影响用户体验)==================== // ==================== 空闲服务初始化====================
const initializeIdleServices = () => { const initializeIdleServices = () => {
setTimeout(async () => { setTimeout(async () => {
try { try {
@@ -202,7 +211,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
} catch (error) { } catch (error) {
logger.error('❌ 空闲服务初始化失败:', error); logger.error('❌ 空闲服务初始化失败:', error);
} }
}, 8000); // 8秒后执行确保不影响用户体验 }, 8000);
}; };
// ==================== 辅助函数 ==================== // ==================== 辅助函数 ====================
@@ -350,13 +359,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
} }
}; };
// ==================== 执行初始化流程 ==================== // 执行权限相关初始化
initializePermissionServices();
// 立即执行关键服务
initializeCriticalServices();
// 延迟执行次要服务
initializeSecondaryServices();
// 交互完成后执行后台服务 // 交互完成后执行后台服务
initializeBackgroundServices(); initializeBackgroundServices();
@@ -364,7 +368,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
// 空闲时执行非关键服务 // 空闲时执行非关键服务
initializeIdleServices(); initializeIdleServices();
}, [dispatch, profile.name]); }, [onboardingCompleted, profile.name]);
React.useEffect(() => { React.useEffect(() => {

View File

@@ -6,9 +6,6 @@ import { preloadUserData } from '@/store/userSlice';
import { router } from 'expo-router'; import { router } from 'expo-router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ActivityIndicator, View } from 'react-native'; import { ActivityIndicator, View } from 'react-native';
import AsyncStorage from '@/utils/kvStore';
import { STORAGE_KEYS } from '@/services/api';
const ONBOARDING_COMPLETED_KEY = STORAGE_KEYS.onboardingCompleted;
export default function SplashScreen() { export default function SplashScreen() {
const backgroundColor = useThemeColor({}, 'background'); const backgroundColor = useThemeColor({}, 'background');
@@ -22,27 +19,28 @@ export default function SplashScreen() {
const checkOnboardingStatus = async () => { const checkOnboardingStatus = async () => {
try { try {
// 先预加载用户数据,这样进入应用时就有正确的 token 状态 // 先预加载用户数据,包括 onboarding 状态
console.log('开始预加载用户数据...'); console.log('开始预加载用户数据(包含 onboarding 状态)...');
await preloadUserData(); const userData = await preloadUserData();
console.log('用户数据预加载完成'); console.log('用户数据预加载完成onboarding 状态:', userData.onboardingCompleted);
// 初始化推送通知(不阻塞应用启动) // 初始化推送通知(不阻塞应用启动,且不会请求权限
console.log('开始初始化推送通知...'); console.log('开始初始化推送通知基础服务...');
initializePushNotifications().catch((error) => { initializePushNotifications().catch((error) => {
console.warn('推送通知初始化失败,但不影响应用正常使用:', error); console.warn('推送通知初始化失败,但不影响应用正常使用:', error);
}); });
const onboardingCompleted = await AsyncStorage.getItem(ONBOARDING_COMPLETED_KEY); // 根据预加载的状态决定跳转
if (userData.onboardingCompleted) {
if (onboardingCompleted === 'true') { console.log('用户已完成引导,跳转到统计页面');
router.replace(ROUTES.TAB_STATISTICS); router.replace(ROUTES.TAB_STATISTICS);
} else { } else {
console.log('用户未完成引导,跳转到引导页面');
router.replace(ROUTES.ONBOARDING); router.replace(ROUTES.ONBOARDING);
} }
} catch (error) { } catch (error) {
console.error('检查引导状态或预加载用户数据失败:', error); console.error('检查引导状态或预加载用户数据失败:', error);
// 如果出现错误,仍然进入应用,但可能会有状态更新 // 如果出现错误,默认进入应用(假设已完成引导)
router.replace(ROUTES.TAB_STATISTICS); router.replace(ROUTES.TAB_STATISTICS);
} }
setIsLoading(false); setIsLoading(false);

View File

@@ -22,10 +22,8 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { palette } from '@/constants/Colors'; import { palette } from '@/constants/Colors';
import { ROUTES } from '@/constants/Routes'; import { ROUTES } from '@/constants/Routes';
import { STORAGE_KEYS } from '@/services/api'; import { useAppDispatch } from '@/hooks/redux';
import AsyncStorage from '@/utils/kvStore'; import { setOnboardingCompleted } from '@/store/userSlice';
const ONBOARDING_COMPLETED_KEY = STORAGE_KEYS.onboardingCompleted;
type OnboardingSlide = { type OnboardingSlide = {
key: string; key: string;
@@ -63,6 +61,7 @@ const SLIDES: OnboardingSlide[] = [
export default function OnboardingScreen() { export default function OnboardingScreen() {
const router = useRouter(); const router = useRouter();
const dispatch = useAppDispatch();
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const listRef = useRef<FlatList<OnboardingSlide>>(null); const listRef = useRef<FlatList<OnboardingSlide>>(null);
@@ -92,9 +91,10 @@ export default function OnboardingScreen() {
); );
const completeOnboarding = useCallback(async () => { const completeOnboarding = useCallback(async () => {
await AsyncStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); // 通过 Redux 更新 onboarding 状态(会自动保存到 AsyncStorage
await dispatch(setOnboardingCompleted());
router.replace(ROUTES.TAB_STATISTICS); router.replace(ROUTES.TAB_STATISTICS);
}, [router]); }, [dispatch, router]);
const handleSkip = useCallback(() => { const handleSkip = useCallback(() => {
completeOnboarding(); completeOnboarding();

View File

@@ -10,15 +10,17 @@ let preloadedUserData: {
token: string | null; token: string | null;
profile: UserProfile; profile: UserProfile;
privacyAgreed: boolean; privacyAgreed: boolean;
onboardingCompleted: boolean;
} | null = null; } | null = null;
// 预加载用户数据的函数 // 预加载用户数据的函数
export async function preloadUserData() { export async function preloadUserData() {
try { try {
const [profileStr, privacyAgreedStr, token] = await Promise.all([ const [profileStr, privacyAgreedStr, token, onboardingCompletedStr] = await Promise.all([
AsyncStorage.getItem(STORAGE_KEYS.userProfile), AsyncStorage.getItem(STORAGE_KEYS.userProfile),
AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed), AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed),
AsyncStorage.getItem(STORAGE_KEYS.authToken), AsyncStorage.getItem(STORAGE_KEYS.authToken),
AsyncStorage.getItem(STORAGE_KEYS.onboardingCompleted),
]); ]);
let profile: UserProfile = { let profile: UserProfile = {
@@ -35,20 +37,24 @@ export async function preloadUserData() {
} }
const privacyAgreed = privacyAgreedStr === 'true'; const privacyAgreed = privacyAgreedStr === 'true';
const onboardingCompleted = onboardingCompletedStr === 'true';
// 如果有 token需要设置到 API 客户端 // 如果有 token需要设置到 API 客户端
if (token) { if (token) {
await setAuthToken(token); await setAuthToken(token);
} }
preloadedUserData = { token, profile, privacyAgreed }; preloadedUserData = { token, profile, privacyAgreed, onboardingCompleted };
return preloadedUserData; return preloadedUserData;
} catch (error) { } catch (error) {
console.error('预加载用户数据失败:', error); console.error('预加载用户数据失败:', error);
preloadedUserData = { preloadedUserData = {
token: null, profile: { token: null,
profile: {
memberNumber: 0 memberNumber: 0
}, privacyAgreed: false },
privacyAgreed: false,
onboardingCompleted: false
}; };
return preloadedUserData; return preloadedUserData;
} }
@@ -56,7 +62,7 @@ export async function preloadUserData() {
// 获取预加载的用户数据 // 获取预加载的用户数据
function getPreloadedUserData() { function getPreloadedUserData() {
return preloadedUserData || { token: null, profile: {}, privacyAgreed: false }; return preloadedUserData || { token: null, profile: {}, privacyAgreed: false, onboardingCompleted: false };
} }
export type Gender = 'male' | 'female' | ''; export type Gender = 'male' | 'female' | '';
@@ -108,6 +114,7 @@ export type UserState = {
error: string | null; error: string | null;
weightHistory: WeightHistoryItem[]; weightHistory: WeightHistoryItem[];
activityHistory: ActivityHistoryItem[]; activityHistory: ActivityHistoryItem[];
onboardingCompleted: boolean; // 是否完成引导流程
}; };
export const DEFAULT_MEMBER_NAME = '朋友'; export const DEFAULT_MEMBER_NAME = '朋友';
@@ -128,6 +135,7 @@ const getInitialState = (): UserState => {
error: null, error: null,
weightHistory: [], weightHistory: [],
activityHistory: [], activityHistory: [],
onboardingCompleted: preloaded.onboardingCompleted, // 引导完成状态
}; };
}; };
@@ -207,6 +215,12 @@ export const setPrivacyAgreed = createAsyncThunk('user/setPrivacyAgreed', async
return true; return true;
}); });
// 设置 onboarding 完成状态
export const setOnboardingCompleted = createAsyncThunk('user/setOnboardingCompleted', async () => {
await AsyncStorage.setItem(STORAGE_KEYS.onboardingCompleted, 'true');
return true;
});
export const logout = createAsyncThunk('user/logout', async () => { export const logout = createAsyncThunk('user/logout', async () => {
await Promise.all([ await Promise.all([
AsyncStorage.removeItem(STORAGE_KEYS.authToken), AsyncStorage.removeItem(STORAGE_KEYS.authToken),
@@ -364,6 +378,9 @@ const userSlice = createSlice({
}) })
.addCase(setPrivacyAgreed.fulfilled, (state) => { .addCase(setPrivacyAgreed.fulfilled, (state) => {
}) })
.addCase(setOnboardingCompleted.fulfilled, (state) => {
state.onboardingCompleted = true;
})
.addCase(fetchWeightHistory.fulfilled, (state, action) => { .addCase(fetchWeightHistory.fulfilled, (state, action) => {
state.weightHistory = action.payload; state.weightHistory = action.payload;
}) })