【Flutter】PageStorageでスクロール位置や変数を保持

  • 2024年4月5日
  • 2024年4月5日
  • Widget

対象者

  • Flutterを使用してアプリ開発を行っているシステムエンジニア
  • ページ間での状態管理に関心がある開発者
  • ユーザーエクスペリエンスを向上させたいと考えているアプリ開発者

はじめに

Flutterを使ったアプリ開発において、ユーザーがページ間を移動するたびに状態がリセットされてしまうことに悩んでいませんか?スムーズなユーザーエクスペリエンスを提供するためには、ページ間での状態管理が非常に重要です。そこで役立つのがPageStorageウィジェットです。

PageStorage というのは、日本語で「ページの記憶」という意味です。Flutterにおいては、ウィジェットの状態をページ間で保持するウィジェットです。そのため、ユーザーが異なるページやタブ間を移動しても、以前の状態を保持することができます。

実際のアプリとしては、ユーザーがスクロールした位置を記憶しておきたいリストビューや、フォーム入力の途中状態を保存しておきたい場合などに、PageStorageを使用してそのような機能を実現することができます。

この記事では、PageStorageの基本から応用、さらには注意点まで、わかりやすく解説します。これを読めば、あなたもアプリの状態管理を効率的に行い、ユーザーに快適な操作感を提供できるようになるでしょう。さあ、一緒にFlutterアプリ開発のスキルをさらに磨きましょう!

PageStorageとは

PageStorageの概要

Flutterでは、アプリケーションの状態を管理することが重要です。特に、ユーザーが複数のページやタブ間を移動する際に、それぞれのページの状態を保持しておくことが求められます。このようなニーズに応えるために、FlutterではPageStorageというウィジェットが提供されています。PageStorageは、ウィジェットの状態を保存し、ユーザーがページを離れた後に戻ってきたときに、その状態を復元する役割を果たします。

PageStorageの役割とメリット

PageStorageの主な役割は、ウィジェットの状態をページ間で保持することです。これにより、以下のようなメリットがあります。

  • ユーザーエクスペリエンスの向上: ユーザーがページを離れても、戻ったときに以前の状態が保持されるため、快適な操作感を提供できます。
  • データの保持: ユーザーが入力したフォームのデータやスクロール位置など、重要な情報を保持できます。
  • 効率的なリソース利用: ページの状態を保持することで、再描画や再計算のコストを削減できます。

PageStorageはFlutterアプリケーションにおいて、ユーザーの操作性とデータの保持を向上させる重要な役割を果たします。

PageStorageの応用例

複数のタブやページでの使用

Flutterアプリにおいて、複数のタブやページを持つ場合、ユーザーがタブ間を移動したときに各ページの状態を保持することが重要です。PageStorageウィジェットを使用すると、複数のページ間でウィジェットの状態を簡単に管理できます。

スクロール位置の保持

リストやグリッドなどのスクロール可能なビューにおいて、ユーザーがスクロールした位置を保持し、ページ間の移動後にその位置を復元することがユーザー体験を向上させます。ScrollablePageの例では、ListView.builderを使用してスクロール可能なリストを作成していますが、PageStorageKeyが設定されていないため、スクロール位置は保持されません。PageStorageKeyを設定することで、スクロール位置を保持し、ページ間の移動後に復元できるようになります。

 ListView.builder(
  key: PageStorageKey('ScrollablePage')
  itemCount: 100,
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
),

State内のデータの保持と復元

アプリの状態(State)に含まれるデータ(例えば、カウンターの値)も、PageStorageを使用して保持し、復元することができます。CountUpPageの例では、_incrementCounterメソッドでカウンターを増加させるたびに、その値をPageStorageに保存しています。そして、didChangeDependenciesメソッドでページが再構築される際に、PageStorageからカウンターの値を読み込み、復元しています。PageStorageKeyが設定されているページでは、このデータの保持と復元が行われます。

class _CountUpPageState extends State<CountUpPage> {
  int _counter = 0;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    final previousValue = PageStorage.of(context).readState(context);
    if (previousValue != null && previousValue is int) {
      _counter = previousValue;
    }
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    PageStorage.of(context).writeState(context, _counter);
  }
}

注意点とトラブルシューティング

キーの重要性

PageStorageを使用する際には、各ウィジェットの状態を一意に識別するためのキーを正しく設定することが非常に重要です。これは、PageStorageが状態を保存する際に、キーを基にしてデータを識別し、適切なウィジェットに状態を復元するためです。

final PageStorageKey _key1 = PageStorageKey('page1');
final PageStorageKey _key2 = PageStorageKey('page2');

Widget page1 = Page1(key: _key1);
Widget page2 = Page2(key: _key2);

この例では、Page1Page2のウィジェットに異なるPageStorageKeyを割り当てることで、それぞれの状態を正しく保持できます。

パフォーマンスへの影響

PageStorageを過度に使用すると、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、大量のデータを保存しようとすると、メモリ使用量が増加し、アプリケーションのレスポンスが遅くなることがあります。そのため、必要なデータのみを保存し、不要なデータは保存しないように注意することが重要です。

// 不要な状態を保存しない
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(); // 状態を持たないシンプルなウィジェット
  }
}

この例のように、状態を持たないシンプルなウィジェットは、PageStorageを使用せずに実装することで、パフォーマンスの低下を防ぐことができます。

Q&A

Q1: PageStorageとは何ですか?

A1: PageStorageは、Flutterでウィジェットの状態を保存し、アプリ内の異なるページ間でその状態を保持する役割を果たすウィジェットです。これにより、ユーザーがページを離れても、戻ったときに以前の状態を復元できます。

Q2: PageStorageの主な利用シナリオは何ですか?

A2: PageStorageは、複数のタブやページがあるアプリケーションでよく使用されます。特に、スクロール位置の保持や、ユーザーが入力したフォームのデータなど、状態を保持したいウィジェットに適しています。

Q3: PageStorageを使用する際の注意点は何ですか?

A3: PageStorageを使用する際には、各ウィジェットの状態を一意に識別するためのキーを正しく設定することが重要です。また、大量のデータを保存しすぎるとパフォーマンスに影響を与える可能性があるため、必要なデータのみを保存するように注意が必要です。

まとめ

この記事では、FlutterのPageStorageウィジェットについて学びました。PageStorageは、アプリ内の異なるページ間でウィジェットの状態を保持する役割を果たします。基本的な使い方から応用例まで、様々なシナリオでの使用方法を理解しました。また、キーの重要性やパフォーマンスへの影響など、注意点も確認しました。これにより、ユーザーエクスペリエンスを向上させるための知識を深めることができました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PageStorage Example',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final PageStorageBucket _bucket = PageStorageBucket();

  final _tabs = [
    const CountUpPage(title: 'PageStorageKeyなし'),
    const CountUpPage(
      title: 'PageStorageKeyあり',
      key: PageStorageKey('CountUpPage'),
    ),
    const ScrollablePage(title: 'PageStorageKeyなし'),
    const ScrollablePage(
      title: 'PageStorageKeyあり',
      key: PageStorageKey('ScrollablePage'),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('PageStorage Example'),
          bottom: TabBar(
              tabs: List.generate(
                  _tabs.length, (index) => Tab(text: 'Page $index'))),
        ),
        body: PageStorage(
          bucket: _bucket,
          child: TabBarView(children: _tabs),
        ),
      ),
    );
  }
}

class CountUpPage extends StatefulWidget {
  const CountUpPage({super.key, required this.title});

  final String title;

  @override
  State<CountUpPage> createState() => _CountUpPageState();
}

class _CountUpPageState extends State<CountUpPage> {
  int _counter = 0;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    final previousValue = PageStorage.of(context).readState(context);
    if (previousValue != null && previousValue is int) {
      _counter = previousValue;
    }
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
    PageStorage.of(context).writeState(context, _counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

class ScrollablePage extends StatefulWidget {
  const ScrollablePage({required this.title, super.key});

  final String title;

  @override
  _ScrollablePageState createState() => _ScrollablePageState();
}

class _ScrollablePageState extends State<ScrollablePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: 100,
              itemBuilder: (BuildContext context, int index) {
                return ListTile(
                  title: Text('Item $index'),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

このFlutterサンプルコードでは、PageStoragePageStorageKeyを使用して、タブ間でウィジェットの状態を保持し、復元する方法を示しています。

コードの解説

  1. MyAppクラス: アプリケーションのエントリーポイントで、MaterialAppウィジェットを使用して基本的なアプリ設定を行います。

  2. MyHomePageクラス: ホームページを表すStatefulWidgetで、タブバーとタブビューを含むレイアウトを定義します。

  3. _MyHomePageStateクラス: MyHomePageの状態を管理します。PageStorageBucketインスタンスを作成し、TabBarView内の各ページの状態を保持します。

  4. CountUpPageクラス: カウンターを表示し、ボタン押下でカウンターを増加させるページです。PageStorageを使用してカウンターの値を保存・復元します。

  5. _CountUpPageStateクラス: CountUpPageの状態を管理します。didChangeDependenciesメソッドでPageStorageからカウンターの値を読み込み、_incrementCounterメソッドでカウンターを増加させ、その値をPageStorageに保存します。

  6. ScrollablePageクラス: スクロール可能なリストを表示するページです。ListView.builderを使用してリストアイテムを動的に生成します。

PageStorageとPageStorageKeyの役割

  • PageStorage: ウィジェットの状態を保持するコンテナとして機能します。この例では、PageStorageウィジェットを使用してTabBarViewの子ウィジェットの状態を保持します。

  • PageStorageKey: 各ページの状態を一意に識別するためのキーです。PageStorageKeyが設定されたページ(CountUpPageScrollablePage)では、その状態が保持され、タブ間の移動後に復元されます。

サンプル作成時の気付き

  1. BottomNavigationBarの挙動: IndexedStackBottomNavigationBarを使用したサンプルアプリを作成しましたが、PageStorageを使用しなくてもスクロール位置が復元されました。PageStorageは動作しないことを確認してから対処すれば良い気がします。そのため、PageStorageを使用する前に、実際に動作を確認してみることが重要です。

  2. PageStorageKeyの使用: スクロールバーにおいては、PageStorageKeyを付けるだけでスクロール位置が保持されます。

  3. PageStorageウィジェットの省略: このサンプルではPageStorageウィジェットを明示的に使用していますが、実際には省略しても正常に動作しました。これは、FlutterのウィジェットツリーにデフォルトでPageStorageが含まれているためだと思われます。PageStorage.of(context)を実施すると、インスタンスが取得できました。

  4. 状態変数の復元: スクロール位置は自動的に復元されることがありますが、カウンターのような状態変数は自動的に復元されないため、そのためのコードを書く必要があります。