いよいよ本シリーズの最終篇になります。
本篇は、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理のreduxパターンを説明します。
三、reduxでテーマ切替&多言語化
flutter_redux: ^0.6.0
Blocと同じのようにreduxにも3つのポイントがある。それは 状態State、アクションAction、変化させる者Reducer。
会社制に例えると、社長が、仕事を各部門に分与し、部門長ははまたそれぞれ部内の業務を部員に委託する。各々が責任を取って行動する。reduxにおいて、全ての状態はstoreによって集中管理し、社長AppStateから階層的に分配していく。
1.redux状態、アクション、変化させる者の準備
社長
import 'themeState_redux.dart';
import 'localState_redux.dart';
class AppState {
final ReduxThemeState themeState;//テーマ状態
final ReduxLocaleState localeState;//言語状態
AppState({this.themeState, this.localeState});
factory AppState.initial()=> AppState(
themeState: ReduxThemeState.initial(),
localeState: ReduxLocaleState.initial()
);
}
//集中的に状態を分配
AppState appReducer(AppState prev, dynamic action)=>
AppState(
themeState:themeDataReducer(prev.themeState, action),
localeState: localReducer(prev.localeState, action),);
部門長① theme
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
//テーマ状態の切り替え
class ReduxThemeState extends ChangeNotifier {
ThemeData themeData;
int colorIndex;
ReduxThemeState(this.themeData,this.colorIndex);
factory ReduxThemeState.initial()=> ReduxThemeState(ThemeData(primaryColor: Colors.green),1);
}
//テーマアクションの切り替え
class ActionSwitchTheme {
final ThemeData themeData;
final int colorIndex;
ActionSwitchTheme(this.themeData,this.colorIndex);
}
//変化させる者の切り替え
var themeDataReducer = TypedReducer<ReduxThemeState, ActionSwitchTheme>((state, action) =>
ReduxThemeState(action.themeData,action.colorIndex));
部門長② local
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
//言語状態の切り替え
class ReduxLocaleState{
Locale locale;
ReduxLocaleState(this.locale);
factory ReduxLocaleState. initial()=> ReduxLocaleState(Locale('ja', 'JP'));
}
//言語アクションの切り替え
class ActionSwitchLocal {
final Locale locale;
ActionSwitchLocal(this.locale);
factory ActionSwitchLocal.ja()=> ActionSwitchLocal(Locale('ja', 'JP'));
factory ActionSwitchLocal.en()=> ActionSwitchLocal(Locale('en', 'US'));
}
//変化させる者の切り替え
var localReducer = TypedReducer<ReduxLocaleState, ActionSwitchLocal>((state, action) =>
ReduxLocaleState(action.locale,));
2.reduxプロパティーの使用
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; //providerパターンのpubspec.yaml配置を参照
import 'home_page.dart';
import 'provider/I18nDelegate.dart'; //providerパターン紹介時のファイル使用
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'redux/appState_redux.dart';
void main() {
runApp(Wrapper(child: MyApp()));
}
class Wrapper extends StatelessWidget {
final Widget child;
Wrapper({this.child});
@override
Widget build(BuildContext context) {
//StoreProviderによってラップする、storeプロパティーでAppStateを配置
return StoreProvider(
store: Store<AppState>(appReducer, initialState: AppState.initial()),
child: MyApp());
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(builder: (context,store) => MaterialApp(
title: "状態管理Demo",
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
I18nDelegate.delegate //言語デリゲート シリーズ初編のproviderパターンのファイルを利用
],
locale: store.state.localeState.locale,
supportedLocales: [store.state.localeState.locale],
debugShowCheckedModeBanner: false,
home: HomePage(),
));
}
}
状態の切り替えは store.dispatch(~action) を使う
import 'package:flutter/material.dart';
import 'provider/i18n.dart'; //providerパターン紹介時のファイル使用
import 'package:flutter_redux/flutter_redux.dart';
import 'redux/appState_redux.dart';
import 'redux/themeState_redux.dart';
import 'redux/localState_redux.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
var screenWidth = MediaQuery.of(context).size.width;
var statusBarH = MediaQuery.of(context).padding.top;
var naviBarH = kToolbarHeight;
//StoreBuilder<AppState>(builder: (context,store) を通じて状態ゲット
return StoreBuilder<AppState>(
builder: (context, store) => Scaffold(
appBar: AppBar(
backgroundColor: store.state.themeState.themeData.primaryColor,
title: Text(
I18N.of(context).title,
style: TextStyle(color: Colors.white),
),
),
drawer: Drawer(
child: Column(
children: <Widget>[
Container(
width: screenWidth,
height: statusBarH + naviBarH,
color: store.state.themeState.themeData.primaryColor,
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Padding(padding: EdgeInsets.all(10)),
Wrap(
children: <Widget>[
RaisedButton(
color: Colors.green,
onPressed: () {
//イベントトリガー、重要
store.dispatch(ActionSwitchTheme(
ThemeData(primaryColor: Colors.green), 1));
},
child: Text(
I18N.of(context).greenBtn,
style: TextStyle(color: Colors.white),
),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.red,
onPressed: () {
store.dispatch(ActionSwitchTheme(
ThemeData(primaryColor: Colors.red), 2));
},
child: Text(
I18N.of(context).redBtn,
style: TextStyle(color: Colors.white),
),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.blue,
onPressed: () {
store.dispatch(ActionSwitchTheme(
ThemeData(primaryColor: Colors.blue), 3));
},
child: Text(
I18N.of(context).blueBtn,
style: TextStyle(color: Colors.white),
),
shape: CircleBorder(),
),
],
),
],
),
Divider(),
SizedBox(
height: 10,
),
Row(
children: <Widget>[
Padding(padding: EdgeInsets.all(10)),
SizedBox(
width: 15,
),
RaisedButton(
color: store.state.themeState.themeData.primaryColor,
onPressed: () {
store.dispatch(ActionSwitchLocal.ja());
},
child: Text(
"日本語",
style: TextStyle(color: Colors.white),
),
),
SizedBox(
width: 15.0,
),
RaisedButton(
color: store.state.themeState.themeData.primaryColor,
onPressed: () {
store.dispatch(ActionSwitchLocal.en());
},
child: Text(
"English",
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
body: ListView(
children: <Widget>[
Center(
child: Column(
children: <Widget>[
SizedBox(height: 10.0),
Container(
width: 200,
height: 300,
color: store.state.themeState.themeData.primaryColor,
),
SizedBox(height: 10.0),
Text(
I18N.of(context).analects,
style: TextStyle(
color: store.state.themeState.themeData.primaryColor,
fontSize: 18.0,
fontWeight: FontWeight.bold),
),
SizedBox(height: 30.0),
FloatingActionButton(
onPressed: () {},
backgroundColor:
store.state.themeState.themeData.primaryColor,
child: Icon(Icons.check)),
],
),
)
],
),
),
);
}
}
まとめ
いかがでしたか。本篇をもってflutterの状態管理シリーズを完了とします。総じて言えば、3パターンともに構造的に、実現方式的に似ているようなところがあります。providerはflutter公式が推奨するパターンとして使っても無難でしょう。Streamに対する理解を深めれば、Blocの使用は少し上級感が感じられます。reduxについては構造は明白で、このパターンを取る方も少なくないと思います。
どのパターンを採用するのかは正解がなく、プロジェクトのニーズに合わせて決めておけば良いでしょう。また、状態を変更する必要のあるwidgetがそれほど無ければ、基礎のsetState()というapiを使っては分かりやすいでしょう。