flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:new_learn/core/components/privacy_startup_gate.dart';
|
||||
import 'package:new_learn/core/config/app_config.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/providers/network_status_provider.dart';
|
||||
import 'package:new_learn/core/providers/privacy_provider.dart';
|
||||
import 'package:new_learn/core/router/app_router.dart';
|
||||
import 'package:new_learn/core/services/local_notification_service.dart';
|
||||
import 'package:new_learn/core/utils/screen_adapt_template.dart'; // 复用 AppAdapt 适配配置
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【TabApp】App 顶层入口(MaterialApp)
|
||||
///
|
||||
/// 职责分工(对照 Vue 项目):
|
||||
/// main.dart = 启动脚本(main 函数)
|
||||
/// app.dart = 根组件:ScreenUtilInit + MaterialApp(主题 + 首页)
|
||||
/// main_tab_page.dart = 底部 4 tab 主框架
|
||||
/// pages/ = 各业务页面
|
||||
///
|
||||
/// 日志体系(talker_flutter + talker_dio_logger):
|
||||
/// 1. 全局唯一实例:core/logger/app_talker.dart 的 talker(全项目 import 即用);
|
||||
/// 2. Dio 网络日志:core/network/dio_client.dart 已接入 TalkerDioLogger;
|
||||
/// 3. App 内查看:本文件在 MaterialApp.builder 外层注入 TalkerWrapper + DebugTalkerBubble,
|
||||
/// 非 Release 模式会出现右下角悬浮小圆球,点开进入 TalkerScreen 查看全部日志;
|
||||
/// 4. Release 自动关闭:kReleaseMode 时不注入 TalkerWrapper/悬浮球,线上不可见。
|
||||
///
|
||||
/// 业务页面直接使用(不需要 new Talker):
|
||||
/// import 'package:new_learn/core/logger/app_talker.dart';
|
||||
/// talker.debug('调试信息');
|
||||
/// talker.warning('警告');
|
||||
/// talker.error('错误');
|
||||
///
|
||||
/// 一、ScreenUtilInit 屏幕适配(flutter_screenutil 5.9.0):
|
||||
/// designSize = 设计稿尺寸(这里 375x812 = iPhone 标准逻辑尺寸,
|
||||
/// 如果你的设计稿是 750 宽(uni-app rpx),就写 Size(750, 1624))。
|
||||
/// 适配原理:按"实际屏幕 / 设计稿"的比例统一缩放,保证不同手机上
|
||||
/// UI 比例一致,不会大屏显小、小屏溢出。
|
||||
///
|
||||
/// 二、页面里怎么用(核心三件套):
|
||||
/// .w 宽度/横向间距 Container(width: 100.w)
|
||||
/// .h 高度/纵向间距 Container(height: 50.h)
|
||||
/// .sp 字号 TextStyle(fontSize: 16.sp)
|
||||
/// .r 圆角 BorderRadius.circular(12.r)
|
||||
///
|
||||
/// 三、高频坑:
|
||||
/// 1. 必须在 ScreenUtilInit 的 builder 内部才能用 .w/.h/.sp/.r,
|
||||
/// 所以初始化要包在最外层(本文件就是最外层);
|
||||
/// 2. 改 main.dart 或这里,必须 Shift+R 热重启,热重载不生效;
|
||||
/// 3. 不要对图片宽高做 .w 缩放后还叠加 transform,会双倍缩放。
|
||||
/// ============================================================
|
||||
class TabApp extends StatelessWidget {
|
||||
const TabApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
// 方案 B:平板(宽≥600dp)自动用大设计稿,缩放比例回到 ~1,
|
||||
// 否则 Pad 上 .sp 会随宽度放大 2 倍+,字体爆炸。
|
||||
designSize: AppAdapt.designSizeOf(context),
|
||||
minTextAdapt: true, // 字号最小自适应,防小屏文字过小
|
||||
splitScreenMode: true, // 支持分屏/平板
|
||||
// 兜底:字体放大封顶 1.2 倍(防横屏/极端宽屏)
|
||||
fontSizeResolver: AppAdapt.fontSizeResolver,
|
||||
builder: (context, child) {
|
||||
// 品牌 × 环境 聚合:App 名 + 主题色板随品牌走(见 core/config/app_config.dart)
|
||||
final brandTheme = AppConfig.theme;
|
||||
return MaterialApp.router(
|
||||
title: AppConfig.appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
// 品牌 seed 色生成整套 Material 色板(换品牌 = 全局换色)
|
||||
colorSchemeSeed: brandTheme.seedColor,
|
||||
scaffoldBackgroundColor: const Color(0xFFF5F7FA),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Color(0xFF333333),
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
),
|
||||
// 方案 A:启动层隐私 Gate。
|
||||
// 这样冷启动时不会先看到登录页/主页闪一下,再被路由重定向到隐私页,
|
||||
// 而是直接由全屏拦截页先接管。
|
||||
builder: (context, child) {
|
||||
final content =
|
||||
PrivacyStartupGate(child: child ?? const SizedBox.shrink());
|
||||
final bootstrapped = _LocalNotificationBootstrapper(child: content);
|
||||
if (kReleaseMode) return bootstrapped;
|
||||
return TalkerWrapper(
|
||||
talker: talker,
|
||||
child: DebugTalkerBubble(
|
||||
child: _NetworkStatusGlobalLogger(child: bootstrapped),
|
||||
),
|
||||
);
|
||||
},
|
||||
// 全局路由表:登录守卫 + 4 tab ShellRoute(见 app_router.dart)
|
||||
routerConfig: appRouter,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalNotificationBootstrapper extends StatefulWidget {
|
||||
const _LocalNotificationBootstrapper({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_LocalNotificationBootstrapper> createState() =>
|
||||
_LocalNotificationBootstrapperState();
|
||||
}
|
||||
|
||||
class _LocalNotificationBootstrapperState
|
||||
extends State<_LocalNotificationBootstrapper> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
privacyAcceptedNotifier.addListener(_onPrivacyChanged);
|
||||
_onPrivacyChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
privacyAcceptedNotifier.removeListener(_onPrivacyChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPrivacyChanged() {
|
||||
if (!privacyAcceptedNotifier.value) return;
|
||||
unawaited(LocalNotificationService.instance.ensureInitialized());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
class _NetworkStatusGlobalLogger extends ConsumerStatefulWidget {
|
||||
const _NetworkStatusGlobalLogger({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<_NetworkStatusGlobalLogger> createState() =>
|
||||
_NetworkStatusGlobalLoggerState();
|
||||
}
|
||||
|
||||
class _NetworkStatusGlobalLoggerState
|
||||
extends ConsumerState<_NetworkStatusGlobalLogger> {
|
||||
late final ProviderSubscription<NetworkStatus> _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final current = ref.read(networkStatusProvider);
|
||||
talker.debug(
|
||||
'网络状态(初始):'
|
||||
'types=[${current.networkTypes.map((e) => e.name).join(',')}] '
|
||||
'hasInternet=${current.hasInternet} '
|
||||
'hasBackend=${current.hasBackend}',
|
||||
);
|
||||
|
||||
_sub = ref.listenManual(networkStatusProvider, (prev, next) {
|
||||
talker.debug(
|
||||
'网络状态变化:'
|
||||
'types=[${next.networkTypes.map((e) => e.name).join(',')}] '
|
||||
'hasInternet=${next.hasInternet} '
|
||||
'hasBackend=${next.hasBackend}',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
Reference in New Issue
Block a user