做为系列文章的第四篇,本篇主要介绍 Flutter 中 Redux 的使用,并结合Redux 完成实时的主题切换与多语言切换功能。git
前文:github
Flutter 做为响应式框架,经过 state
实现跨帧渲染的逻辑,不免让人与 React 和 React Native 联系起来,而其中 React 下*“广为人知”*的 Redux 状态管理,其实在 Flutter 中一样适用。redux
咱们最终将实现以下图的效果,相应代码在 GSYGithubAppFlutter 中可找到,本篇 Flutter 中所使用的 Redux 库是 flutter_redux 。bash
Redux 的概念是状态管理,那在已有 state
的基础上,为何还须要 Redux ?app
由于使用 Redux 的好处是:共享状态和单一数据。框架
试想一下,App内有多个地方使用到登录用户的数据,这时候若是某处对用户数据作了修改,各个页面的同步更新会是一件麻烦的事情。less
可是引入 Redux 后,某个页面修改了当前用户信息,全部绑定了 Redux 的控件,将由 Redux 自动同步刷新。See!这在必定程度节省了咱们的工做量,而且单一数据源在某些场景下也方便管理。同理咱们后面所说的 主题 和 多语言 切换也是如此。ide
如上图,Redux 的主要由三部分组成:Store 、Action 、 Reducer 。post
因此通常流程为:字体
一、Widget 绑定了 Store 中的 state 数据。
二、Widget 经过 Action 发布一个动做。
三、Reducer 根据 Action 更新 state。
四、更新 Store 中 state 绑定的 Widget。
根据这个流程,首先咱们要建立一个 Store 。
以下图,建立 Store 须要 reducer
,而 reducer
其实是一个带有 state
和 action
的方法,并返回新的 State 。
因此咱们须要先建立一个 State 对象 GSYState
类,用于储存须要共享的数据。好比下方代码的: 用户信息、主题、语言环境 等。
接着咱们须要定义 Reducer 方法 appReducer
:将 GSYState
内的每个参数,和对应的 action
绑定起来,返回完整的 GSYState
。这样咱们就肯定了 State 和 Reducer 用于建立 Store。
///全局Redux store 的对象,保存State数据
class GSYState {
///用户信息
User userInfo;
///主题
ThemeData themeData;
///语言
Locale locale;
///构造方法
GSYState({this.userInfo, this.themeData, this.locale});
}
///建立 Reducer
///源码中 Reducer 是一个方法 typedef State Reducer<State>(State state, dynamic action);
///咱们自定义了 appReducer 用于建立 store
GSYState appReducer(GSYState state, action) {
return GSYState(
///经过自定义 UserReducer 将 GSYState 内的 userInfo 和 action 关联在一块儿
userInfo: UserReducer(state.userInfo, action),
///经过自定义 ThemeDataReducer 将 GSYState 内的 themeData 和 action 关联在一块儿
themeData: ThemeDataReducer(state.themeData, action),
///经过自定义 LocaleReducer 将 GSYState 内的 locale 和 action 关联在一块儿
locale: LocaleReducer(state.locale, action),
);
}
复制代码
如上代码,GSYState 的每个参数,是经过独立的自定义 Reducer 返回的。好比 themeData
是经过 ThemeDataReducer
方法产生的,ThemeDataReducer
实际上是将 ThemeData
和一系列 Theme 相关的 Action 绑定起来,用于和其余参数分开。这样就能够独立的维护和管理 GSYState 中的每个参数。
继续上面流程,以下代码所示,经过 flutter_redux 的 combineReducers
与 TypedReducer
,将 RefreshThemeDataAction
类 和 _refresh
方法绑定起来,最终会返回一个 ThemeData
实例。也就是说:用户每次发出一个 RefreshThemeDataAction ,最终都会触发 _refresh 方法,而后更新 GSYState 中的 themeData。
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
///经过 flutter_redux 的 combineReducers,建立 Reducer<State>
final ThemeDataReducer = combineReducers<ThemeData>([
///将Action,处理Action动做的方法,State绑定
TypedReducer<ThemeData, RefreshThemeDataAction>(_refresh),
]);
///定义处理 Action 行为的方法,返回新的 State
ThemeData _refresh(ThemeData themeData, action) {
themeData = action.themeData;
return themeData;
}
///定义一个 Action 类
///将该 Action 在 Reducer 中与处理该Action的方法绑定
class RefreshThemeDataAction {
final ThemeData themeData;
RefreshThemeDataAction(this.themeData);
}
复制代码
OK,如今咱们能够愉悦的建立 Store 了。以下代码所示,在建立 Store 的同时,咱们经过 initialState
对 GSYState 进行了初始化,而后经过 StoreProvider
加载了 Store 而且包裹了 MaterialApp
。 至此咱们完成了 Redux 中的初始化构建。
void main() {
runApp(new FlutterReduxApp());
}
class FlutterReduxApp extends StatelessWidget {
/// 建立Store,引用 GSYState 中的 appReducer 建立 Reducer
/// initialState 初始化 State
final store = new Store<GSYState>(
appReducer,
initialState: new GSYState(
userInfo: User.empty(),
themeData: new ThemeData(
primarySwatch: GSYColors.primarySwatch,
),
locale: Locale('zh', 'CH')),
);
FlutterReduxApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// 经过 StoreProvider 应用 store
return new StoreProvider(
store: store,
child: new MaterialApp(),
);
}
}
复制代码
And then,接下来就是使用了。以下代码所示,经过在 build
中使用 StoreConnector
,经过 converter
转化 store.state 的数据,最后经过 builder
返回实际须要渲染的控件,这样就完成了数据和控件的绑定。固然,你也可使用StoreBuilder
。
class DemoUseStorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
///经过 StoreConnector 关联 GSYState 中的 User
return new StoreConnector<GSYState, User>(
///经过 converter 将 GSYState 中的 userInfo返回
converter: (store) => store.state.userInfo,
///在 userInfo 中返回实际渲染的控件
builder: (context, userInfo) {
return new Text(
userInfo.name,
);
},
);
}
}
复制代码
最后,当你须要触发更新的时候,只须要以下代码便可。
StoreProvider.of(context).dispatch(new UpdateUserAction(newUserInfo));
复制代码
So,或者简单的业务逻辑下,Redux 并无什么优点,甚至显得繁琐。可是一旦框架搭起来,在复杂的业务逻辑下就会显示格外愉悦了。
Flutter 中官方默认就支持主题设置,MaterialApp
提供了 theme
参数设置主题,以后能够经过 Theme.of(context)
获取到当前的 ThemeData
用于设置控件的颜色字体等。
ThemeData
的建立提供不少参数,这里主要说 primarySwatch
参数。 primarySwatch
是一个 MaterialColor 对象,内部由10种不一样深浅的颜色组成,用来作主题色调再合适不过。
以下图和代码所示,Flutter 默认提供了不少主题色,同时咱们也能够经过 MaterialColor
实现自定义的主题色。
MaterialColor primarySwatch = const MaterialColor(
primaryValue,
const <int, Color>{
50: const Color(primaryLightValue),
100: const Color(primaryLightValue),
200: const Color(primaryLightValue),
300: const Color(primaryLightValue),
400: const Color(primaryLightValue),
500: const Color(primaryValue),
600: const Color(primaryDarkValue),
700: const Color(primaryDarkValue),
800: const Color(primaryDarkValue),
900: const Color(primaryDarkValue),
},
);
复制代码
那如何实现实时的主题切换呢?固然是经过 Redux 啦!
前面咱们已经在 GSYState 中建立了 themeData
,此时将它设置给 MaterialApp 的 theme
参数,以后咱们经过 dispatch 改变 themeData
便可实现主题切换。
注意,由于你的 MaterialApp 也是一个 StatefulWidget
,以下代码所示,还须要利用 StoreBuilder
包裹起来,以后咱们就能够经过 dispatch
修改主题,经过 Theme.of(context).primaryColor
获取主题色啦。
@override
Widget build(BuildContext context) {
/// 经过 StoreProvider 应用 store
return new StoreProvider(
store: store,
child: new StoreBuilder<GSYState>(builder: (context, store) {
return new MaterialApp(
theme: store.state.themeData);
}),
);
}
····
ThemeData themeData = new ThemeData(primarySwatch: colors[index]);
store.dispatch(new RefreshThemeDataAction(themeData));
复制代码
Flutter的国际化按照官网文件 internationalization 看起来稍微有些复杂,也没有说起实时切换,因此这里介绍下快速的实现。固然,少不了 Redux !
如上图所示大体流程,一样是经过默认 MaterialApp
设置,自定义的多语言须要实现的是: LocalizationsDelegate
和 Localizations
。最终流程会经过 Localizations
使用 Locale
加载这个 delegate
。因此咱们要作的是:
以下代码所示,建立自定义 delegate 须要继承 LocalizationsDelegate
对象,其中主要实现 load
方法。咱们能够是经过方法的 locale
参数,判断须要加载的语言,而后返回咱们自定义好多语言实现类 GSYLocalizations
,最后经过静态 delegate
对外提供 LocalizationsDelegate
。
/**
* 多语言代理
* Created by guoshuyu
* Date: 2018-08-15
*/
class GSYLocalizationsDelegate extends LocalizationsDelegate<GSYLocalizations> {
GSYLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
///支持中文和英语
return ['en', 'zh'].contains(locale.languageCode);
}
///根据locale,建立一个对象用于提供当前locale下的文本显示
@override
Future<GSYLocalizations> load(Locale locale) {
return new SynchronousFuture<GSYLocalizations>(new GSYLocalizations(locale));
}
@override
bool shouldReload(LocalizationsDelegate<GSYLocalizations> old) {
return false;
}
///全局静态的代理
static GSYLocalizationsDelegate delegate = new GSYLocalizationsDelegate();
}
复制代码
上面提到的 GSYLocalizations
实际上是一个自定义对象,以下代码所示,它会根据建立时的 Locale
,经过 locale.languageCode
判断返回对应的语言实体:GSYStringBase的实现类。
由于 GSYLocalizations 对象最后会经过Localizations
加载,因此 Locale
也是在那时,经过 delegate 赋予。同时在该 context 下,能够经过Localizations.of
获取 GSYLocalizations,好比: GSYLocalizations.of(context).currentLocalized.app_name
。
///自定义多语言实现
class GSYLocalizations {
final Locale locale;
GSYLocalizations(this.locale);
///根据不一样 locale.languageCode 加载不一样语言对应
///GSYStringEn和GSYStringZh都继承了GSYStringBase
static Map<String, GSYStringBase> _localizedValues = {
'en': new GSYStringEn(),
'zh': new GSYStringZh(),
};
GSYStringBase get currentLocalized {
return _localizedValues[locale.languageCode];
}
///经过 Localizations 加载当前的 GSYLocalizations
///获取对应的 GSYStringBase
static GSYLocalizations of(BuildContext context) {
return Localizations.of(context, GSYLocalizations);
}
}
///语言实体基类
abstract class GSYStringBase {
String app_name;
}
///语言实体实现类
class GSYStringEn extends GSYStringBase {
@override
String app_name = "GSYGithubAppFlutter";
}
///使用
GSYLocalizations.of(context).currentLocalized.app_name
复制代码
说完了 delegate , 接下来就是 Localizations
了。在上面的流程图中能够看到, Localizations 提供一个 override
方法构建 Localizations
,这个方法中能够设置 locale,而咱们须要的正是实时的动态切换语言显示。
以下代码,咱们建立一个 GSYLocalizations
的 Widget,经过 StoreBuilder
绑定 Store,而后经过 Localizations.override
包裹咱们须要构建的页面,将 Store 中的 locale
和 Localizations 的 locale
绑定起来。
class GSYLocalizations extends StatefulWidget {
final Widget child;
GSYLocalizations({Key key, this.child}) : super(key: key);
@override
State<GSYLocalizations> createState() {
return new _GSYLocalizations();
}
}
class _GSYLocalizations extends State<GSYLocalizations> {
@override
Widget build(BuildContext context) {
return new StoreBuilder<GSYState>(builder: (context, store) {
///经过 StoreBuilder 和 Localizations 实现实时多语言切换
return new Localizations.override(
context: context,
locale: store.state.locale,
child: widget.child,
);
});
}
}
复制代码
以下代码,最后将 GSYLocalizations
使用到 MaterialApp
中。经过 store.dispatch
切换 Locale
便可。
@override
Widget build(BuildContext context) {
/// 经过 StoreProvider 应用 store
return new StoreProvider(
store: store,
child: new StoreBuilder<GSYState>(builder: (context, store) {
return new MaterialApp(
///多语言实现代理
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GSYLocalizationsDelegate.delegate,
],
locale: store.state.locale,
supportedLocales: [store.state.locale],
routes: {
HomePage.sName: (context) {
///经过 Localizations.override 包裹一层。---这里
return new GSYLocalizations(
child: new HomePage(),
);
},
});
}),
);
}
///切换主题
static changeLocale(Store<GSYState> store, int index) {
Locale locale = store.state.platformLocale;
switch (index) {
case 1:
locale = Locale('zh', 'CH');
break;
case 2:
locale = Locale('en', 'US');
break;
}
store.dispatch(RefreshLocaleAction(locale));
}
复制代码
最后的最后,在改变时记录状态,在启动时取出后dispatch
,至此主题和多语言设置完成。
自此,第四篇终于结束了!(///▽///)
《Flutter完整开发实战详解(1、Dart语言和Flutter基础)》