Flutter
发布到如今有一段时间了,目前为止,不少公司都尚未接受Flutter进行开发,这缘由是多方面的,毕竟在没有足够的“探索者”前,贸然使用遇到麻烦时,解决起来太过麻烦。前端
如今市面上感受作的比较好的一款产品,除了官方demo:Gallery,再就是第一款Flutter开发的Github客户端,详细信息请移步:gitme。android
参照着Flutter中文网
以及,大牛正在编写中的书籍:Flutter实战,大体梳理了一下Flutter开发流程,不过因为架构的不一样,以前ios
和Android
最主要的仍是操做dom
,而Flutter则与RN相似的采用的响应式操做,这就致使在迁移开发平台时,会不自主想将之前的经验进行搬移时遇到麻烦。ios
在android、ios以及web端
,开发框架流程等都已经很纯熟和完善了,而Flutter就目前的生态来讲,仍是有些“薄弱”,具体的开发仍是须要进行系统的学习,这里只是简单给出一种方式来完成前端很常见的国际化与换肤功能
。git
完整代码移步github-demo:flutter_skin_locale
程序员
效果图以下:github
由于Flutter
出身同为Android
的google
,因此这里暂时以Android
实现方式进行比对;web
安卓中国际化操做
比较简单,由于系统已经提供了这方面的解决方案,咱们只要将对应的资源文件放入不一样的res资源目录
下面就能够了,系统会根据当前的locale
值查找对应的 res 资源目录,而后根据资源id
查找资源名称
,最后根据资源名称查找到具体文件
。这个流程对于应用层开发人员来讲是无感知的,全部要作的只是配置...而后打包。redux
安卓中原生是不支持换肤的,其实在安卓诞生时候也没有这方面的需求,后来大厂商为了某些销售活动,或者为了更好适应夜间模式及个性化,才有了这方面需求。就目前来看使用最多的是换肤框架,好比不少star的Android-skin-support;即使有框架的支持,在涉及大量自定义控件的状况下,仍须要作不少的适配工做。数组
颇有意思的是,在 Android 中只须要依照配置就能够完成的国际化功能,在Flutter中很难行得通,由于Flutter中没有了Android的 res 系统
,全部须要操做的图片,图标颜色值,都只能经过文件或者代码硬性插入,这一点很不舒服,虽然官方给了flutter_localizations
库,但使用起来就知道,真的至关麻烦,咱们不妨抛开Flutter部分平台
的机制,看本身搭轮子是否能够实现换肤功能;bash
在真正开始以前,仍是得先了解一点Flutter中入口的逻辑,不然确定找不到头绪:
return MaterialApp(
title: 'Flutter Title',
routes: ...,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 组件库所使用的字符串
GlobalWidgetsLocalizations.delegate, // 在当前的语言中,文字默认的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')),
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
复制代码
Flutter 入口程序通常都是这个样子,针对涉及国际化的每一个字段,简单的说明一下:
字段 | 含义 |
---|---|
localizationsDelegates | 国际化代理类,这个只需知道是国际化字符串资源类的集合便可,通常咱们会将自定义的国际化字符串对象在这里声明;具体功能能够查看[LocalizationsDelegate]类 |
supportedLocales | 系统支持的语言环境,好比中文简体,中文繁体等等,注意的是,locale要同时赋值语言和国家,以英文为例:Locale("en", "") |
localeResolutionCallback | 若是当前手机设置的语言环境或者说宿主app设置的语言环境不在 supportedLocales 中,那么须要默认一个locale 值,不默认也能够,系统会默认取支持列表supportedLocales 中第一个值 |
locale | 本身设定一个当前语言locale,若是不设置或者设置为null,就取宿主app当前的语言环境(等价于设置语言环境为:"跟随系统") |
咱们大体只须要知道上面所说的部分便可,假设如今须要实现一个语言切换的功能,须要包含如下几种类型:
对于跟随系统
来讲,只要将MaterialApp
中locale
字段置为null
,其余四种状况分别对应不一样的 locale
便可;至于MaterialApp
中另外几个字段,也只须要根据支持列表一一填入;最重要的部分是国际化资源代理类:S
的建立与更新。
虽然说Flutter不支持,但只要程序员够懒,总会有适合的工具出现的,这里给出一个最简单的用于国际化的插件:Flutter i18n
有了这个插件开发起来会方便的多,在安装此插件后,项目中会自动生成${project}/res/values/*.arb
以及${project}/lib/generated/i18n.dart
文件;*.arb
表明多个文件(工具只会帮忙生成strings_en.arb
),相似于安卓中多个目录下针对字符串的配置,这个能够自行添加或者删除*.arb
文件,多添加一个正确的配置文件,就至关于多一种语言支持,添加*.arb
文件后编辑器会本身处理剩下的逻辑,或者点击工具栏顶部的这个图标:
Flutter i18n已经集成了快捷键来提取字符串,这个和android中的这个功能使用方法相同:
继续以前工具自动生成的两部分文件说,*.arb
文件相似下面的结构:
注意:strings_en.arb
是默认的arb文件,其余arb文件须要根据默认的arb文件生成对应的字符串。
i18n.dart
是国际化的核心代码,大体结构以下:
能够看到,里面包含了不一样国家地区(本身配置支持的国际语言,和*.arb
相对应),一样的,已经替咱们生成了MaterialApp
中所需的几乎全部配置(具体使用配置可参照demo)。
最后使用的话也很简单,在代码中须要字符串的地方替换为这种:
S.of(context).label_soft_setting
复制代码
S
类是i18n.dart
自动帮助咱们生成的,相似于一个代理类,根据不一样的语言环境代理$zh_HK、$zh_TW、$en、$zh_CN
这些具体实现类,达到国际化的目的
通过上面的总结,咱们来看Flutter i18n
到底完成了什么:
strings_en.arb
默认字符串模版*.arb
文件中*.arb
自动生成i18n.dart
文件,包含支持语言列表,国际化代理类等i18n.dart
提供在运行时提取不一样国家语言字符串的功能方法总体来看,该工具没有依赖任何库,也就是说相对于官方提供的方法,Flutter i18n
不须要对pubspec.yaml
作出修改。
基本上,若是项目中没有太过复杂的要求,只提供这种字符串国际化足够,但有些状况下, 针对不一样的语言环境,图片也须要动态进行更替,关于更换图片的逻辑,放到文章下面介绍换肤功能的时候再考虑。
对比其余平台换肤,我以为Flutter换肤最为简单(这里换肤是指应用内换肤,不支持从互联网下载皮肤包换肤),由于系统默认提供了Theme
,不得不说,在Flutter中到处可见Android开发的影子,Theme表示主题
,表示应用总体的风格,theme中可定义各类类型用途的背景颜色,文字颜色,高亮;甚至于能够修改文字的字体大小,字体库,所以经过theme还能够实现应用内更改字体大小的功能
,不过这篇文章先不考虑这个问题,在换肤后功能完成后,要实现应用内换肤是很容易的事情。
对于theme,能够截取一部分查看大体状况:
如上所见,对于分割线,主题色,按钮,高亮等,能够分别定义不一样的颜色值,而后咱们能够在MaterialApp
中设置不一样的theme(跟国际化配置在相同的地方):
return MaterialApp(
title: 'Flutter Mudule',
theme: themes[gCurrentThemeIndex],
routes: ...,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 组件库所使用的字符串
GlobalWidgetsLocalizations.delegate, // 在当前的语言中,文字默认的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')), // 不存对应locale时,默认取值英文
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
复制代码
其中theme字段即为当前应用或者说界面采用的主题,若是咱们能够对其进行更改,就至关于对app进行换肤(这里的换肤只是指颜色换肤,真正换肤可能还须要涉及图片更换,状况与国际化相似,下面会提到这种解决方式)。
想要在代码中使用某个颜色时(有些颜色值为自定义,所以系统没法动态感知须要使用哪一个样式),可使用以下方式:
Theme.of(context).textTheme.display1.color
复制代码
到此为止,简单的颜色换肤和国际化处理思路已经清晰,遵循上面的逻辑,基本能够完成大部分的需求,不过就如上面所提,若是涉及图片部分,Flutter框架就没法直接处理了,事实上,flutter 若是须要获取一个图片,是须要知道具体路径的,相似这样:
Image.asset(
"assets/images/icon_test.png",
width: 45,
height: 45,
),
复制代码
相比于android读取图片,Flutter这种方式麻烦的多,而且没有任何的智能提示;但也由于是这种调用方式,给了咱们很大的自定义图片读取的空间,好比这样:
定义同级两个文件夹,里面分别放置不一样主题样式,而后根据当前选中的主题,取图片时,选择不一样的路径,相似这样:
/// 获取图片路径(中转,用于多环境等状况) [PlatformAssetBundle] 类查看资源获取逻辑
///
/// [useDefault] 是否使用默认的主题资源(当多theme使用相同image时,会有这种状况)
/// [picFormat] 图片格式,默认为png,
String dispatcherPictureByName(String picName, {bool useDefault = false, String picFormat = "png"}) {
RegExp filter = RegExp("^[^.]+\.(png)|(jpg)|(jpeg)|(gif)|(webp)|(bmp)|(wbmp)\$", caseSensitive: false, multiLine: false);
// 添加后缀
picName = filter.hasMatch(picName) ? picName : "$picName.$picFormat";
// 取系统主题颜色
String pathName = "assets/images-$gCurrentThemeIndex/$picName";
// 返回须要的路径
return useDefault ? "assets/images-1/$picName" : pathName;
}
复制代码
其中$gCurrentThemeIndex
表示当前主题下标序号,而后在代码中这样使用:
Image.asset(
dispatcherPictureByName("icon_test",useDefault: true),
width: 45,
height: 45,
),
复制代码
如今应该明白了,若是是想让国际化时也取值不一样的图片,只要相似这样定义不一样的文件包,而后将图片放入便可,这个取值规则是自定义的,根据实际状况能够作出修改。
上面给出了要实现国际化与换肤基本思路,但其中还有不少细节须要思考,好比:
S.of(context).***
,若是须要在没有context
的地方获取字符串值,该如何处理?module
的形式混入原生应用,该如何保证原生与Flutter层保持一致?固然,最后一条能够能够不用管,那个属于混合开发的范畴,真正使用的时候再考虑跨平台混入bridge;对于记录保存,使用第三方的库便可:shared_preferences
针对上面的问题,分别进行讨论:
主题包的定义可使用最简单的方式,新建一个文件app_theme_config.dart
,将全部预约义的主题放在一块儿:
List<ThemeData> themes = [
ThemeData(...),
ThemeData(...),
ThemeData(...),
ThemeData(...),
]
复制代码
里面每个ThemeData
表示一套风格,或者说是一个皮肤;
再新建一个文件app_status_holder.dart
,保存当前选择的皮肤的下标,取值范围为:[0,length-1],int类型
,这样的话,每次须要更换皮肤时,只须要修改下标的值,而后从themes数组
中取出对应的主题,赋值给MaterialApp
中theme
字段便可。
app_status_holder.dart
文件:
/// 当前系统主题(暂不考虑外部引入主题状况)
int gCurrentThemeIndex = 0;
复制代码
跟上面的主题处理方式相似,咱们也新建一个文件,保存当前app可能使用的字符串类app_locale_config.dart
:
/// 某些地方没法 获取context ,但又须要获取国际化的字符串时,但系统切换可能致使文字不会改变,由于字符串没有在 state方法中初始化
List<S> ss = [
S(),
$zh_CN(),
$zh_TW(),
$zh_HK(),
$en(),
];
/// 当context 不存在时,经过SS而非S去获取字符串
S get SS {
return ss[gCurrentSupportLocale];
}
复制代码
在没有context
的状况若是想要获取到字符串,就必须知道当前语言环境究竟是什么,咱们模拟代理类S
定义一个代理方法SS
,而后在全局记录当前语言环境,间接的读取到正确的字符串值;
固然,只作这个是不够的,若是设置了语言设置为跟随系统
,在系统语言进行切换时,调用方法SS
获取到的一直会是S()
,即系统默认的英文形式,那确定是不行的,所以,必须在合适的地方调用一次这样的代码:
// 系统语言改变时,若是当前为跟随系统,则须要修改字符串读取对象
if (gCurrentSupportLocale == 0) {
print("当前系统语言为:${Localizations.localeOf(context)}");
ss[0] = S.of(context);
}
复制代码
也就是说,须要动态的修改ss数组列表
中默认的语言。
通常来讲,修改语言环境或皮肤后,除了当前界面,已打开或建立的界面也都须要进行刷新,对于安卓平台来讲,系统默认在config
变化后重启界面来达到刷新的目的;
对于Flutter来讲,刷新界面不须要重建,只须要调用setState
方法便可;方即是方便,但这涉及到事件的推送;消息队列是最简单的推送方式,或者说是事件总线EventBus,这个框架在几乎全部的平台存在。
为了方便界面刷新,咱们最好在最顶层监听事件,而后直接刷新MaterialApp
,确保全部的界面均可以触发重绘操做,监听操做能够相似这样:
// 当通知系统时,刷新一下状态(换肤/切换语言/涨跌颜色)
eventBus.on<SystemThemeSwitch>().listen((it) {
setState(() {
gCurrentThemeIndex = it.currentThemeIndex;
});
});
复制代码
在修改系统语言和皮肤切换的界面,若是修改为功,则须要触发事件:
/// 切换主题
eventBus.fire(SystemThemeSwitch(currentThemeIndex: news.index));
setState(() {});
复制代码
如上所述,结合了国际化、换肤、本地持久化、路由跳转框架以及图片更改等思路,入口程序应该像这样:
/// 程序入口
void main() => runApp(CustomApp());
/// 自定义包裹 app, 实现换肤等功能
class CustomApp extends StatefulWidget {
@override
State createState() => _CustomAppState();
}
class _CustomAppState extends State<CustomApp> {
@override
void initState() {
super.initState();
// 初始化皮肤取值等全局 所需 参数
SharedPreferences.getInstance().then((it) {
setState(() {
gCurrentThemeIndex = it.getInt(KEY_THEME_MODE) ?? 0;
gCurrentSupportLocale = it.getInt(KEY_SUPPORT_LOCALE) ?? 0;
});
});
// 当通知系统时,刷新一下状态(换肤/切换语言/涨跌颜色)
eventBus.on<SystemThemeSwitch>().listen((it) {
setState(() {
gCurrentThemeIndex = it.currentThemeIndex;
});
});
eventBus.on<SupportLocaleSwitch>().listen((it) {
setState(() {
gCurrentSupportLocale = it.currentSupportLocale;
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Mudule',
debugShowCheckedModeBanner: false,
theme: themes[gCurrentThemeIndex],
routes: gActivityRoutes,
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate, //Material 组件库所使用的字符串
GlobalWidgetsLocalizations.delegate, //在当前的语言中,文字默认的排列方向
],
supportedLocales: S.delegate.supportedLocales,
localeResolutionCallback: S.delegate.resolution(fallback: const Locale('en', '')), // 不存对应locale时,默认取值英文
locale: mapLocales[SupportLocale.values[gCurrentSupportLocale]],
);
}
}
复制代码
前面还提到,须要在程序第一个界面widget
的build方法
添加以下代码:
像上面这样配置后,主流程基本已经完成了,剩下的代码就是编写页面,变量定义等等,事实上在变量少的状况下,使用event-bus
尚可。
若是须要改变变量过多逻辑较大的状况下,能够尝试使用flutter_redux
库,项目中有提供简单的使用方式:main_redux.dart;
更多功能请提issues
完成项目请参照flutter_skin_locale