Roadmap.shで学ぶFlutter - Responsive Widgets -

# 導入
元々Flutterを仕事で使っていましたが、転職後、特に業務で使うことがなかったので、確実に理解が薄れている。勉強せねば、、、
ということで、roadmap.shというサイトを基に自分がちゃんと理解できていないFlutterの要素について学習していくことにしました。
そのシリーズになります。

https://roadmap.sh/flutter

# Responsive Widgets
## 対象ページ
- 公式サイト

https://docs.flutter.dev/ui/adaptive-responsive

## 概要

<details><summary>日本語訳</summary>

Flutterの主な目標の一つは、どのプラットフォームでも素晴らしい見た目と操作感を提供するアプリを、単一のコードベースから開発できるフレームワークを作ることです。

つまり、あなたのアプリは時計から二画面の折りたたみ式の電話、高解像度のモニターまで、さまざまなサイズの画面に表示される可能性があります。また、入力デバイスは物理キーボードや仮想キーボード、マウス、タッチスクリーン、その他多数のデバイスである可能性があります。

これらのデザインコンセプトを表す用語に、アダプティブ（適応）とレスポンシブ（応答）が含まれます。理想的には、アプリは両方であるべきですが、具体的にそれがどういう意味かを理解することが重要です。

### レスポンシブとアダプティブの違いとは？

簡単に考えると、レスポンシブデザインはUIをスペースに適合させること、アダプティブデザインはUIをスペース内で使いやすくすることです。

つまり、レスポンシブなアプリは、利用可能なスペースに合わせてデザイン要素の配置を調整します。一方、アダプティブなアプリは、利用可能なスペース内で使いやすいレイアウトと入力デバイスを選択します。例えば、タブレットのUIでは、ボトムナビゲーションを使用すべきか、サイドパネルナビゲーションを使用すべきか、といった選択が考えられます。

:::note warn
多くの場合、アダプティブとレスポンシブの概念は単一の用語にまとめられます。ほとんどの場合、アダプティブデザインが両方の概念を指すために使用されます。
:::

</details>

### 要点
Flutterには2つのデザインアプローチがある。
- レスポンシブ
- アダプティブ

前者はUIをスペースに合わせること、後者はスペースで使いやすいUIを提供することを意味する。
両方の要素を持つことが理想的である。

### 整理
`レスポンシブ`というのは`UIをスペースに合わせる`、つまり<font color="red">UIは端末・プラットフォームに依存せず、全て同じものを利用し、スペースに合わせてサイズを変更する</font>というもの。

一方、`アダプティブ`は`スペースで使いやすいUIを提供する`、つまり<font color="red">端末・プラットフォーム毎に適切で使いやすいUIを提供する</font>というもの。

この2点の違いを理解することが重要です。

## 詳細ページの内容
さらに詳細ページを読んでいきます。
要約した内容です。

### [General approach](https://docs.flutter.dev/ui/adaptive-responsive/general)

- 従来のモバイルデバイス向けアプリを様々なデバイスで美しく表示させるには。
    - Googleのエンジニアによる3ステップ
        - ステップ1: 抽象化する
            - 動的にするウィジェットを特定し、再利用(share)可能なデータを抽象化していく
            - 例:
              - ダイアログウィジェットでダイアログの内容を再利用
              - 小さな画面ではナビゲーションバー、大きな画面ではナビゲーションレールに切り替える
        - ステップ2: (画面サイズなどを)測定する
            - MediaQueryの利用
              - アプリウィンドウの全体サイズを取得する
              - フルスクリーン表示が必要な場合に使用する
              - 過去の推奨方法(MediaQuery.of)よりも効率的なsizeOfメソッドを使用する
            - LayoutBuilderの利用
              - 親ウィジェットからのレイアウト制約を提供する
              - カスタムウィジェットのサイズ情報を取得する場合に有用
        - ステップ3: 分岐する
            - UIのバージョンを選択するためのサイズブレークポイントを決定
              - 例: 幅が600論理ピクセル未満のウィンドウにはボトムナビゲーションバー、600ピクセル以上にはナビゲーションレールを使用

### [SafeArea & MediaQuery](https://docs.flutter.dev/ui/adaptive-responsive/safearea-mediaquery#safearea)

- SafeAreaとMediaQueryの使い方と使う場面について
    - **SafeArea**
      - 最新デバイスでアプリを実行すると、画面の切り欠き(カットアウト)などによって**UIの一部がブロックされる**ことがある
          - スマホアプリを開発したことがある人はわかると思うが、Android/iPhoneともに上部のステータスバーにエリアにまで、Widgetが食い込んだりする現象のことと思われる
      - SafeAreaウィジェットは、切り欠き(カットアウト)やカメラの部分、ステータスバーなどを避けるように子ウィジェットのインセットを調整する
      - デフォルトでは四方向すべてにパディングが適用される。
          - 任意の方向のパディングを無効にすることもできる。
      - 一般的には、ScaffoldウィジェットのbodyをSafeAreaでラップすることが推奨されるが、必ずしもツリーレベルで最上部に配置する必要はない。
      - SafeAreaを使用すると、アプリのコンテンツが物理的なディスプレイの特徴やOSのUIによって切り取られることがない。
      - SafeAreaは、MediaQueryオブジェクトを内部で使用している。
    - **MediaQuery**
      - アダプティブアプリを作成するためのウィジェット
      - 前項で触れた通り、MediaQueryは、アプリの現在のウィンドウサイズ、アクセシビリティ設定（高コントラストモードやテキストスケーリング）、デバイスのディスプレイの特徴（ヒンジや折り畳み）などの情報を提供する
      - SafeAreaは、MediaQueryのデータを使用して子ウィジェットのインセット量を決定している
      - MediaQuery.paddingプロパティは、システムUIやディスプレイの切り欠き、ステータスバーによって部分的に隠されているエリアのサイズを示す
          - **結構重要かも**
      - SafeAreaは、子ウィジェットに適用されるMediaQueryのパディングを削除して、複数のSafeAreaがネストされてもトップレベルのSafeAreaのみがパディングを適用するようにしてくれる
      - SafeAreaを使用すると、ウィジェットを移動したり、複数のSafeAreaが存在しても過剰なパディングが適用される心配がない
    - **推奨使用方法**
      - ScaffoldウィジェットのbodyをSafeAreaでラップすることが推奨される
          - 全体をラップする必要はない
      - アプリが切り欠き(カットアウト)の下に伸びるようにする場合は、SafeAreaを適切なコンテンツにのみ適用し、他の部分をフルスクリーンで表示させる
      - <font color="blue">AppBarウィジェットはデフォルトでステータスバーの下に配置される</font>ため、Scaffoldの全体をラップするのではなく、body部分のみをラップするのが推奨される

### [Large screen devices](https://docs.flutter.dev/ui/adaptive-responsive/large-screens)

- 大画面デバイスの最適化
    - 大画面デバイスについて
        - タブレット、折りたたみデバイス、ChromeOSデバイス、ウェブ、デスクトップ、iPadなど
        - 2024年1月時点で、2億7000万以上の大画面デバイスと折りたたみデバイスがAndroid上で稼働し、1490万以上のiPadユーザーがいる。
    - 大画面デバイスをサポートするメリット
        - ユーザーエンゲージメントの向上
        - Playストアでのアプリの視認性向上
        - iPadOS提出ガイドラインを満たし、App Storeで受理される
    - 大画面デバイスにおける課題
        - テキストやボックスが画面全体(画面幅いっぱい)を占めるのはガイドライン上不適切
            - 解決策: GridViewの利用
            - ListViewをGridViewに変更し、アイテムを2次元配列で配置できる
            - デザイン調整に利用できる仕組み
                - GridView.count: 固定列数を指定
                - GridView.builder: アイテム数が多い場合に利用
                - gridDelegate設定
                    - SliverGridDelegateWithFixedCrossAxisCount: 列数を指定
                    - SliverGridDelegateWithMaxCrossAxisExtent: 最大アイテム幅を指定
        - 折りたたみデバイスの折りたたみ時と展開時の画面表示の不一致
            - 解決策
                - すべての画面向きをサポート
                - Flutter 3.13で導入されたDisplay APIを使用して物理画面サイズを取得し、利用する
    
    - アダプティブ入力
        - Tier3: Basic
            - 大画面に対応する
                - キーボード、マウス、トラックパッド、タッチペンなどの外部入力デバイスの基本的なサポート(入力サポート拡張)
        - Tier2: Better
            - 大画面向け最適化
                - すべての画面サイズとデバイス構成に対するレイアウトの最適化
                - 外部入力の強化
        - Tier1: Best
            - 大画面によって差別化
                - タブレット、折りたたみ式デバイス、ChromeOS デバイス向けに設計されたUXを提供
                - マルチタスク、折りたたみ式デバイスの形状、ドラッグ＆ドロップ、タッチペンによる入力をサポート

### [User input & accessibility](https://docs.flutter.dev/ui/adaptive-responsive/input)
- 多様なユーザー入力のサポート
    - アプリの外観だけでなく、様々な**ユーザー入力もサポート**する必要がある。
    - マウスとキーボードの入力には、スクロールホイール、右クリック、ホバー、タブ移動、キーボードショートカットが含まれる。
    - サポートすべき項目
        - カスタムウィジェットにおける**スクロールホイール**のサポート
            - `ScrollView` や `ListView` はデフォルトでサポートしている
            - カスタムスクロール動作を実装する場合、`Listener` ウィジェットを使用する
        - タブ移動とフォーカスの相互作用
            - 物理キーボードを持つユーザーは、`Tabキー`を使ってアプリ内を素早く移動できることを期待する
            - **`FocusableActionDetector`** ウィジェットで、フォーカスやホバーのハイライトを提供する
        - タブ移動順序の制御
            - **`FocusTraversalGroup`** を使用して、ウィジェットのタブ移動順序を制御できる
            - デフォルトの **`ReadingOrderTraversalPolicy`** をカスタマイズすることも可能
        - キーボードショートカットの実装
            - **`KeyboardListener`** や **`Focus`** ウィジェットを使用して、キーボードイベントをリスンする
            - **`Shortcuts`** ウィジェットを使用して、ショートカットキーを定義し、アクションにバインドする
            - グローバルリスナーを使用して、常に有効なショートカットを実装することも可能

        - カスタムウィジェットにおけるMouse Enter/Exit/Hoverの処理
            - `MouseRegion` ウィジェットを使用して、カスタムウィジェットのマウスカーソルを変更したり、ホバー効果を追加する
        - 視覚的密度の調整
            - デバイスによってタッチ・クリックの精度のレベルやヒット領域のサイズが異なる
            - タッチスクリーンを考慮して、ウィジェットの「ヒットエリア」を拡大する。
            - `VisualDensity` クラスを使用して、ビューの密度をアプリ全体で調整することが可能
            - `MaterialApp` テーマに密度を注入してカスタマイズできる
                ~~~dart
                double densityAmt = touchMode ? 0.0 : -1.0;
                VisualDensity density =
                        VisualDensity(horizontal: densityAmt, vertical: densityAmt);
                return MaterialApp(
                    theme: ThemeData(visualDensity: density),
                    home: MainAppScaffold(),
                    debugShowCheckedModeBanner: false,
                );
                ~~~

### [Capabilities & policies](https://docs.flutter.dev/ui/adaptive-responsive/capabilities)

- **デバイスの強みを活かしたデザイン**
    - 各デバイスの特性や制約を考慮してデザインを行う
    - 例: AppleのApp StoreとGoogleのPlay Storeのガイドラインが異なる
    - Webアプリの場合、リンクの深度を考慮したナビゲーションルートを設計する
- **Capabilities（機能）**
    - コードやデバイスが**できること**を定義する
    - 例: APIの存在、OSの制限、物理的なハードウェア要件（カメラなど）
- **Policies（方針）**
  - コードが**すべきこと**を定義する
  - 例: アプリストアのガイドライン、デザインの好み、ホストデバイスに関連する資産やコピー、サーバーサイドで有効化される機能

- **ポリシーコードの構造化**
    - Platform.isAndroid、Platform.isIOS、kIsWebなどのAPIを使用する
    - レイアウトの決定やデバイスの能力に関する仮定は避ける
        - 例: iOSデバイスで購入リンクを表示しないコードの実装
    - クラスを用いたポリシーの管理
        - ポリシーや機能に基づくクラスを作成し、コードの分岐理由を明確にする
        - クラス化することで、テストがしやすくなる
            - 機能やポリシーをモックして、ウィジェットテストが変更に影響されないようにする
    - ポリシーと機能の分離
        - アプリの能力(Capabilities)とすべきこと(Policy)を論理的に区別することで、大規模な製品がプラットフォームの変更に対応しやすくなる

### [Best practices for adaptive design](https://docs.flutter.dev/ui/adaptive-responsive/best-practices)

**Adaptive Designのベストプラクティス**

1. **ウィジェットの分割**
   - 大きく複雑なウィジェットを小さくシンプルなウィジェットに分割する
   - コードの共有やパフォーマンス改善、読みやすさの向上に役立つ

2. **各フォームファクターの強みを活かす**
   - デバイスごとに異なる機能・特徴を考慮し、最適なUIを設計する
       - モバイル: コンテンツのキャプチャと位置データのタグ付けすることに焦点を当てる
       - タブレットやデスクトップ: コンテンツの整理や操作に焦点を当てる

3. **タッチUIを優先**
   - タッチデバイス向けのUIをまず設計し、その後にマウスやキーボードの入力を考慮する
   - デスクトップターゲットでのテストを行いながら、モバイルデバイスでの**感触を確認**する

4. **アプリの向きを固定しない**
   - 異なるサイズや形のウィンドウでアプリが見栄えよく表示されるようにする
   - マルチウィンドウサポートや折りたたみデバイスに対応する

5. **ハードウェアタイプのチェックを避ける**
   - デバイスの種類に依存せず、ウィンドウサイズに基づいてレイアウトを決定する

6. **さまざまな入力デバイスをサポート**
   - マウス、トラックパッド、キーボードショートカットなどをサポートする
   - キーボードナビゲーションのアクセシビリティベストプラクティスに従う

7. **リストの状態を復元**
   - デバイスの向きが変わった際にリストのスクロール位置を維持するために`PageStorageKey`クラスを使用する

8. **アプリの状態を保存**
   - デバイスの回転やウィンドウサイズの変更、折りたたみ/展開時にアプリの状態を保持または復元する
   - プラグインやネイティブ拡張機能がデバイスの種類をサポートしているか確認する

# 終わりに

今回の学習では、Flutterのアダプティブなデザインについて深く掘り下げました。
Flutterは単一のコードベースから多様なデバイスに対応したアプリケーションを作成するための強力なフレームワークであり、そのデザインアプローチであるレスポンシブとアダプティブを理解することは非常に重要に感じました。
特に、画面サイズに応じたレイアウトの調整や、様々な入力デバイスへの対応は特に気をつけるべきだと感じました。
ただ、考えることが多いなぁ...とも思いました。