182 lines
6.1 KiB
Dart
182 lines
6.1 KiB
Dart
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,
|
||
);
|
||
}
|
||
}
|