top of page

[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を使わないといけない場合もあります。

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

最新記事

すべて表示

やりたいこと タイトル通り。 2つのWidgetを画面上で1列に並べたい 1つは画面中央、もう1つは画面右端 RowのmainAxisAligmentでやろうとしたら地味に難しかった 解決策 以下のようにする Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Expanded(

現象 あるアイコンは長押し時に所定の動作を実行します これを実現するために、GestureDetectorのonLongPressに処理を登録していました しかしいざビルドしてみると、アイコンが表示されたタイミングで処理が実行されてしまいました 以下が該当部分のソースコードです。 さあ、どこが間違っているでしょう? return GestureDetector( child: Containe

前提 以下のようなケースを考えます。 アイテムの一覧がある ある1つのアイテムの詳細を表示するページがある 詳細ページではそのアイテムの削除ができる 削除したら他のページ(一覧ページなど)に戻る これはアプリではよくあるパターンだと思います。 課題 reduxパターンを用いている、より具体的にはProviderで状態を管理している場合、以下のような構成になっているのではないでしょうか? アプリ全体

あなたの買い物をサポートする
アプリ Shop Plan

iphone6.5p2.png

​いつ何を買うかの計画を立てられるアプリです。

google-play-badge.png
Download_on_the_App_Store_Badge_JP_RGB_blk_100317.png

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

納品:iPhone6.5①.png

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

google-play-badge.png
Download_on_the_App_Store_Badge_JP_RGB_blk_100317.png

「後で読む」を忘れないアプリ ArticleReminder

気になった​Webサイトを登録し、指定時刻にリマインダを送れるアプリです

google-play-badge.png
Download_on_the_App_Store_Badge_JP_RGB_blk_100317.png
bottom of page