表示・非表示系のWidgetの使い分けを考えてみる Visibility vs Offstage vs Opacity vs Collection If

# 元質問はこちら

自分がVisibility関連で調べているとStack Overflowで気になる質問を見つけました。

https://stackoverflow.com/questions/62355547/visibility-vs-offstage-vs-opacity-which-is-best-in-hiding-a-child-from-widget-t

表示・非表示できるやり方が

* Opacity
* OffStage
* Visibility
* 条件分岐 (Collection Ifや三行演算子で空白っぽく見せるWidgetを返すなど)

と色々あります。
それぞれのWidgetクラス+αの特徴から基本的な使い分けを比較して自分なりにまとめてみました。

## この記事では触れないこと

以下のことは把握してない、テーマと少し違う、派生系などで触れません。

* VisibilityのSemantic指定できる Semanticに関すること
* Opacityのアニメーション版のAnimatedOpacity

# 試してみた

## 環境
* Flutter 2.5.3 (fvmで指定)
* 最終的なコードはこちら

https://github.com/kentomiya89/sample_show_and_hide/blob/main/lib/show_and_hide_sample/show_and_hide_sample.dart

## 機能

それぞれのWidget+αのコードを書いて、実際にシミュレーターで動きを見て比較しました。
画面上に

* 青・緑・赤のブロックをそれぞれ4列
* Hideをタップすると真ん中の緑が非表示になる
* Showをタップすると真ん中の緑が表示になる
* 表示・非表示のやり方は一番上のテキストに書いてあるやり方で行っている
* 緑のブロックに内にある数字はテキストボタンになっており、タップするごとに1つずつそのタップした数字が加算されていく

の機能を追加して動きを比較しました。

<img width="300" alt="simulator_screenshot_AE028FD0-8576-4EDB-A597-F172245EEF5D.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/20529a61-6449-e95c-d873-a21231e4df08.png">

デモ動画はこちらです。
Opacity, OffStage, Visibility, Collection Ifの緑内ブロックにあるテキストボタンを1回ずつタップして、Hideボタンタップ → Showボタンタップで動きを見ました。

<img width="300" alt="test.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/bf33fcb2-82b4-06a3-0fd2-b527edbda143.gif">

上記の動きに関してそれぞれのWidgetクラス＋αがなぜその挙動をするかに関して、次の章で一つずつ記載していきます。

# Opacity
Opacityは以下の特徴があります。

(Stack Overflowの上記質問の回答者より抜粋)
> This one sets the opacity (alpha) to anything you want. Setting it to 0.0 is just slightly less visible than setting it to 0.1, so hopefully that's easy to understand. The widget will still maintain its size and occupy the same space, and maintain every state, including animations. Since it leaves a gap behind, users can still touch it or click it. (BTW, if you don't want people to touch an invisible button, you can wrap it with an IgnorePointer widget.)

* 透明度を0.0~1.0で設定できる
* 0の場合はその領域を確保する(子Widgetの状態値も維持したまま)

過去のWIDGET of the WEEKの動画でも領域を確保して簡単に非表示できるよと紹介しています。

https://www.youtube.com/watch?v=9hltevOHQBw

なので

* 非表示: 緑のブロックの領域を維持したたまま余白がある形に
* 表示: 0ではなく1の状態のまま

|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/9c578aeb-a719-c79b-2806-68ebb11657c7.png">　|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/9ab94072-61fd-fc96-14a4-93aae77c6f83.png">|
|---|---|
|非表示|表示|

になります。

ちなみにOpacityは非表示でもタップができます。 動画はちょっとわかりづらいですが、非表示の状態で3回タップしています。

<img width="300" alt="名称未設定.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/f835d6c9-e497-5cae-a168-e48abfda1c34.gif">

非表示の場合でタップ判定を消したい場合はIgnorePointerで子Widgetを囲ってあげると防げます。

Opacityを使うときは

* 非表示でも領域を確保したい(ついでに状態も維持)
* 0と1以外の透明度を設定したい(例 0.5とか)

ではないでしょうか。

# OffStage

OffStageは以下の特徴があります。

(Stack Overflowの上記質問の回答者より抜粋)

> This one hides the child widget. You can imagine it as putting the widget "outside of the screen" so users won't see it. The widget still goes through everything in the flutter pipeline, until it arrives at the final "painting" stage, where it doesn't paint anything at all. This means it will maintain all the state and animations, but just won't render anything on the screen. In addition, it also won't occupy any space during layout, leaving no gap behind, and naturally users cannot click it.

* 領域を確保しない(画面内の外側に置いてるような感じ)
* (子Widgetの)状態を維持する

なので

非表示: 緑のブロックの領域を確保せずに上に詰める形で描画
表示: 0ではなく1の状態のまま

|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/665ec1d5-9bec-4f4a-7184-473c80a1b14b.png">　|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/86d7b7b1-c69b-f517-679b-460ea2d44ef2.png">|
|---|---|
|非表示|表示|

になります。

公式ドキュメントより、画面上に描画しないだけで状態を維持するのでアニメーションや非同期処理が走ってしまうようなWidgetの場合は注意です。(画面上に表示されてないのにアニメーションが完了した状態のUIが出てしまうなど)

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

> Animations continue to run in offstage children, and therefore use battery and CPU time, regardless of whether the animations end up being visible.

また、描画しないのでウィジットのサイズを測るのにも使えるらしいですが、いまいちユースケースまでは思い付かず。。。

> Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet)

OffStageを使うときは

* 非表示は領域を確保したくない
* 状態は維持したい

ではないでしょうか。

# Visibility

Visibilityは以下の特徴があります。

(Stack Overflowの上記質問の回答者より抜粋)
> This one combines the above (and more) for your convenience. It has parameters like maintainState, maintainAnimation, maintainSize, maintainInteractivity etc. Depending on how you set those properties, it decides from the following:
> if you want to maintain state, it will either wrap the child with an Opacity or with an Offstage, depends on whether you also want to maintain size. Further, unless you want to maintainInteractivity, it will also wrap an IgnorePointer for you, because clicking on a transparent button is kinda weird.
> if you don't want to maintainState at all, it directly replaces the child with a SizedBox so it's completely gone. You can change the blank SizedBox to anything you want, with the replacement property.

特徴としては単純な表示・非表示もできますが、高機能で色々なプロパティがありOpacityやOffStageの特徴を持たせることができます。

単純な表示・非表示の場合は

非表示: 緑のブロックの領域を確保せずに上に詰める形で描画
表示: 状態を維持せずに初期値の0になる

になります。

|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/33ff81d6-e717-e5ba-3aa8-c0d1ac37456a.png">　|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/3c467aea-a8bd-bfda-bcba-307c874a1bf2.png">|
|---|---|
|非表示|表示|

ただ、Visibilityには色々なプロパティがあり

```Dart
const Visibility({
    Key? key,
    required this.child,
    this.replacement = const SizedBox.shrink(),
    this.visible = true,
    this.maintainState = false,
    this.maintainAnimation = false,
    this.maintainSize = false,
    this.maintainSemantics = false,
    this.maintainInteractivity = false,
  })
```

`maintainState = true`にすると非表示でも状態を維持してOffstageのように、
`maintainAnimation = true, maintainSize= true`にすると領域を確保してOpacityっぽくできます。

さらに、 `maintainInteractivity = true`を設定すると従来のOpacityのようにタップもできるようになります。

また、`replacement`はvisibleがfalseの時に渡したWidgetを代わりに表示するようにします。

`maintainState, maintainAnimation, maintainSize, maintainInteractivity`を全部trueにして動かした動画はこんな感じです。

3回タップ → 非表示 → 表示　で状態の維持を確認　→ 再度非表示 → わかりづらいですが、非表示されているときに複数回タップ → 再表示 → 合計8回
をデモしています。 
<img width="300" alt="test (1).gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80912/aecfc02a-cfbc-479b-1f82-aff98899e794.gif">


次のCollection Ifでも記載しますが、
注意点として公式の記載より、子ウィジットを隠すならVisibilityを使う必要はなく、Widgetツリー上に"含めない"ことがシンプルと記載されています。

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


> 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を使うときは


* 単純な表示・非表示(もできる)
* 凝ったUIを作って複雑な制御をしたい
    * 場合によって状態を維持したい
    * 場合によって領域を確保したい
    * replacementで代わりのUIを表示したい
    * etc....

ではないでしょうか。

# Collection If

こちらはWidgetではなくやり方です。

(Stack Overflowの上記質問の回答者より抜粋)
> If you don't need to maintain states and etc, it's usually recommended to completely remove the widget from the tree. For example, you can use if (condition) to decide whether to include a widget in a list, or use condition ? child : SizedBox() to replace it with a SizedBox directly. This avoid unnecessary calculations and is the best for performance.

条件分岐を使って、シンプルにWidgetツリー上から含めないようにしています。
一番最初のデモ動画の動きでも

非表示: 緑のブロックの領域を確保せずに上に詰める形で描画
表示: 状態を維持せずに初期値の0になる

という動きでシンプルに変更できます。
Visibilityの公式ドキュメントの記載の通り、基本的に表示・非表示するだけなら条件分岐でやる方がいいかもしれません。

# まとめ

表示と非表示以外での違いを以下表にまとめてみました。
需要に合わせて使い分けてみてください！

| |Opacity|OffStage|Visibility| Collection If (条件分岐) |
|:-:|:-:|:-:|:-:|:-:|
| タップ判定ありにしたい |　○　|　×　| maintainInteractivity ＝ trueで○ | × |
| 描画領域を確保したい | ○ | × | maintainAnimation と maintainSize = trueで○ | × |
| 状態を維持する  |  ○ |  ○ | maintainState = trueで○ | × |
| 上記の複雑な制御をしたい | × |  × | ○ |  × |
| 透明度を0・1以外入れたい(例　半透明) | ○ |  × | × |  × |


誤字・脱字、間違いがあれば気軽にご連絡お願いします。

# 他参考にしたもの

https://qiita.com/taiju59/items/555e2e504ec1c83e1232

https://stackoverflow.com/questions/44489804/show-hide-widgets-in-flutter-programmatically/53044562#53044562

https://itome.team/blog/2020/01/flutter-with-dart27/



# おまけ 1
元気があったら対話型UIをCollection Ifで使った例書きます。

# おまけ 2
元気があったら下タブを残すVisibilityを使った例書きます。