作為系列文章的第四篇,本篇主要介紹 Flutter 中 Redux 的使用,並結合Redux 完成實時的主題切換與多語言切換功能。
前文:
Flutter 作為響應式框架,通過 state
實現跨幀渲染的邏輯,難免讓人與 React 和 React Native 聯絡起來,而其中 React 下*“廣為人知”*的 Redux 狀態管理,其實在 Flutter 中同樣適用。
我們最終將實現如下圖的效果,相應程式碼在 GSYGithubAppFlutter 中可找到,本篇 Flutter 中所使用的 Redux 庫是 flutter_redux 。
一、Redux
Redux 的概念是狀態管理,那在已有 state
的基礎上,為什麼還需要 Redux ?
因為使用 Redux 的好處是:共享狀態和單一資料。
試想一下,App內有多個地方使用到登陸使用者的資料,這時候如果某處對使用者資料做了修改,各個頁面的同步更新會是一件麻煩的事情。
但是引入 Redux 後,某個頁面修改了當前使用者資訊,所有繫結了 Redux 的控制元件,將由 Redux 自動同步重新整理。See!這在一定程度節省了我們的工作量,並且單一資料來源在某些場景下也方便管理。同理我們後面所說的 主題 和 多語言 切換也是如此。
如上圖,Redux 的主要由三部分組成:Store 、Action 、 Reducer 。
- Action 用於定義一個資料變化的請求行為。
- Reducer 用於根據 Action 產生新狀態,一般是一個方法。
- Store 用於儲存和管理 state。
所以一般流程為:
1、Widget 繫結了 Store 中的 state 資料。
2、Widget 通過 Action 釋出一個動作。
3、Reducer 根據 Action 更新 state。
4、更新 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
。所以我們要做的是:
- 實現 LocalizationsDelegate。
- 實現 Localizations。
- 通過 Store 的 Locale 切換語言。
如下程式碼所示,建立自定義 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
,至此主題和多語言設定完成。
自此,第四篇終於結束了!(///▽///)
資源推薦
- Github : github.com/CarGuo
- 本文程式碼 :github.com/CarGuo/GSYG…