[Flutter/dart] 親WidgetからのリビルドかsetState()によるリビルドかを判別する


概要


StatefulWidgetでページを作っている際、親Widgetからリビルドされたのか、setState()でページ内からリビルドされたのかを判別したいという状況に遭遇しました。


より具体的には、Providerの子Widgetにページを置いて、ある状態が変更されたらそれがページにも反映されるようにしています。

Providerによるリビルドの場合は、状態が変更したということなので、それを反映させるための処理を行いたいです。

一方、setState()による変更は監視している状態が変更したわけではない(例:BottomNavigationのページの切り替わり)ので、この処理は行いたくありません。


(initState()でやれば良いのでは?と思うかもしれませんが、ProviderなどによるリビルドではinitStateは最初の1回だけしか通りません)


このような状況への対応案を記したいと思います。



方法


  1. StatefulWidget内に親Widgetからのリビルドかどうかを示すbool変数を持たせる

  2. 親Widget(Providerなど)からこのStatefulWidgetのコンストラクタを呼ぶときは、このbool変数をtrueにセットする。

  3. buildメソッド内でこの変数の値を参照して所望の処理を行う。その後この変数はfalseにする。

このようにすればビルド時に1度だけ処理を行えば、それ以降のsetState()によるリビルドではこの処理は通りません。



具体例を以下に示します。

このページはBottomNavigationで3つのページを切り替えられるようになっています。

ViewModelはある状態を表すクラスです。ViewModelのインスタンスを元に3つのページを作成します。

このページ作成処理はmodelの内容が変わった場合は行いたいですが、buld()の中にそのまま記述してしまうと、ページが切り替わる度に作成処理が走ってしまい、非効率です。

そこで、上記の対策を行っています。

class basePage extends StatefulWidget{

  ViewModel model;
  bool buildFromParent;

  basePage({this.model, this.buildFromParent});

  @override
  State<StatefulWidget> createState() {
    return basePageState();
  }

}

class basePageState extends State<basePage>{

  List<Widget>_pageList;
  int _pageInd=0;

  @override
  Widget build(BuildContext context) {

    if (widget.buildFromParent) {
      _pageList = [APage(model: widget.model,), 
      BPage(model: widget.model), CPage(model: widget.model,)];
      widget.buildFromParent=false;  //ここ
    }

    return Scaffold(
      body:  _pageList[_pageInd],
      bottomNavigationBar:BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.calendar_today)               
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list),    
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),       
          )
        ],
        currentIndex: _pageInd,
        onTap: (index) {
          setState(() {
            _pageInd = index;
           });
        },
      )
    );
  }

}

このbasePageを呼び出すコードは以下の通りです。buildFromParentはtrueにします。


@override
Widget build(BuildContext context) {
  return StoreProvider<AppState>(
    store: store,
    child: StoreConnector<AppState, ViewModel>(
      distinct: true,
      converter: (store)=>ViewModel.create(store),
      builder: (BuildContext context, ViewModel model)=>
        basePage(model: model, buildFromParent: true,) //ここ
    )
  );
}

自分はreduxで状態管理をしているのでStoreProviderをよく使います。



最後に


本当はconstなどを使ってなるべくリビルドされないようにするというのが最初の対策なのですが、状態に応じてページの表示内容を変えたい場合もあると思います。

また、Providerを使うならばStatelessWidgetで充分という意見もありますが、BottomNavigationやDropdownButtonのようにStatefulWidgetを使わないといけない場合もあります。

そんな時は是非この方法を試してみてください。

最新記事

すべて表示

現象 flutter_local_notificationを使って通知を作成。通知タップ時の動作も登録。 通知が来た時にアプリが立ち上がっている(foregroundでもbackgroundでも)と、期待通りの動作となる。 しかし、通知が来た時にアプリが終了していると、通知をタップしても登録した動作とならない。 原因と解決策 アプリが終了すると登録した動作は消えてしまうらしい(参考)。 Note:

概要 Firebase Messagingでメッセージを送信し、通知がタップされたら何らかの動作を実行する(例:特定のページに移動する)という場合は多いかと思います。 通知タップ時にアプリがforeground状態(つまりアプリを開いている)ならば簡単なのですが、アプリがbackground状態にある場合は少し工夫が必要です。 方法 FirebaseMessaging.onBackgroundMe

現象 FirebaseMessaging.onBackgroundMessage()でタイトルのエラーが発生。 原因と解決策 上記メソッドの引数に渡すデリゲートはトップレベル関数でなければならない。自分の場合はインスタンスメソッドを渡していたのでエラーとなった。 渡すメソッドをトップレベル関数にするとエラーは解消した。 @main.dart void main() async{ //略 F

靴を大切にしよう!靴管理アプリ SHOES_KEEP

納品:iPhone6.5①.png

靴の履いた回数、お手入れ回数を管理するアプリです。

google-play-badge.png
Download_on_the_App_Store_Badge_JP_RGB_blk_100317.png

テーマ日記:テーマを決めてジャンルごとに記録

訂正①2040×1152.jpg

ジャンルごとにテーマ、サブテーマをつけて投稿、記録できる日記アプリです。

google-play-badge.png