Flutter圈子Android进阶之路Android开发

Flutter之旅 -- 多语言(本地化/国际化)

2025-08-13  本文已影响0人  开心wonderful

本篇文章主要介绍以下几个内容:

  • Flutter 国际化原理
  • 与 Android/iOS 原生国际化的对比
  • 使用 Riverpod 实现语言切换的代码示例
Flutter之旅

本文实现效果:


Flutter 国际化

1. Flutter 国际化底层原理

1.1 核心组件与数据流

Flutter 国际化的核心由三部分构成,并通过一条明确的数据流完成 “选择语言 → 加载资源 → 分发到树 → 精准重建”。

  1. LocalizationsLocalizationsDelegate

    • Localizations 是一个 InheritedWidget,负责在 Widget 树中提供当前 Locale 的本地化资源实例。
    • LocalizationsDelegate<T> 负责“按需加载 + 构建”类型为 T 的资源(本项目为 S),并交由 Localizations 管理与缓存。
  2. Locale 的解析与选取

    • 入口在 MaterialAppsupportedLocales 定义支持列表;locale 可显式指定当前语言(不指定则跟随系统);
    • 可选回调 localeListResolutionCallback/localeResolutionCallback 用于自定义匹配策略(如兼容 zhzh_CN)。
  3. 资源访问类 S

    • 由 Intl 工具链根据 .arb 生成,用于类型安全地访问文案:S.of(context)S.current
    • 委托 S.delegateAppLocalizationDelegate)在 load(Locale) 时构建并更新当前实例。

Localizations 树装配流程(逐步)

示例(来自生成代码的关键路径):

// lib/generated/l10n.dart(节选逻辑)
static Future<S> load(Locale locale) {
  final localeName = Intl.canonicalizedLocale(/* ... */);
  return initializeMessages(localeName).then((_) {
    Intl.defaultLocale = localeName;
    final instance = S();
    S._current = instance; // 更新全局当前实例
    return instance;
  });
}

依赖追踪与重建触发

Delegate 缓存与 shouldReload

Locale 解析顺序(默认)

本质上,Flutter 通过 Localizations 在 Widget 树中下发“当前语言资源”的引用;当 locale 变更时,由 Localizations 精准通知依赖节点重建,实现动态更新多语言文案。

1.2 与主题系统的类比

2. 跨平台国际化方案对比

2.1 Android 原生(资源限定符)

<!-- res/values/strings.xml -->
<resources>
    <string name="ok">OK</string>
    <string name="cancel">Cancel</string>
    <string name="device_list_count">%1$d devices</string>
}</resources>

<!-- res/values-zh/strings.xml -->
<resources>
    <string name="ok">确定</string>
    <string name="cancel">取消</string>
    <string name="device_list_count">%1$d个设备</string>
</resources>

2.2 iOS 原生(Localizable.strings)

// Localizable.strings (English)
"ok" = "OK";
"cancel" = "Cancel";

// Localizable.strings (Chinese - Simplified)
"ok" = "确定";
"cancel" = "取消";

// 代码
let title = NSLocalizedString("ok", comment: "OK button")

2.3 Flutter 对比(跨平台一致)

维度 Android 原生 iOS 原生 Flutter
资源组织 res/values-xx Localizable.strings .arb + 代码生成(S 类)
切换机制 配置变更/Activity 重建 系统语言变更触发 Localizations 局部重建
一致性 平台差异明显 平台差异明显 跨平台一致
自定义能力 依赖系统 依赖系统 完全代码可控

3. 实现步骤与代码示例

用 Flutter Intl 实现国际化:https://juejin.cn/post/7410645914585546779

3.1 安装 Flutter Intl 插件

在 IDE 中安装 Flutter Intl 插件,以 Android studio 为例:

Android Studio --->Settings --->Plugins ---> Marketplace ---> 搜索框输入Intl ---> Install

3.2 初始化 Flutter Intl

Tools → Flutter Intl → Initialize for the project;

3.3 添加国际化语言

Tools ---> Flutter Intl ---> Add Locale,新增如 `en`、`zh_CN` 等国际化语言
{
  "drawer_item_theme": "暗黑模式",
  "drawer_item_language": "语言设置",
  "device_list_count": "{count}个设备"
}

占位符在 .arb 中需声明参数类型:

{
  "device_list_count": "{count}个设备",
  "@device_list_count": {
    "placeholders": { "count": { "type": "int" } }
  },
  // 复合参数
  "storage_usage": "已用{usedGB}GB / 共{totalGB}GB"
}

3.4. 在项目中配置

MaterialApp 中接入:

// lib/main.dart(节选)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'generated/l10n.dart';

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 如果使用 Riverpod 管理语言,直接 watch 对应的 Locale(详见下一节)
    final locale = ref.watch(languageProvider); // Locale?,null 表示跟随系统

    return MaterialApp(
      localizationsDelegates: const [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      locale: locale,
      // ... 其他配置
    );
  }
}

3.5 在代码中使用

import 'generated/l10n.dart';

// 推荐:会随着语言变化而自动更新
Text(S.of(context).drawer_item_language);

// 特殊场景(不随 context 重建,如后台任务):
final text = S.current.ok; // 注意:不会触发 UI 自动刷新

3.6 注意事项

命名规范(建议)

数字/日期/复数与参数
借助 intl 格式化与 plural/gender 支持:

// .arb
{
  "files_count": "{count, plural, =0{没有文件} =1{1个文件} other{{count}个文件}}",
  "@files_count": { "placeholders": { "count": { "type": "int" } } }
}

// Dart
Text(S.of(context).files_count(count));

修改调用方式前缀 S
S.of(context).xxx 中的 S 也可以定义成自己喜欢的名称,可以在 pubspec.yaml 中进行配置:

flutter_intl:
  enabled: true
  class_name: S # 可选。 Default: S
  main_locale: en # 可选。可以改成 zh_CN 中文简体,这关系到默认生成的arb文件的名字。Default: en

4. 使用 Riverpod 实现语言切换

4.1 实现持久化存储

如使用 SharedPreferences 保存用户的语言选择:

// lib/common/utils/share_preferences_utils.dart
class SharePreferenceUtils {
  /// 保存语言
  static Future<bool> saveLanguage(String languageCode) async {
    return await saveString(SharePreferenceKey.language, languageCode);
  }

  /// 获取语言
  static Future<String> getLanguage() async {
    return await getString(SharePreferenceKey.language, "");
  }
}

class SharePreferenceKey {
  static const String language = "APP_LANGUAGE";  // 语言
}

// 语言常量
class LanguageConstant {
  static const Locale en = Locale('en', 'US');
  static const Locale zh = Locale('zh', 'CN');
}

4.2 创建主题状态管理器

使用 Riverpod 的 StateNotifier 创建主题状态管理器:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../generated/l10n.dart';
import '../channel/platform_service.dart';
import '../constant.dart';
import '../utils/share_preferences_utils.dart';

class LanguageNotifier extends StateNotifier<Locale?> {
  LanguageNotifier() : super(null) {
    _initLanguage();
  }

  /// 初始化语言设置
  Future<void> _initLanguage() async {
    final savedLanguage = await SharePreferenceUtils.getLanguage();
    if (savedLanguage.isNotEmpty) {
      state = _localeFromString(savedLanguage);
    } else {
      final systemLocale = await _getSystemLocale();
      state = _getSupportedLocale(systemLocale);
    }
  }

  /// 获取系统语言
  Future<Locale> _getSystemLocale() async {
    final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
    return systemLocale;
  }

  /// 转换字符串为Locale
  Locale _localeFromString(String localeStr) {
    final parts = localeStr.split('_');
    return Locale(parts[0], parts.length > 1 ? parts[1] : '');
  }

  /// 获取支持的语言
  Locale _getSupportedLocale(Locale systemLocale) {
    final supportedLocales = S.delegate.supportedLocales;
    final isSupported = supportedLocales.any((loc) =>
        loc.languageCode == systemLocale.languageCode &&
        (loc.countryCode == null ||
            loc.countryCode == systemLocale.countryCode));

    return isSupported ? systemLocale : LanguageConstant.en; // 默认英语
  }

  /// 切换应用语言
  Future<void> setLanguage(Locale locale) async {
    state = locale;
    await SharePreferenceUtils.saveLanguage(
        '${locale.languageCode}_${locale.countryCode}');

    // 同步语言设置到小组件
    await PlatformService.syncLanguage(locale.languageCode);
  }
}

/// 语言状态提供者
final languageProvider = StateNotifierProvider<LanguageNotifier, Locale?>(
  (ref) => LanguageNotifier(),
);

/// 系统语言提供者
final systemLanguageProvider = Provider<Locale>((ref) {
  return WidgetsBinding.instance.platformDispatcher.locale;
});

/// 判断当前是否为中文的提供者
final isChineseProvider = Provider<bool>((ref) {
  final currentLocale = ref.watch(languageProvider);
  return currentLocale?.languageCode == LanguageConstant.zh.languageCode;
});

/// 字体提供者
final fontFamilyProvider = Provider<String>((ref) {
  final currentLocale = ref.watch(languageProvider);
  return currentLocale == LanguageConstant.zh
      ? FontConstant.notoSansSC
      : FontConstant.inter;
});

4.3 在 MaterialApp 中应用语言

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'language/language_provider.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final language = ref.watch(languageProvider);
    
    return MaterialApp(
      title: 'Flutter语言切换示例',
      locale: language,
      localizationsDelegates: [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      home: MyHomePage(),
    );
  }
}

使用示例:

// 切换到中文(简体)
ref.read(languageProvider.notifier).setLanguage(const Locale('zh', 'CN'));

// 切换到英文
ref.read(languageProvider.notifier).setLanguage(const Locale('en'));

4.4 为什么要用 Riverpod 管理语言?

5. 其他事项

  1. S.of(context) 优先于 S.current

    • S.of(context):对 Localizations 建立依赖,locale 变化时会被精准通知并重建;
    • S.current:读取的是全局当前实例,不会“自己触发”重建。若没有其他状态变化导致该 Widget 重建,使用 S.current 的文本不会自动更新。
  2. locale 交由顶层 MaterialApp 管控

    • 避免在子树多处“覆盖”Localizations.override,防止维护复杂度与重建范围不可控。
  3. 避免在频繁重建的 Widget 中做昂贵的格式化

    • 大量 DateFormat/NumberFormat 可复用或上移到较少重建的层级;或做轻量缓存。
  1. FAQ:为什么上面代码里用 S.current 也能更新?
上一篇 下一篇

猜你喜欢

热点阅读