一、问题本质(关键)
用flutter 开发 web项目时,中文字体先乱码再出现,根本原因是渲染流程导致的问题。
流程是这样的:
页面渲染
↓
系统 fallback 字体(Roboto / sans-serif)
↓
NotoSansSC
↓
全局字体替换
↓
UI 重排 + "闪一下"
知道了问题来源,就知道了解决办法:预加载字体。
二、解决方案
Roboto 保持英英文字母和数字效果各平台统一。
Noto Sans SC 字体用于支持中文。
js
fonts:
- family: Roboto
fonts:
- asset: assets/fonts/Roboto-Regular.ttf
- asset: assets/fonts/Roboto-Medium.ttf
weight: 500
- family: Noto Sans SC
fonts:
- asset: assets/fonts/NotoSansSC-Regular.ttf
- asset: assets/fonts/NotoSansSC-Medium.ttf
weight: 500
方案 1: 提前 preload 字体
在 web/index.html:
js
<link rel="preload" href="../assets/fonts/NotoSansSC-Regular.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="../assets/fonts/NotoSansSC-Medium.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="../assets/fonts/Roboto-Regular.ttf" as="font" type="font/ttf" crossorigin>
<link rel="preload" href="../assets/fonts/Roboto-Medium.ttf" as="font" type="font/ttf" crossorigin>
作用:
- 提前进入下载队列
- 减少首屏等待时间
- 明显降低闪烁概率
方案 2: 提前 preload 字体
dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppFontLoader.preload();//字体预加载
runApp(const MyApp());
}
app_font_loader.dart
dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Web 端字体预加载
class AppFontLoader {
AppFontLoader._();
/// Web 端在首帧绘制前注册字体;配合 index.html 的 preload 命中浏览器缓存
static Future<void> preload() async {
if (!kIsWeb) {
return;
}
await Future.wait([
_loadFont(
family: 'Noto Sans SC',
assets: [
'assets/fonts/NotoSansSC-Regular.ttf',
'assets/fonts/NotoSansSC-Medium.ttf',
],
),
_loadFont(
family: 'Roboto',
assets: [
'assets/fonts/Roboto-Regular.ttf',
'assets/fonts/Roboto-Medium.ttf',
],
),
]);
}
static Future<void> _loadFont({
required String family,
required List<String> assets,
}) async {
final FontLoader loader = FontLoader(family);
for (final String asset in assets) {
loader.addFont(rootBundle.load(asset));
}
await loader.load();
}
}
三、使用
dart
class AppThemeService extends ChangeNotifier {
static final AppThemeService _instance = AppThemeService._();
AppThemeService._();
factory AppThemeService() => _instance;
/// 全平台使用本地 Noto Sans SC,避免 Web CDN 与系统字体名不一致
static const String appFontFamily = 'Noto Sans SC';
/// Web 端西文回退到本地 Roboto
static List<String>? get appFontFamilyFallback => AppPlatform.isWeb ? const ['Roboto'] : null;
......
ThemeData get lightTheme => ThemeData(
useMaterial3: false,
brightness: Brightness.light,
fontFamily: appFontFamily,
fontFamilyFallback: appFontFamilyFallback,
......
ThemeData get darkTheme => ThemeData(
useMaterial3: false,
brightness: Brightness.dark,
fontFamily: appFontFamily,
fontFamilyFallback: appFontFamilyFallback,
最后、总结
特殊情况相下比如:
SnackBar / Dialog / Overlay 中的字体可能因为Overlay + Web 字体异步加载 + TextTheme 未完全接管导致的链路断裂,无法使用 fontFamily 和 fontFamilyFallback。这时候需要单独处理一下,TextStyle后边调用 .copyWithFontFamily()即可。
dart
extension TextStyleFontFamilyExtension on TextStyle {
/// 设置字体家族
TextStyle copyWithFontFamily() {
return copyWith(
fontFamily: AppThemeService.appFontFamily,
fontFamilyFallback: AppThemeService.appFontFamilyFallback,
);
}
}