ウィジェットの State を保存する PageStorage の使い方

ウィジェットの State (状態) とは?

ステートフル・ウィジェット (StatefulWidget) では、State クラスがウィジェットの状態を保持することになっています。 State に変更を加える時 (あるいは変更した時) に State クラスの setState メソッドを呼ぶことで、 その変更を Flutter フレームワークに通知します。

これにより、フレームワークはウィジェット (サブ) ツリーを再構築し、その結果ユーザーインターフェイスが更新されます。

State が破棄されると、UI の状態もリセットされる

上述のように State は UI の状態を保持するので、フレームワークによって State が破棄されれば、 UI の状態もクリアされてしまいます。

基本的にあるページから次のページに移動したタイミングで State は破棄されるので、通常はそれで問題ないのですが、時に問題が発生する場合もあります。

例えば、次の画面のように TabBarView で、はじめの画面で何か入力して、 ちょっとお隣のページにちょっとスライドして移動して、すぐに戻ってきた時に、もとの画面の入力が消えているのでは、違和感がありますよね。

期待する動作は、次のように元の入力内容が維持されていることでしょう。

ここでは、実際にこの画面を作成して、どのように対策するか具体的に説明します。

最初の画面作り 〜 TabBarView

このように横スライドする画面は TabBar と TabBarView で作ります。

TabBar と TabBarView については次のページも参考にしてください。

タブでスクリーンを切替える TabBarView の利用方法

アプリケーションのメインのアプリケーションクラスは次の通りです。

main.dart

import 'package:flutter/material.dart';
import './page1.dart';
import './page2.dart';
import './page3.dart';

void main() => runApp(
      MaterialApp(
        debugShowCheckedModeBanner: false,
        home: MyApp(),
      ),
    );

class TabInfo {
  String label;
  Widget widget;
  TabInfo(this.label, this.widget);
}

class MyApp extends StatelessWidget {
  final List<TabInfo> _tabs = [
    TabInfo("FIRST", Page1()),
    TabInfo("SECOND", Page2()),
    TabInfo("THIRD", Page3()),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Tab Controller'),
          bottom: PreferredSize(
            child: TabBar(
              isScrollable: true,
              tabs: _tabs.map((TabInfo tab) {
                return Tab(text: tab.label);
              }).toList(),
            ),
            preferredSize: Size.fromHeight(30.0),
          ),
        ),
        body: TabBarView(children: _tabs.map((tab) => tab.widget).toList()),
      ),
    );
  }
}

page1.dart

ここで Text ウィジェットと IconButton ウィジェットを並べて、 カウンターを表示しています。

import 'package:flutter/material.dart';


class Page1 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _Page1State();
  }
}

class Page1Params {
  int counter1 = 0;
  int counter2 = 0;
}

class _Page1State extends State<Page1> {
  Page1Params _params = Page1Params();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Text(
            '${_params.counter1}',
            style: TextStyle(
              fontSize: 48,
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.remove, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter1--;
                  });
                },
              ),
              IconButton(
                icon: Icon(Icons.add, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter1++;
                  });
                },
              ),
            ],
          ),
          Text(
            '${_params.counter2}',
            style: TextStyle(
              fontSize: 48,
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.remove, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter2--;
                  });
                },
              ),
              IconButton(
                icon: Icon(Icons.add, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter2++;
                  });
                },
              ),
            ],
          )
        ],
      ),
    );
  }
}

page2.dart と page3.dart

Page2Page3 は次のようなコードです。ページを移動するために用意しただけなので、何もしないので何でも構いません。

import 'package:flutter/material.dart';

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Page3 はクラス名を Page3 としてください。

ここまで作り、上で見たように次のような画面になれば OK です。

さて、これをデバッグしていきましょう。

PageStorage の利用方法

ウィジェットの State 保存維持のために、Flutter フレームワークでは PageStorage クラスが用意されています。

使い方は簡単です。ここでは Page1 で PageStorage を使いたいので、次の手順で PageStorage を組み込みます。

1. キーを受け取るコンストラクタを作成

Page1 ウィジェットでコンストラクタを作成して、キーを受け取ります。page1.dart 内の Page1 クラスにコンストラクタを追加します。

class Page1 extends StatefulWidget {
  Page1({Key key}) : super(key: key); //この行を追加

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

受け取ったキーは直ちに super に渡すだけで OK です。これによって、 このキーに関連付けされたストレージが用意されます。

2. Page1 作成時にキーを渡す

main.dartPage1 を作成する時に、ページストレージキー (PageStorageKey) を渡します。

class MyApp extends StatelessWidget {
  final List<TabInfo> _tabs = [
    TabInfo(
      "FIRST", 
      Page1(key: PageStorageKey<String>("key_Page1"))
    ), // ここでキーを渡している
    TabInfo("SECOND", Page2()),
    TabInfo("THIRD", Page3()),
  ];

キーに設定する文字列は一意になるようにすれば、何でも構いません。

3. PageStorage を読み取る

page1.dart 内の Page1State クラスで、 didChangeDependencies メソッドをオーバーライドします。

class _Page1State extends State<Page1> {
  Page1Params _params;

  @override
  void didChangeDependencies() { // このメソッドをオーバーライド
    Page1Params p = PageStorage.of(context).readState(context);
    if (p != null) {
      _params = p;
    } else {
      _params = Page1Params();
    }
    super.didChangeDependencies();
  }

これはステートフルウィジェットのライフサイクルで initState の次に呼びされるメソッドです。 ここで State の初期化をします。

readState メソッドでストレージから読み取りますが、何も書き込まれていなければ null が返ります。

4. PageStorage に書き込む

同じく Page1State クラス内の build メソッドで、 UI の状態を保持するオブジェクト (ここでは Page1Params というオブジェクトにまとめています) の変更後、writeState を呼び出して、 PageStorage に書き込んでいます。

  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Text(
            '${_params.counter1}',
            style: TextStyle(
              fontSize: 48,
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.remove, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter1--;
                  });
                  PageStorage.of(context).writeState(context, _params);
                },
              ),
              IconButton(
                icon: Icon(Icons.add, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter1++;
                  });
                  PageStorage.of(context).writeState(context, _params);
                },
              ),
            ],
          ),
          Text(
            '${_params.counter2}',
            style: TextStyle(
              fontSize: 48,
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.remove, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter2--;
                  });
                  PageStorage.of(context).writeState(context, _params);
                },
              ),
              IconButton(
                icon: Icon(Icons.add, size: 32.0),
                onPressed: () {
                  setState(() {
                    _params.counter2++;
                  });
                  PageStorage.of(context).writeState(context, _params);
                },
              ),
            ],
          )
        ],
      ),
    );
  }

これで終わりです。これで次のように状態が維持 (実際は復元) されます。

手順のおさらい

コード例を示したので長々となりましたが、まとめるとステップは次の通りです。

  1. StatefulWidget クラスのコンストラクタでキーを受け取り super に渡す
  2. ウィジェット作成時にキーを渡す
  3. readState でページストレージを読み取る (didChangeDependencies() メソッドのオーバーライド)
  4. writeState でページストレージに書き込む (随時)

以上で、State を保持する PageStorage の使用方法について説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Flutter 入門