Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。
本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。
##一、providerでテーマ切替&多言語化
provider: ^4.3.1 (providerパッケージをpub.devから取得)
###1.テーマ切替
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
####1.1.状態クラス
_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。
changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。
import 'package:flutter/material.dart';
class ProviderThemeState extends ChangeNotifier{
ThemeData _themeData;//テーマ
int _colorIndex;//色ボタン選択中の状態を記録
ProviderThemeState(this._themeData, this._colorIndex);
void changeThemeData(ThemeData themeData, int colorIndex){
this._themeData = themeData;
this._colorIndex = colorIndex;
notifyListeners();
}
int get colorIndex => this._colorIndex;
ThemeData get themeData => this._themeData;
}
####1.2.管理する必要がある部分をラップする(wrap)
MultiProviderは複数のproviderを入れることができる。widgetをwrapperだけでなく他のコンポーネントでもラップやカバーできる。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';
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.green,
);
final initIndex = 1;
return MultiProvider(providers: [
ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),
],child: child,);
}
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return Consumer<ProviderThemeState>(builder: (context,state,widget) =>MaterialApp(
title: "状態管理Demo",
theme: state.themeData,
debugShowCheckedModeBanner: false,
home: HomePage(),
));
}
}
####1.3.状態の使用、メソッドの実行
Provider.of(context)よりConsumerのほうは結構細かい対象を絞れるため、widget再ビルドの消費コストが削減される。また、ConsumerはwidgetのBuildContextが不要で使えるのも一大メリット。
ボタンの押下により、changeThemeData()が呼び出されConsumerの状態の状態が更新される。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/theme_state.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;
return Scaffold(
appBar: AppBar(
title: Text("Flutter状態管理",style: TextStyle(color: Colors.white),),
),
drawer: Drawer(
child: Consumer<ProviderThemeState>(builder: (context, state, widget) {
return Column(
children: <Widget>[
Container(
width: screenWidth,
height: statusBarH + naviBarH,
color: state.themeData.primaryColor,
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Padding(padding: EdgeInsets.all(10)),
Wrap(children: <Widget>[
RaisedButton(
color: Colors.green,
onPressed: () {
state.changeThemeData(ThemeData(primaryColor: Colors.green), 1);
},
child: Text("緑",style: TextStyle(color: Colors.white),),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.red,
onPressed: () {
state.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
},
child: Text("赤",style: TextStyle(color: Colors.white),),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.blue,
onPressed: () {
state.changeThemeData(ThemeData(primaryColor: Colors.blue), 3);
},
child: Text("青",style: TextStyle(color: Colors.white),),
shape: CircleBorder(),
),
],),
],
),
Divider(),
SizedBox(height: 10,),
Row(children: <Widget>[
Padding(padding: EdgeInsets.all(10)),
SizedBox(width: 15,),
RaisedButton(
color: state.themeData.primaryColor,
onPressed: () {
},
child: Text("日本語",style: TextStyle(color: Colors.white),),
),
SizedBox(width: 15.0,),
RaisedButton(
color: state.themeData.primaryColor,
onPressed: () {
},
child: Text("English",style: TextStyle(color: Colors.white),),
),
],),
],
);
}),
),
body: ListView(
children: <Widget>[
Consumer<ProviderThemeState>(builder: (context, state, widget) {
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 10.0),
Container(
width: 200,
height: 300,
color: state.themeData.primaryColor,
),
SizedBox(height: 10.0),
Text(
"学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し",
style: TextStyle(
color: state.themeData.primaryColor,
fontSize: 18.0,
fontWeight: FontWeight.bold),
),
SizedBox(height: 30.0),
FloatingActionButton(onPressed: (){
},
backgroundColor: state.themeData.primaryColor,
child: Icon(Icons.check)),
],
),
);
})
],
),
);
}
}
###2.言語切替
flutter_localizations:
sdk: flutter (多言語をpubspec.yamlで配置)
ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。
####2.1.言語データを準備する
class LanguageData {
static final EN = {
"title":"Flutter State Management",
"greenBtn":"Green",
"redBtn":"Red",
"blueBtn":"Blue",
"analects":"To learn without thinking is blindness, \nto think without learning is idleness (Confucius)."
};
static final JP = {
"title":"Flutter状態管理",
"greenBtn":"緑",
"redBtn":"赤",
"blueBtn":"青",
"analects":"学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し"
};
}
####2.2.言語データを使用するためのクラス
import 'package:flutter/material.dart';
import 'language_data.dart';
class I18N {
final Locale locale;
I18N(this.locale);
static Map<String, Map<String, String>> _localizedValues = {
"en": LanguageData.EN,
"ja": LanguageData.JP,
};
static I18N of(BuildContext context) {
return Localizations.of(context, I18N);
}
get title {
return _localizedValues[locale.languageCode]['title'];
}
get greenBtn {
return _localizedValues[locale.languageCode]['greenBtn'];
}
get redBtn {
return _localizedValues[locale.languageCode]['redBtn'];
}
get blueBtn {
return _localizedValues[locale.languageCode]['blueBtn'];
}
get analects {
return _localizedValues[locale.languageCode]['analects'];
}
}
####2.3.多言語のデリゲートクラス
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'i18n.dart';
class I18nDelegate extends LocalizationsDelegate<I18N> {
@override
bool isSupported(Locale locale) {
///サポートする言語
return ['en', 'ja'].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();
}
####2.4.状態クラス
localeというフィールドしか使わない
import 'package:flutter/material.dart';
class LocaleState extends ChangeNotifier{
Locale _locale;//地域
LocaleState(this._locale);
factory LocaleState.jp()=>
LocaleState(Locale('ja', 'JP'));
factory LocaleState.en()=>
LocaleState(Locale('en', 'US'));
void changeLocaleState(LocaleState state){
_locale=state.locale;
notifyListeners();
}
Locale get locale => _locale; //言語ゲット
}
####2.5.状態の使用、メソッドの実行
Consumer() の他に Consumer2() から Consumer6() まであり、得たい状態値の種類数によって使い分けることができる。
#####2.5.1.多言語化の配置
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:state_dancer/provider/theme_state.dart';
import 'home_page.dart';
import 'provider/locale_state.dart';
import 'provider/I18nDelegate.dart';
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.green,
);
final initIndex = 1;
return MultiProvider(providers: [
ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),//themeのprovider
ChangeNotifierProvider(create: (_) => LocaleState.jp()),//localeのprovider
],child: child,);
}
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>MaterialApp(
title: "状態管理Demo",
localizationsDelegates: [GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
I18nDelegate.delegate//言語デリゲート
],
locale: localState.locale,
supportedLocales: [localState.locale],
debugShowCheckedModeBanner: false,
home: HomePage(),
));
}
}
#####2.5.2.多言語化の実行
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/locale_state.dart';
import 'provider/theme_state.dart';
import 'provider/i18n.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;
return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>Scaffold(
appBar: AppBar(
backgroundColor: themeState.themeData.primaryColor,
title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),),
),
drawer: Drawer(
child: Consumer2<ProviderThemeState,LocaleState>(builder: (_,themeState,localState,__) {
return Column(
children: <Widget>[
Container(
width: screenWidth,
height: statusBarH + naviBarH,
color: themeState.themeData.primaryColor,
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Padding(padding: EdgeInsets.all(10)),
Wrap(children: <Widget>[
RaisedButton(
color: Colors.green,
onPressed: () {
themeState.changeThemeData(ThemeData(primaryColor: Colors.green), 1);
},
child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.red,
onPressed: () {
themeState.changeThemeData(ThemeData(primaryColor: Colors.red), 2);
},
child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),),
shape: CircleBorder(),
),
RaisedButton(
color: Colors.blue,
onPressed: () {
themeState.changeThemeData(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: themeState.themeData.primaryColor,
onPressed: () {
localState.changeLocaleState(LocaleState.jp());
},
child: Text("日本語",style: TextStyle(color: Colors.white),),
),
SizedBox(width: 15.0,),
RaisedButton(
color: themeState.themeData.primaryColor,
onPressed: () {
localState.changeLocaleState(LocaleState.en());
},
child: Text("English",style: TextStyle(color: Colors.white),),
),
],),
],
);
}),
),
body: ListView(
children: <Widget>[
Consumer2<ProviderThemeState,LocaleState>(builder: (_, themeState, localeState,__) {
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 10.0),
Container(
width: 200,
height: 300,
color: themeState.themeData.primaryColor,
),
SizedBox(height: 10.0),
Text(I18N.of(context).analects,
style: TextStyle(
color: themeState.themeData.primaryColor,
fontSize: 18.0,
fontWeight: FontWeight.bold),
),
SizedBox(height: 30.0),
FloatingActionButton(onPressed: (){
},
backgroundColor: themeState.themeData.primaryColor,
child: Icon(Icons.check)),
],
),
);
})
],
),
),
);
}
}
##まとめ
本文ではProviderでテーマや多言語の切替という多状態を管理してみました。Flutterの状態管理はandroid, iOSと違い、多分多くの方が悩み続けていると思いますね。Flutterの状態管理について、代表的にprovider,BLoC,reduxがありますので、次回ではBLoCパターンをご紹介していきたいと思います。