gcx/frontend/genex-mobile/lib/app/main_shell.dart

182 lines
6.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../app/theme/app_colors.dart';
import '../app/i18n/app_localizations.dart';
import '../core/updater/update_service.dart';
import '../core/providers/notification_badge_manager.dart';
import '../features/coupons/presentation/pages/home_page.dart';
import '../features/coupons/presentation/pages/market_page.dart';
import '../features/message/presentation/pages/message_page.dart';
import '../features/profile/presentation/pages/profile_page.dart';
/// GenexMobile主Shell - Bottom Navigation
///
/// Tab: 首页 / 交易 / 消息 / 我的
class MainShell extends StatefulWidget {
const MainShell({super.key});
@override
State<MainShell> createState() => _MainShellState();
}
/// 混入 WidgetsBindingObserver 以监听 App 生命周期变化(前后台切换)
class _MainShellState extends State<MainShell> with WidgetsBindingObserver {
int _currentIndex = 0;
/// 首次进入 Shell 是否已触发过更新检查(防止 didChangeDependencies 多次调用)
bool _updateChecked = false;
/// 上一次执行更新检查的时间(用于节流,避免短时间内重复检查)
DateTime? _lastUpdateCheck;
/// 本次 App 生命周期(冷启动到下次冷启动)是否已向用户展示过更新弹窗。
///
/// 设计逻辑:
/// - 若检查到新版本并弹窗,此标志置为 true之后从后台切回不再重复弹窗
/// 避免用户选择"稍后再说"后频繁打扰。
/// - 标志仅存活于内存App 被彻底杀死并重启后自动重置,届时再次检查。
bool _updatePromptShown = false;
final _pages = const [
HomePage(),
MarketPage(),
MessagePage(),
ProfilePage(),
];
@override
void initState() {
super.initState();
// 注册生命周期观察者,以便在 didChangeAppLifecycleState 中感知前后台切换
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 冷启动后仅执行一次:等待 3 秒页面稳定后检查更新
if (!_updateChecked) {
_updateChecked = true;
_checkForUpdate();
}
}
/// App 从后台切换回前台时触发
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// 本次 session 已弹过更新弹窗(用户选择了"稍后"),不再重复打扰,
// 等用户下次冷启动 App 时才会再次检查
if (_updatePromptShown) return;
final now = DateTime.now();
// 节流:两次后台唤起检查之间至少间隔 2 分钟,防止频繁切换时多次触发网络请求
if (_lastUpdateCheck == null ||
now.difference(_lastUpdateCheck!) > const Duration(minutes: 2)) {
_checkForUpdate();
}
}
}
/// 执行静默版本检查,有新版本时才弹更新弹窗
///
/// 流程:
/// 1. 记录本次检查时间(用于节流)
/// 2. 延迟 3 秒,确保页面已完全渲染
/// 3. 先调用 silentCheck() 查询服务端是否有新版本(不弹窗)
/// 4. 若有新版本,标记 _updatePromptShown = true 后调用 checkForUpdate() 弹窗
/// 若无新版本,静默结束,下次切回前台满 2 分钟后再次检查
Future<void> _checkForUpdate() async {
_lastUpdateCheck = DateTime.now();
await Future.delayed(const Duration(seconds: 3));
if (!mounted) return;
final updateService = UpdateService();
final versionInfo = await updateService.silentCheck();
if (!mounted) return;
if (versionInfo != null) {
// 有新版本:先锁定标志,再弹窗(无论用户选更新还是关闭,本 session 均不再弹)
_updatePromptShown = true;
await updateService.checkForUpdate(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: Container(
decoration: const BoxDecoration(
color: AppColors.surface,
border: Border(top: BorderSide(color: AppColors.borderLight, width: 0.5)),
),
child: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) => setState(() => _currentIndex = index),
destinations: [
_buildDestination(Icons.home_rounded, Icons.home_outlined, context.t('nav.home')),
_buildDestination(Icons.show_chart_rounded, Icons.show_chart_outlined, context.t('nav.trading')),
_buildBadgeDestination(
Icons.notifications_rounded,
Icons.notifications_outlined,
context.t('nav.messages'),
),
_buildDestination(Icons.person_rounded, Icons.person_outlined, context.t('nav.profile')),
],
),
),
);
}
NavigationDestination _buildDestination(
IconData selected,
IconData unselected,
String label,
) {
return NavigationDestination(
icon: Icon(unselected),
selectedIcon: Icon(selected),
label: label,
);
}
NavigationDestination _buildBadgeDestination(
IconData selected,
IconData unselected,
String label,
) {
return NavigationDestination(
icon: ValueListenableBuilder<int>(
valueListenable: NotificationBadgeManager().unreadCount,
builder: (context, count, _) {
if (count <= 0) return Icon(unselected);
return Badge(
label: Text('$count', style: const TextStyle(fontSize: 10)),
child: Icon(unselected),
);
},
),
selectedIcon: ValueListenableBuilder<int>(
valueListenable: NotificationBadgeManager().unreadCount,
builder: (context, count, _) {
if (count <= 0) return Icon(selected);
return Badge(
label: Text('$count', style: const TextStyle(fontSize: 10)),
child: Icon(selected),
);
},
),
label: label,
);
}
}