🫥

Visibility ウィジェットを深掘りしてみる(Flutter)

2024/03/08に公開

はじめに

Flutterの Visibility ウィジェットのリファレンスを見てみると、結構使ったことのないプロパティがあったので以下について実際に触りながら調べてみました

  • いつ使うのか、それぞれの役割
  • Visibilityを使うタイミング

https://api.flutter.dev/flutter/widgets/Visibility-class.html

Visibility とは?

公式には以下のように記載されています。

Whether to show or hide a child.
By default, the visible property controls whether the child is included in the subtree or not; when it is not visible, the replacement child (typically a zero-sized box) is included instead.
A variety of flags can be used to tweak exactly how the child is hidden. (Changing the flags dynamically is discouraged, as it can cause the child subtree to be rebuilt, with any state in the subtree being discarded. Typically, only the visible flag is changed dynamically.)

(DeepL翻訳)
子供を見せるか隠すか。
デフォルトでは、visibleプロパティは、子プロパティがサブツリーに含まれるかどうかを制御します; 子プロパティが表示されない場合、代わりに置換子(通常はゼロサイズのボックス)が含まれます。
さまざまなフラグを使用して、子がどのように隠されるかを正確に調整することができます。(フラグを動的に変更することは推奨されません。子サブツリーが再構築され、サブツリーの状態が破棄される可能性があるからです。通常、可視フラグだけが動的に変更されます)。

様々な記事で紹介されていますので詳細は割愛させていただきますが、以下のような特徴を持つWidgetであることが読み取れます。

  • child で指定したウィジェットの表示/非表示を visible プロパティで切り替える
  • 非表示は指定がなければ zero-sized box で表現される
  • フラグの指定によって、様々な「非表示の方法」を実現できる

今回は フラグの指定によって、様々な「非表示の方法」を実現できる 点について深掘りしていきます。

今回取り上げるプロパティ

今回取り上げるプロパティは以下です。「設定したらどうなるの?」と疑問に思ったものに絞りました。

  • maintainState
  • maintainInteractivity
  • maintainSemantics
  • maintainAnimation

それぞれについて、デモアプリを作ってみました。
デモアプリは公開しています。
https://github.com/Yuta-KTD/visibility_demo

maintainState

maintainState プロパティは、Visibility ウィジェットが非表示にされている時でも、その子ウィジェットの状態を保持するかどうかを制御します。これが true に設定されている場合、子ウィジェットは画面からは消えますが、内部の状態(例えばテキストフィールドの内容やスクロール位置)は保持されます。これは、ユーザーが操作を再開した際に前の状態から容易に再開できるようにするために有用です。

使用例: 利用規約画面などの長いスクロール画面で、表示・非表示を切り替えたい時

maintainState: falseの場合
非表示時にスクロール位置が保持されていないのがわかります。
maintainStateFlase

maintainState: trueの場合
非表示時にスクロール位置が保持されているのがわかります。
maintainStateTrue

実装
maintain_state_page.dart
Expanded(
    child: Visibility(
        maintainState: _maintainState, // スクロール位置を保持する
        visible: _isVisible,
        child: ListView.builder(
            itemCount: 100, // 長いリストを生成
            itemBuilder: (context, index) {
                return ListTile(
                title: Text('利用規約的な文章 $index'),
                );
            },
        ),
    ),
),

maintainInteractivity

maintainInteractivity プロパティは、ウィジェットが視覚的には非表示であっても、ユーザーの操作(タップやスクロールなど)を受け付けるかどうかを制御します。
ちなみに、詳細ページを読んでみると、maintainInteractivitytrue にしたいときは、 maintainSizetrue にする必要があります。そして maintainSizetrue にするためには maintainAnimationtrue である必要があったりと芋づる式に true が必要になっています。
Visibility は、それぞれの maintain プロパティによって最終的に使用するウィジェットが分かれており、それによって必須要素が変わるのでこのような制約がいくつかあります。

使用例: いい案が思いつかなかったです。実際にこんな用途で使っているよ!という方がいたら教えて欲しいです!

maintainInteractivity: trueにすると、非表示時でもボタンのタップができることがわかります。

maintainInteractivity

実装
maintain_interactivity_page.dart
Visibility(
    maintainSize: true, // maintainAnimation: trueが必要
    maintainAnimation: true, // maintainState: trueが必要
    maintainState: true,
    maintainInteractivity: true, // maintainSize: trueが必要
    visible: _isButtonVisible,
    child: ElevatedButton(
        onPressed: _incrementCounter,
        child: const Text('Increment'),
    ),
),

maintainSemantics

maintainSemantics プロパティは、ウィジェットが非表示になっても、アクセシビリティツール(スクリーンリーダーなど)によるそのウィジェットの認識を維持するかどうかを制御します。

使用例: 視覚障害のあるユーザーが使用するアプリで、一時的にコンテンツを非表示にするが、その情報をスクリーンリーダーを通じて引き続き提供したい場合

find.bySemanticsLabel() でテストすることで、 maintainSemantics: true が想定通りに動くことが確認できました。

実装
maintain_semantics_text.dart
  Widget build(BuildContext context) {
    return Visibility(
      visible: false,
      maintainSemantics: true,
      maintainSize: true,
      maintainAnimation: true,
      maintainState: true,
      child: Text(
        text,
        semanticsLabel: text,
      ),
    );
  }
maintain_semantics_text_test.dart
expect(find.bySemanticsLabel('Semantics!'), findsOneWidget);

maintainAnimation

maintainAnimation プロパティは、ウィジェットが非表示になっている間も、その子のアニメーションを維持し続けるかを制御します。
AnimationController を使って何か別のロジックを動かす際、アニメーションが非表示の場合にも継続して実行したい場合には maintainAnimation: true を設定してあげる必要があります。
例えばanimationController.addListener()の中で実行する処理などが該当します。

使用例: アニメーションの経過時間を非表示状態でも保持し続けたい場合?いい例が思いつかなかったです...

true の方が挙動が直感的でわかりやすい気がするので true から紹介します。

maintainAnimation: trueの場合
アニメーションが非表示の時にカウンターの数値が動き続けていることがわかります。
maintainStateTrue

maintainAnimation: falseの場合
ちょっとわかりづらいのですが、アニメーションが非表示の時にカウンターの数値が止まっていることがわかります。
アニメーション停止時のカウンターの数値が true の時と異なります。
maintainStateFlase

実装
maintain_animation_page.dart
Visibility(
    visible: _isVisible,
    maintainState: true,
    maintainAnimation: true,
    child: const FadeLogo(),
),
fade_logo.dart
class _FadeLogoState extends State<FadeLogo>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  int _counter = 0;

  
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 10),
    );
    _animationController.forward();
    _animationController.addListener(() {
      setState(() {
        _counter++;
      });
    });
  }

  
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _restartAnimation() {
    setState(() {
      _counter = 0;
    });
    _animationController.reset();
    _animationController.forward();
  }

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        FadeTransition(
          opacity: _animationController,
          child: const FlutterLogo(size: 200.0),
        ),
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: _restartAnimation,
          child: const Text('Restart Animation'),
        ),
        const SizedBox(height: 20),
        Text(
          '_counter: $_counter',
          style: Theme.of(context).textTheme.displaySmall,
        ),
      ],
    );
  }
}

いつ Visibility を使うのか

私は、 Visibility ウィジェットは「ウィジェットの非表示に何か別の制御を加えたい時」に使用するのが適切かと考えます。
公式にもそのようなニュアンスの内容が記載されています。

Using this widget is not necessary to hide children. The simplest way to hide a child is just to not include it, or, if a child must be given (e.g. because the parent is a StatelessWidget) then to use SizedBox.shrink instead of the child that would otherwise be included.

ウィジェットの表示・非表示を制御しながら、自分の思い通りに状態を管理する際には考慮するべきことがたくさんあります。
その手助けをしてくれるのが Visibility ウィジェットということですね。

まとめ

内部実装にも入り込んで紹介したい点もいくつかありましたが、思ったより長くなったのでそれぞれのプロパティ紹介に留めさせていただきました。
こういったタイミングで一度深掘りしておくと、いざという時に「今このウィジェット使えるかも?」となるのがいいですよね。
個人的には maintainAnimation 周りは想定していた挙動と異なっていたので学びがありました。

Discussion