Flutter完整開發實戰詳解(4、 Redux、主題、國際化) | 掘金技術徵文

做爲系列文章的第四篇,本篇主要介紹 Flutter 中 Redux 的使用,並結合Redux 完成實時的主題切換多語言切換功能。git

前文:github

Flutter 做爲響應式框架,經過 state 實現跨幀渲染的邏輯,不免讓人與 ReactReact Native 聯繫起來,而其中 React 下*「廣爲人知」*的 Redux 狀態管理,其實在 Flutter 中一樣適用。redux

咱們最終將實現以下圖的效果,相應代碼在 GSYGithubAppFlutter 中可找到,本篇 Flutter 中所使用的 Redux 庫是 flutter_reduxbash

Let's do it

1、Redux

Redux 的概念是狀態管理,那在已有 state 的基礎上,爲何還須要 Redux ?app

由於使用 Redux 的好處是:共享狀態單一數據框架

試想一下,App內有多個地方使用到登錄用戶的數據,這時候若是某處對用戶數據作了修改,各個頁面的同步更新會是一件麻煩的事情。less

可是引入 Redux 後,某個頁面修改了當前用戶信息,全部綁定了 Redux 的控件,將由 Redux 自動同步刷新。See!這在必定程度節省了咱們的工做量,而且單一數據源在某些場景下也方便管理。同理咱們後面所說的 主題多語言 切換也是如此。ide

大體流程圖

如上圖,Redux 的主要由三部分組成:Store 、Action 、 Reducerpost

  • Action 用於定義一個數據變化的請求行爲。
  • Reducer 用於根據 Action 產生新狀態,通常是一個方法。
  • Store 用於存儲和管理 state。

因此通常流程爲:字體

一、Widget 綁定了 Store 中的 state 數據。

二、Widget 經過 Action 發佈一個動做。

三、Reducer 根據 Action 更新 state。

四、更新 Store 中 state 綁定的 Widget。

根據這個流程,首先咱們要建立一個 Store

以下圖,建立 Store 須要 reducer ,而 reducer 其實是一個帶有 stateaction 的方法,並返回新的 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_reduxcombineReducersTypedReducer,將 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 並無什麼優點,甚至顯得繁瑣。可是一旦框架搭起來,在複雜的業務邏輯下就會顯示格外愉悅了。

2、主題

Flutter 中官方默認就支持主題設置,MaterialApp 提供了 theme 參數設置主題,以後能夠經過 Theme.of(context) 獲取到當前的 ThemeData 用於設置控件的顏色字體等。

ThemeData 的建立提供不少參數,這裏主要說 primarySwatch 參數。 primarySwatch 是一個 MaterialColor 對象,內部由10種不一樣深淺的顏色組成,用來作主題色調再合適不過。

以下圖和代碼所示,Flutter 默認提供了不少主題色,同時咱們也能夠經過 MaterialColor 實現自定義的主題色。

image.png

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 ,此時將它設置給 MaterialApptheme 參數,以後咱們經過 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));

複製代碼

愉悅的切換

3、國際化

Flutter的國際化按照官網文件 internationalization 看起來稍微有些複雜,也沒有說起實時切換,因此這裏介紹下快速的實現。固然,少不了 Redux !

大體流程

如上圖所示大體流程,一樣是經過默認 MaterialApp 設置,自定義的多語言須要實現的是: LocalizationsDelegateLocalizations。最終流程會經過 Localizations 使用 Locale 加載這個 delegate。因此咱們要作的是:

  • 實現 LocalizationsDelegate
  • 實現 Localizations
  • 經過 StoreLocale 切換語言。

以下代碼所示,建立自定義 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基礎)》

《Flutter完整開發實戰詳解(2、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(3、 打包與填坑篇)》

《跨平臺項目開源項目推薦》

《移動端跨平臺開發的深度解析》

咱們還會再見嗎?

從 0 到 1:個人 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索