[- Flutter 状态篇 -] 主题色切换+国际化 三连

7,044 阅读9分钟

本文收录于张风捷特烈的公众号编程之王: 文章内存地址f-s-a-04
如何获取更多知识干粮,详见 <<编程之王食用规范1.0>>


很多Flutter状态管理文章都是改计数器,搞得总感觉用了反而麻烦。搞太复杂的例子,一篇文章又不现实。就拿主题色切换+国际化开刀吧。本文会说一下provoderBLoCredux的三种实现主题色切换+国际化的实现方式,所以称三连击。


一.provoder实现主题切换和国际化:provider: ^03.1.0+1

1-主题色切换

点击颜色切换按钮,进行全局主题色切换。


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-语言切换切换

点击侧栏按钮进行语言切换


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 changeLocaleState(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.changeLocaleState(LocaleState.zh())
state.changeLocaleState(LocaleState.zh())

这样就演示了Provider在多状态的情况下如何工作。


二、redux实现主题切换和国际化:flutter_redux: ^0.5.3

作为一个但数据源的全局状态管理库,redux采取标准的分封制。总状态作为天子,再将任务细化分给各大诸侯,诸侯同样也细化分给卿大夫。当每个人都管理好自己的责任,那么就天下太平,生生不息。这里只用两个状态来说,也就是主题色和国际化。


1-redux三大件

点击颜色切换按钮,进行全局主题色切换。思路是极为一致的,让我们看看有哪些不同,首先要说的是rudux的三大件:状态State,行为Action处理器Reducer。所有状态由仓库统一管理,天子状态AppState向下分封。

在定义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{
  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(),
    ));
  }
}

在使用时无论是状态,还是事件分发,统一由仓库进行管理,结果是一致的:

---->[获取状态量]----
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事件


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,期待与你的交流与切磋。另外欢迎关注公众号编程之王