一、前言:
所谓国际化,就是中英文切换,甚至更多的语种切换。我在网上搜了一部分,大都是两个语种的切换,而且实时切换还得额外插件。所以我直接在登录页增加一个按钮,点击之后切换。效果如下:
二、插件安装
- 首先编辑器的插件:打开Plugins,安装Flutter.Intl(需要翻墙,主要是国际化自动配置使用)
- 代码插件:打开pubspec.yaml,配置flutter_localization、provider、get、flutter_intl:
name: my_test_app description: "A new Flutter project." publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ^3.5.1 dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter provider: ^6.1.2 # 全局的状态管理,用于切换本地化语言 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter get: ^4.6.5 flutter_svg: ^2.0.10 shared_preferences: ^2.0.15 intl: ^0.19.0 intl_utils: ^2.4.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^4.0.0 flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - assets/images/ # 这将包括该目录下的所有文件 flutter_intl: enabled: true class_name: S main_locale: en output_dir: lib/language/generated # 模板代码生成路径 arb_dir: lib/language/l10n # .arb 文件存放的路径
配置完成之后执行 flutter pub get
三、增加arb文件
pubspec.yaml中配置的flutter_intl中有备注arb文件存放路径,直接在 lib 下新建 language 文件夹,然后再新建 l10n 文件夹,在里面建两个文件 intl_en.arb 与 intl_zh.arb
// intl_en.arb
{
"home": "Home",
"settingLanguage": "Set Language",
"languageName_en": "English",
"languageName_zh": "Simplified Chinese",
"languageName_unknown": "Unknown",
"login_pageName": "Login Page",
"login_title": "Test App Demo",
"login_userName": "Username",
"login_userName_empty": "Username can't be empty!",
"login_password": "Password",
"login_password_empty": "Password can't be empty!",
"login_btn": "Login"
}
// intl_zh.arb
{
"home": "首页",
"settingLanguage": "语言设置",
"languageName_en": "英语",
"languageName_zh": "简体中文",
"languageName_unknown": "未知语种",
"login_pageName": "登录页",
"login_title": "App测试",
"login_userName": "用户名",
"login_userName_empty": "用户名不能为空!",
"login_password": "密码",
"login_password_empty": "密码不能为空!",
"login_btn": "登录"
}
四、配置模版文件(generated)
直接使用编辑器的插件实现
执行完之后便生成了如下圈出的文件
五、修改项目入口的Widget(main.dart)
直接上全部代码:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'language/generated/l10n.dart';
import 'page/login_page.dart';
void main() {
runApp(
// 入口文件改造
ChangeNotifierProvider(
create: (context) => LocaleProvider(),
child: const MyApp(),
),
);
}
class LocaleProvider with ChangeNotifier {
Locale _locale = const Locale('zh', ''); // 设置默认语言
Locale get locale => _locale;
void setLocale(Locale locale) {
if (locale.languageCode.isNotEmpty && locale != _locale) {
_locale = locale;
notifyListeners();
}
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return Consumer<LocaleProvider>(
builder: (context, localeProvider, child) {
// 原本的入口
return MaterialApp(
title: 'Test Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 所支持的语言
supportedLocales: S.delegate.supportedLocales,
locale: localeProvider.locale,
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// 设备系统或者浏览器语言环境发生改变的时候的回调
localeResolutionCallback:
(Locale? locale, Iterable<Locale> supportedLocales) {
Locale currentLocale =
Locale.fromSubtags(languageCode: locale?.languageCode ?? "zh");
return supportedLocales.contains(currentLocale)
? currentLocale
: const Locale.fromSubtags(languageCode: "zh");
},
home: const LoginPage(),
);
}
);
}
}
六、登录页(login_page.dart)手动切换功能
有之前两章的铺垫,这边我也不过多解释,照样全部代码
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../language/generated/l10n.dart';
import '../main.dart';
import 'home_page.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final FocusNode _usernameFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
String? _usernameError;
String? _passwordError;
bool _isObscure = true;
final _borderRadius = 10.0;
@override
void initState() {
super.initState();
_usernameFocusNode.addListener(() {
if (!_usernameFocusNode.hasFocus) {
_validateUsername();
}
});
_passwordFocusNode.addListener(() {
if (!_passwordFocusNode.hasFocus) {
_validatePassword();
}
});
}
// 用户验证
void _validateUsername() {
setState(() {
if (_usernameController.text.isEmpty) {
_usernameError = S.of(context).login_userName_empty;
} else {
_usernameError = null;
}
});
}
// 密码验证
void _validatePassword() {
setState(() {
if (_passwordController.text.isEmpty) {
_passwordError = S.of(context).login_password_empty;
} else {
_passwordError = null;
}
});
}
// 登录点击事件
void _login() async {
if (_formKey.currentState?.validate() ?? false) {
_validateUsername();
_validatePassword();
final userName = _usernameController.text;
final password = _passwordController.text;
if (_usernameError == null && _passwordError == null) {
// 底部消息弹框
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$userName 已登录')),
);
// 登录数据存储
final prefs = await SharedPreferences.getInstance();
prefs.setString('userName', userName);
// 跳转到下一个页面
// Navigator.push(
// context,
// MaterialPageRoute(builder: (context) => HomePage(userName: username)),
// );
// 跳转后替换整个导航堆栈,无返回按钮
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => HomePage(userName: userName),
),
(route) => false, // 移除前置路由
);
}
}
}
// 选择本地语言
void _showLanguageDialog(BuildContext context) {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(_borderRadius)),
),
builder: (BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: S.delegate.supportedLocales.map((locale) {
return ListTile(
title: Text(_getLanguageName(context, locale)),
onTap: () {
Provider.of<LocaleProvider>(context, listen: false)
.setLocale(locale);
Navigator.of(context).pop();
},
);
}).toList(),
),
);
},
);
}
// 弹框选择展示,这个可以继续优化,不然每次增加语种这里也要增加相对应代码!
String _getLanguageName(BuildContext context, Locale locale) {
switch (locale.languageCode) {
case 'en':
return S.of(context).languageName_en; // 注意这种写法哦,arb文件
case 'zh':
return S.of(context).languageName_zh;
default:
return S.of(context).languageName_unknown;
}
}
// hex颜色
Color _hexToColor(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 7 || hexString.length == 9) buffer.write('FF');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
S.of(context).login_pageName, // 注意这种写法,arb文件
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
)
),
backgroundColor: _hexToColor('#037cfd'), // 标题栏颜色
foregroundColor: Colors.white,
centerTitle: true,
),
backgroundColor: _hexToColor('#037cfd'), // 修改外部背景颜色
body: Padding(
padding: const EdgeInsets.all(40.0), // 设置内边距
child: Column(
// mainAxisAlignment: MainAxisAlignment.center, // 垂直居中
// crossAxisAlignment: CrossAxisAlignment.center, // 水平居中
children: [
Text(
S.of(context).login_title,
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16.0),
// width: screenWidth * 0.75, // 宽度设置0.75
decoration: BoxDecoration(
color: Colors.white, // 内部颜色
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Form(
key: _formKey,
child: Column(
children: [
SvgPicture.asset(
'assets/images/login.svg'
),
const SizedBox(height: 16),
TextFormField(
controller: _usernameController,
focusNode: _usernameFocusNode, // 失焦事件
style: const TextStyle(fontSize: 12, color: Colors.black87), //文字大小、颜色
//输入组件
decoration: InputDecoration(
labelText: S.of(context).login_userName,
// hintText: 'Username',
errorText: _usernameError,
prefixIcon: Icon(Icons.person, color: _hexToColor('#7ba8c0')),
fillColor: Colors.grey[80], //背景颜色,
filled: true, //必须设置为true,fillColor才有效
isCollapsed: true, //相当于高度包裹的意思,必须设置为true
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15), //内容内边距
enabledBorder: OutlineInputBorder(
//设置输入框可编辑时的边框样式
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide:
const BorderSide(color: Colors.white, width: 0) //输入框白色隐藏
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: BorderSide(color: _hexToColor('#7ba8c0'), width: 2.0),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocusNode, // 失焦事件
obscureText: _isObscure,
decoration: InputDecoration(
labelText: S.of(context).login_password,
errorText: _passwordError,
prefixIcon: Icon(Icons.lock, color: _hexToColor('#7ba8c0'),),
fillColor: Colors.grey[80], //背景颜色,必须结合filled: true,才有效
filled: true, //必须设置为true,fillColor才有效
isCollapsed: true, //相当于高度包裹的意思,必须设置为true
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15), //内容内边距,影响高度
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: const BorderSide(color: Colors.white, width: 0) //输入框白色隐藏
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: const BorderSide(color: Colors.white, width: 0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: BorderSide(color: _hexToColor('#7ba8c0'), width: 2.0),
),
suffixIcon: IconButton(
icon: Icon(
!_isObscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: _hexToColor('#037cfd'), //修改按钮背景色
foregroundColor: Colors.white, //文本的颜色
minimumSize: const Size(double.infinity, 50), // 修改按钮大小
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_borderRadius), // 修改按钮圆角
),
),
child: Text(S.of(context).login_btn),
),
],
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showLanguageDialog(context),
tooltip: S.of(context).settingLanguage,
child: const Icon(Icons.language),
),
);
}
}
完结撒盐!