[- Flutter 狀態篇 -] 主題色切換+國際化 三連

張風捷特烈 發表於 2019-10-19

本文收錄於張風捷特烈的公眾號程式設計之王: 文章記憶體地址f-s-a-04
如何獲取更多知識乾糧,詳見 <<程式設計之王食用規範1.0>>


很多Flutter狀態管理文章都是改計數器,搞得總感覺用了反而麻煩。搞太複雜的例子,一篇文章又不現實。就拿主題色切換+國際化開刀吧。本文會說一下provoderBLoCredux的三種實現主題色切換+國際化的實現方式,所以稱三連擊。

[- Flutter 狀態篇 -] 主題色切換+國際化 三連


一.provoder實現主題切換和國際化:provider: ^03.1.0+1

1-主題色切換

點選顏色切換按鈕,進行全域性主題色切換。

[- Flutter 狀態篇 -] 主題色切換+國際化 三連


1.1- 狀態類

既然是狀態管理,首先來看狀態。顏色毋庸置疑,還有一個是顏色的選中索引,用來體現顏色按鈕的選中情況。繼承自ChangeNotifier,將狀態量作為屬性,使用changeThemeData來方法改變狀態量,並通知需要小夥伴們,讓它們重新整理。

---->[provider/theme_state.dart]----
class ThemeState extends ChangeNotifier{
  ThemeData _themeData;//主題
  int _colorIndex;//主題

  ThemeState(this._colorIndex,this._themeData,);

  void changeThemeData(int colorIndex,ThemeData themeData){
    _themeData = themeData;
    _colorIndex = colorIndex;
    notifyListeners();
  }

  ThemeData get themeData => _themeData; //獲取主題
  int get colorIndex => _colorIndex; //獲取數字
}
複製程式碼

1.2- 頂上包裹

狀態管理庫的套路基本一致,將需要管理的部分包裹起來,這裡直接上多個provider的包裹器。為了好看點,這裡新建一個Wrapper元件來包裹。

void main() => runApp(Wrapper(child:MyApp()));

class Wrapper extends StatelessWidget {
  final Widget child;
  Wrapper({this.child});

  @override
  Widget build(BuildContext context) {

   final initThemeData=  ThemeData( //初始主題
      primaryColor: Colors.blue,
    );
 
   final initIndex=4;//初始索引

    return MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (_) => ThemeState(initIndex,initThemeData)), //在這提供provider
      ],
      child: child, //孩子
    );
  }
}
複製程式碼

1.3- 使用狀態和呼叫方法

Provider.of(context).themeData就可以獲取ThemeData 不過為了縮小構建的粒度,使用Consumer進行對點消費。

---->[main.dart]----
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeState>(builder: (_,state,__)=>MaterialApp(//對點消費
      title: 'Flutter Demo',
      theme: state.themeData,//獲取資料
      home: MyHomePage(),
    ));
  }
}

---->[pages/home_page.dart]----
children: <Widget>[
    Consumer<ThemeState>(builder: (_,state,__) => 
    Text( '----海的彼岸,有我未曾見證的風采',
        style: TextStyle(color: state.themeData.primaryColor,
        fontSize: 18,
        fontWeight: FontWeight.bold),
...

複製程式碼

所以只要有需要顏色的地方,都可以使用這種方法從狀態中拿主題色,顏色的切換事件觸發也是非常簡單。ColorChooser是我自定義的元件,在點選時會將索引和顏色值回撥出來,在此觸發changeThemeData方法來更新消費者的狀態。

var colors = Consumer<ThemeState>(builder: (_,state,__)=>ColorChooser(
  colors: Cons.THEME_COLORS,
  initialIndex: state.colorIndex,//同步索引狀態
  onChecked: (i,color) {
    ThemeData themeData = ThemeData(primaryColor: color);//顏色
     state.changeThemeData(i,themeData);//觸發事件
  },
));
複製程式碼

這樣主題切換色切換就OK了


2-語言切換切換

點選側欄按鈕進行語言切換

[- Flutter 狀態篇 -] 主題色切換+國際化 三連


dependencies: # 庫依賴
  ...
  flutter_localizations: #國際化
    sdk: flutter
複製程式碼

2.1-首先準備資料
class Data{
  static final EN={
    "title":"ZF·G·Toly ",
    "subTitle":"---- You are nothing at all",
    "content":"public: The King Of Coder",
    "sideTitle":"I Have a Dream",
    "step1":"Unified the Earth",
    "step2":"Unified the Solar System",
    "step3":"Unified the Galaxy",
    "step4":"Unified the Universe",
    "step5":"Unified All Universe",
    "step4SubTitle":"To be the king of Universe" ,
    "step4Info":"A.D. 34679,toly unified the Universe,be the first omniscient。",
    "btn2CN":"切換中文。",
    "btn2EN":"To English。",
  };


  static final ZN={
    "title":"張風捷特烈 ",
    "subTitle":"----海的彼岸,有我未曾見證的風采",
    "content":"公眾號:程式設計之王",
    "sideTitle":"列一個小目標",
    "step1":"統一地球",
    "step2":"統一太陽系",
    "step3":"統一銀河系",
    "step4":"統一宇宙",
    "step5":"統一平行宇宙",
    "step4SubTitle":"成為宇宙之王",
    "step4Info":"公元34679年,捷特統一已知宇宙,成為第一個全知。",
    "btn2CN":"切換中文。",
    "btn2EN":"To English。",
  };
}
複製程式碼

2.2-然後我寫了個工具類一鍵生成相關程式碼

執行後自動生成下面的檔案:

---->[I18N代理相關]----
///Power By 張風捷特烈--- Generated file. Do not edit.
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'i18n.dart';

///多語言代理類
class I18nDelegate extends LocalizationsDelegate<I18N> {
  I18nDelegate();
  @override
  bool isSupported(Locale locale) {
    ///設定支援的語言
    return ['en', 'zh'].contains(locale.languageCode);
  }
  ///載入當前語言下的字串
  @override
  Future<I18N> load(Locale locale) {
    return  SynchronousFuture<I18N>( I18N(locale));
  }
  @override
  bool shouldReload(LocalizationsDelegate<I18N> old) {
    return false;
  }
  ///全域性靜態的代理
  static I18nDelegate delegate =  I18nDelegate();
}

---->[I18N使用類]----
  /// Power By 張風捷特烈--- Generated file. Do not edit.
  import 'package:flutter/material.dart';
  import 'data.dart';

  class I18N {
  final Locale locale;
  I18N(this.locale);
  static Map<String, Map<String,String>> _localizedValues = {
    'en': Data.EN,//英文
    'zh': Data.ZN,//中文
  };
  static I18N of(BuildContext context) {
    return Localizations.of(context, I18N);
  }
  get title {
 return _localizedValues[locale.languageCode]['title'];}
get subTitle {
 return _localizedValues[locale.languageCode]['subTitle'];}
get content {
 return _localizedValues[locale.languageCode]['content'];}
get sideTitle {
 return _localizedValues[locale.languageCode]['sideTitle'];}
get step1 {
 return _localizedValues[locale.languageCode]['step1'];}
get step2 {
 return _localizedValues[locale.languageCode]['step2'];}
get step3 {
 return _localizedValues[locale.languageCode]['step3'];}
get step4 {
 return _localizedValues[locale.languageCode]['step4'];}
get step5 {
 return _localizedValues[locale.languageCode]['step5'];}
get step4SubTitle {
 return _localizedValues[locale.languageCode]['step4SubTitle'];}
get step4Info {
 return _localizedValues[locale.languageCode]['step4Info'];}
get btn2CN {
 return _localizedValues[locale.languageCode]['btn2CN'];}
get btn2EN {
 return _localizedValues[locale.languageCode]['btn2EN'];}
}
複製程式碼

2.3-狀態類

就一個欄位,很簡單,為了方便使用,這裡定義兩個factory來快速生成物件。

class LocaleState extends ChangeNotifier{
  Locale _locale;//主題
  LocaleState(this._locale);

  factory LocaleState.zh()=>
      LocaleState(Locale('zh', 'CH'));

  factory LocaleState.en()=>
      LocaleState(Locale('en', 'US'));

  void changeThemeData(LocaleState state){
    _locale=state.locale;
    notifyListeners();
  }

  Locale get locale => _locale; //獲取語言
}
複製程式碼

2.4-使用

如果一個元件有多個狀態值可以用Consumer2,最多有6個。
另外這裡層級不深,也可以直接使用Provider.of(context)來獲取狀態類

---->[main.dart 新增提供器]----
return MultiProvider(
  providers: [
    ChangeNotifierProvider(builder: (_) => ThemeState(initIndex,initThemeData)), //在這提供provider
    ChangeNotifierProvider(builder: (_) => LocaleState.zh()), //在這提供provider
  ],
  child: child, //孩子
);

---->[MaterialApp中進行國際化配置]----
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer2<ThemeState, LocaleState>(
        builder: (_, themeState, localeState, __) =>
            MaterialApp( //對點消費
              title: 'Flutter Demo',
              localizationsDelegates: [
                GlobalMaterialLocalizations.delegate,
                GlobalWidgetsLocalizations.delegate,
                I18nDelegate.delegate, //新增
              ],
              locale: localeState.locale,
              supportedLocales: [
                localeState.locale
              ],
              theme: themeState.themeData, //獲取資料
              home: MyHomePage(),
            ));
  }
}

---->[國際化的使用]----
Consumer<ThemeState>(builder: (_,state,__) => Text(
   I18N.of(context).subTitle,//獲取字串
   style: TextStyle(
        color: state.themeData.primaryColor,
        fontSize: 18,
        fontWeight: FontWeight.bold),
 ),),
 
---->[行為觸發]----
state.changeThemeData(LocaleState.zh())
state.changeThemeData(LocaleState.zh())
複製程式碼

這樣就演示了Provider在多狀態的情況下如何工作。


二、redux實現主題切換和國際化:flutter_redux: ^0.5.3

作為一個但資料來源的全域性狀態管理庫,redux採取標準的分封制。總狀態作為天子,再將任務細化分給各大諸侯,諸侯同樣也細化分給卿大夫。當每個人都管理好自己的責任,那麼就天下太平,生生不息。這裡只用兩個狀態來說,也就是主題色和國際化。


1-redux三大件

點選顏色切換按鈕,進行全域性主題色切換。思路是極為一致的,讓我們看看有哪些不同,首先要說的是rudux的三大件:狀態State,行為Action處理器Reducer。所有狀態由倉庫統一管理,天子狀態AppState向下分封。

[- Flutter 狀態篇 -] 主題色切換+國際化 三連

在定義redux狀態時,我習慣定義一個初始狀態,方便使用。當然你也可以不用,直接在使用時來構建。

---->[全域性redux]----
class AppState {
  final ThemeState themeState;//左翼護衛主題管理大臣
  final LocaleState localeState;//右翼護衛語言管理大臣
  AppState({this.themeState, this.localeState});

  factory AppState.initial()=> AppState(
      themeState: ThemeState.initial(),
      localeState: LocaleState.initial()
  );
}

//總處理器--分封職責
AppState appReducer(AppState prev, dynamic action)=>
    AppState(
      themeState:themeDataReducer(prev.themeState, action),
      localeState: localReducer(prev.localeState, action),);
複製程式碼

---->[主題redux]----
//切換主題狀態
class ThemeState extends ChangeNotifier {
  ThemeData themeData; //主題
  int colorIndex; //數字
  ThemeState(this.colorIndex,
      this.themeData,);

  factory ThemeState.initial()=> ThemeState(4, ThemeData(primaryColor: Colors.blue,));
}

//切換主題行為
class ActionSwitchTheme {
  final ThemeData themeData;
  final int colorIndex;
  ActionSwitchTheme(this.colorIndex, this.themeData);
}

//切換主題理器
var themeDataReducer = TypedReducer<ThemeState, ActionSwitchTheme>((state, action) =>
    ThemeState(action.colorIndex, action.themeData,));
複製程式碼

---->[國際化redux]----
//切換語言狀態
class LocaleState extends ChangeNotifier{
  Locale locale;//主題
  LocaleState(this.locale);
  factory LocaleState. initial()=> LocaleState(Locale('zh', 'CH'));
}

//切換語言行為
class ActionSwitchLocal {
  final Locale locale;
  ActionSwitchLocal(this.locale);
  factory ActionSwitchLocal.zh()=> ActionSwitchLocal(Locale('zh', 'CH'));
  factory ActionSwitchLocal.en()=> ActionSwitchLocal(Locale('en', 'US'));
}

//切換語言處理器
var localReducer = TypedReducer<LocaleState, ActionSwitchLocal>(( state,  action) => 
    LocaleState(action.locale,));
複製程式碼

2-redux的屬性使用

redux需要用StoreProvider進行包裹,其中在store屬性下進行倉庫的配置。
StoreBuilder就像Provider中的Consumer一樣的存在,只不過泛型都是統一的天子AppState。

void main() => runApp(Wrapper(child: MyApp()));

class Wrapper extends StatelessWidget {
  final Widget child;
  Wrapper({this.child});
  @override
  Widget build(BuildContext context) {
    return StoreProvider(
        store: Store<AppState>(
          appReducer,
          initialState: AppState.initial(),//初始狀態
        ),
        child:child);
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreBuilder<AppState>(builder: (context, store) => MaterialApp( //對點消費
      title: 'Flutter Demo',
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        I18nDelegate.delegate, //新增
      ],
      locale: store.state.localeState.locale,
      supportedLocales: [
        store.state.localeState.locale
      ],
      theme: store.state.themeState.themeData, //獲取資料
      home: MyHomePage(),
    ));
  }
}
複製程式碼

在使用時無論是狀態,還是事件分發,統一由倉庫進行管理,結果是一致的:

[- Flutter 狀態篇 -] 主題色切換+國際化 三連

---->[獲取狀態量]----
StoreBuilder<AppState>(
  builder: (_, store) =>Text(
  I18N.of(context).subTitle,
  style: TextStyle(
    color: store.state.themeState.themeData.primaryColor,//通過倉庫拿資料
    fontSize: 18,
    fontWeight: FontWeight.bold),
),),

---->[分發事件]----
var colors =  StoreBuilder<AppState>(
    builder: (_, store) =>ColorChooser(
  colors: Cons.THEME_COLORS,
  initialIndex: store.state.themeState.colorIndex,//同步索引狀態
  onChecked: (i,color) {
    ThemeData themeData = ThemeData(primaryColor: color);//顏色
     store.dispatch(ActionSwitchTheme(i,themeData));//觸發事件
  },
));
複製程式碼

redux的好處在於狀態資源統一管理。層層分封,結構清晰。


三、BLoC實現主題切換和國際化:flutter_bloc: ^0.22.1

如果是redux是中央集權,地方分權,那麼BloC就是完全的自由民主。一個BloC也有三大件:Bloc 業務邏輯單元State狀態Events事件

[- Flutter 狀態篇 -] 主題色切換+國際化 三連


1.主題色的BloC
  • 狀態類

可以根據自己的愛好寫出自己的風格。下面是我比較喜歡的風格。將狀態量放在抽象類中,其他狀態去繼承他來實現狀態的分化。只要你想,也可以加一些常用狀態。

@immutable
abstract class ThemeState {
  final ThemeData themeData; //主題
  final int colorIndex;//數字
  ThemeState( this.colorIndex,this.themeData);
}

class InitialThemeState extends ThemeState {
  InitialThemeState() : super(4, ThemeData(primaryColor: Colors.blue,));
}

class ThemeStateImpl extends ThemeState {
  ThemeStateImpl(int colorIndex, ThemeData themeData) : super(colorIndex, themeData);
}

複製程式碼

  • 事件類

定義Bloc可執行的事件,比如這裡直接傳兩參切換和重置狀態

@immutable
abstract class ThemeEvent {}

class EventSwitchTheme extends ThemeEvent{
  final ThemeData themeData; //主題
  final int colorIndex;//數字
  EventSwitchTheme( this.colorIndex,this.themeData);
}

class EventResetTheme extends ThemeEvent{}
複製程式碼

  • 業務邏輯單元類

這是Bloc的核心,主要通過事件去生成狀態。

class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
  @override
  ThemeState get initialState => InitialThemeState();//初始狀態

  @override
  Stream<ThemeState> mapEventToState(ThemeEvent event,) async* {//使用非同步生成器
    if(event is EventSwitchTheme){//如果是切換主題事件,生成對應的ThemeState
      yield ThemeStateImpl(event.colorIndex,event.themeData);
    }
    if(event is EventResetTheme){//如果是重置主題事件,生成initialState
      yield InitialThemeState();
    }
  }
}
複製程式碼

2.國際化的BloC
  • 狀態類
@immutable
abstract class LocaleState {
 final Locale locale;
  LocaleState(this.locale); 
}

class InitialLocaleState extends CnLocaleState {}

class CnLocaleState extends LocaleState {
  CnLocaleState() : super(Locale('zh', 'CH'));
}

class EnLocaleState extends LocaleState {
  EnLocaleState() : super(Locale('en', 'US'));
}
複製程式碼

  • 事件類
@immutable
abstract class LocaleEvent {}

class EventSwitch2CN extends LocaleEvent{}

class EventSwitch2EN extends LocaleEvent{}
複製程式碼

  • 業務邏輯單元類
class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
  @override
  LocaleState get initialState => InitialLocaleState();

  @override
  Stream<LocaleState> mapEventToState(LocaleEvent event,) async* {
    if(event is EventSwitch2CN){//如果是切換到CN,生成CnLocaleState
      yield CnLocaleState();
    }
    if(event is EventSwitch2EN){//如果是重置主題事件,生成EnLocaleState
      yield EnLocaleState();
    }
  }
}
複製程式碼

3.Bloc的使用

用起來都極為相似,外層使用:MultiBlocProvider

void main() => runApp(Wrapper(child: MyApp()));

class Wrapper extends StatelessWidget {
  final Widget child;

  Wrapper({this.child});
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
        providers: [
          BlocProvider<ThemeBloc>(builder: (context) => ThemeBloc(),),
          BlocProvider<LocaleBloc>(builder: (context) => LocaleBloc(),),
        ],
        child: MyApp()
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeBloc, ThemeState>(builder: (_, theme) =>
        BlocBuilder<LocaleBloc, LocaleState>(builder: (_, local) =>
            MaterialApp( //對點消費
              title: 'Flutter Demo',
              localizationsDelegates: [
                GlobalMaterialLocalizations.delegate,
                GlobalWidgetsLocalizations.delegate,
                I18nDelegate.delegate, //新增
              ],
              locale: local.locale,
              supportedLocales: [
                local.locale
              ],
              theme: theme.themeData, //獲取資料
              home: MyHomePage(),
            )));
  }
}
複製程式碼

狀態的獲取通過BlocBuilder<XXXBloc, XXXState>(builder: (_, theme)

--->[獲取狀態量]----
BlocBuilder<ThemeBloc, ThemeState>(
  builder: (_, state) =>Text(
  I18N.of(context).subTitle,
  style: TextStyle(
      color: state.themeData.primaryColor,
      fontSize: 18,
      fontWeight: FontWeight.bold),
),),


---->[分發事件]----
var colors =  BlocBuilder<ThemeBloc, ThemeState>(
    builder: (_, state) =>ColorChooser(
  colors: Cons.THEME_COLORS,
  initialIndex:state.colorIndex,//同步索引狀態
  onChecked: (i,color) {
    ThemeData themeData = ThemeData(primaryColor: color);//顏色
    BlocProvider.of<ThemeBloc>(context).add(EventSwitchTheme(i, themeData));//觸發事件
  },
));
複製程式碼

總的來說,大同小異。如果Stream流理解地較好,BloC用起來可以感覺是非常優雅的。個人還是比較喜歡redux。Provider作為官宣,也挺好用的。如果hold得住,混用也是可以的。本文理解了,你的Flutter狀態管理也只不過剛剛入門。之後還會有很長的路要走...


結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。另外歡迎關注公眾號程式設計之王

[- Flutter 狀態篇 -] 主題色切換+國際化 三連