5
1

More than 3 years have passed since last update.

【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる①

Last updated at Posted at 2020-07-22

Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。

本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。

一、providerでテーマ切替&多言語化

provider: ^4.3.1  (providerパッケージをpub.devから取得)

1.テーマ切替

ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
color1.pngcolor2.pngcolor3.pngcolor4.pngcolor5.pngcolor6.png

1.1.状態クラス

_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。

changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。

theme_state.dart
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だけでなく他のコンポーネントでもラップやカバーできる。

main.dart
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の状態の状態が更新される。

home_page.dart
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で配置)

ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。

language1.pnglanguage2.pnglanguage3.pnglanguage4.png

2.1.言語データを準備する

language_data.dart
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.言語データを使用するためのクラス

i18n.dart
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.多言語のデリゲートクラス

I18nDelegate.dart
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というフィールドしか使わない

locale_state.dart
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.多言語化の配置
main.dart
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.多言語化の実行
main.dart
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パターンをご紹介していきたいと思います。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1