UI Pocketで、FlutterとFigmaとデザイン、まとめて学習しちゃおう

# 概要
**UI Pocket**というサービスを活用した学習方法について話をさせていただきます。
デサインに興味がある方には、ぜひ知ってほしいサービスです！
タイトルに記載の通りですが、
**UI Pocketを通じて、以下に対する理解を深められると感じている最中です。**
- Flutter
- Figma
- デザイン

##### Flutterの経験について
業務経験はないですが、モバイル開発に興味があり、1年弱さわっています。

## UI Pocketとは

https://www.ui-pocket.com/mobile/apps

一言でいえば、**UIカタログ**、でしょうか。
様々なアプリのスクリーンショットを無料で閲覧できるサービスです。（無料で利用可能なコンテンツあり）

トップ画面です。（PC表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/7ad8fd6a-791d-78c9-afce-101be3a7c8f6.png)
<details><summary>モバイルの場合は、画面下の方にタブバーで表示</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/55114a56-e489-05da-5cf2-35c8084d797d.png)
</details>

以下3つの機能は、無料プランでは制限があり、全画像の閲覧はできません。制限解除には、**Proプランに加入が必要です。**
といっても、月額480円です。本格的に利用される方にとっては、安いものかと思いました！
これらの機能も利用することで、作成したいUI構築に役立てることが可能になります。
- UIコンポーネント
- UIパターン
- フロー

### 各機能の紹介（ざっくり）
#### アプリ
ここは冒頭で記載した通り、閲覧可能なアプリ一覧が並んでいます。
**ここは無料ユーザでも一通り閲覧可能です。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/89465de1-2e4f-528a-36a4-ab21ff644814.png)

#### UIコンポーネント
テキストフィールドやボタンなど、特定の要素ごとのデザインをサービスを跨いで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/40d264ea-daaf-0b40-e3cc-a2c72ff4b9f4.png)
#### UIパターン
様々なサービスを跨いで、特定の機能に関する情報を一度に閲覧できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/abc756f3-eadf-2350-e54c-0bee9df41b6b.png)
#### フロー
特定サービスの特定機能にフィルタリングした状態で、一連の流れを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/8ab38c72-3a0a-ffbd-0da4-7731522aae5a.png)

## 本題へ
以下3つの方法を用いて、学習を進めています。
（FlutterやFigmaの基本的な話は特に記載していません。）
1. **UI Pocketの画像を見て、Figma上でUIを作成して、プラグインでコード出力**
1. **UI Pocketの画像をClaudeにアップロードして、コード提供してもらう**
1. **UI Pocketの画像を見て、実際にコード実装しながら、適宜ネットやClaudeを利用**

基本的には、UIPocketで提供されている[タブとフォームがある画面（「口座情報」でログイン）](https://www.ui-pocket.com/mobile/apps/smbc/2025-01#localizedFlow3455)を参照して進めました。

### 1. UI Pocketの画像を見て、Figma上でUIを作成して、プラグインでコード出力
**Figmaの理解と利用しやすいコードを出力するための作り方を理解**
Figmaの操作方法を知るためにも使っています。
私はプラン加入していないので、開発モードは使えません。UI Pocketの特定画像を見ながら、Figma上で作成を行います。作成完了後に、プラグインでコード生成を行います。

利用者数が多い順に2つのプラグインで確認しました。
#### Figma to Code (HTML, Tailwind, Flutter, SwiftUI)

https://www.figma.com/community/plugin/842128343887142055/figma-to-code-html-tailwind-flutter-swiftui

Figma上で作成して、コードを出力しました。
しかしそのコードには、[TabBarコンポーネント](https://api.flutter.dev/flutter/material/TabBar-class.html)や[Formコンポーネント](https://api.flutter.dev/flutter/widgets/Form-class.html)が存在しませんでした。
その後、エディター上のエラー解消後に実端末で確認しましたが、**OVERFLOWEDが発生**しました。

<details><summary>実端末確認時のスクリーンショット</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/48a74e63-9263-fff5-7c42-1b7a3dddf0e6.png)
</details>

#### FigmaToFlutter

https://www.figma.com/community/plugin/844008530039534144/figmatoflutter

1つ目のプラグインと同じ成果物を利用しましたが、
プラグインが立ち上がりませんでした。
[Material 3 Design Kitで提供されたExample](https://www.figma.com/community/file/1035203688168086460)では、以下ウインドウが表示されました。私の成果物がプラグインに適応した形で作成できていないと思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/6f9e65c8-b25c-87b1-f1cc-895a88cc1d12.png)

`Material 3 Design Kit`のExampleを使って、コードを出力しました。
エディター上のエラー解消後に実端末で確認しましたが、こちらも**OVERFLOWEDが発生**しました。

<details><summary>実端末確認時のスクリーンショット</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/aa751b8b-4ff2-dd9b-b038-6c04fe1a07cd.png)
</details>

Figmaの利用経験が少ないので、今後改善可能な部分はありますが、UI全体を頼るのではなく、必要な部分を適宜ピックアップして利用する、といった形で適切な気がしました！

### UI Pocketの画像をClaudeにアップロードして、コード提供してもらう
**特定のUIや機能の実装方法を把握して選択肢を広げる**
chatgptではなくClaudeです。両方を試してみて、アップロード画像に対するFlutterのコード生成は、Claudeの方がより実物に近いUIかつ、動くものに近いコードが生成される印象を受けました。
#### プロンプト入力
画面を添付して、添付した画像について、Flutterでコードを作成してほしいです。と伝えました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1863439d-bd4d-e42d-b196-471eb136a2b9.png)
#### コード生成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3850e816-3b11-a996-e514-484c666efe45.png)
#### 動作確認
生成されたコードで特にエラー発生していませんので、修正箇所はありませんでした。
実機を用いた確認に進みました。こちらは、**OVERFLOWEDがありませんでした！**
<details><summary>実端末で確認</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1e957f62-dddd-0ddd-a9c9-6f9b72cffa42.png)
画像1枚からここまでの精度でコード出力してくれることに感謝です。
UIがそれっぽいだけではありません。FormウィジェットとTextFormFieldウィジェットで構成されていました。

<details><summary>キーボードが数字入力になっている</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1789ebbc-003f-ae62-280e-3065b20eadc9.png)
</details>
<details><summary>ラジオボタンがONになる</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3ae42409-455a-9992-4e85-4d7416057f4e.png)
</details>

</details>

#### コード確認
テキスト入力欄やボタンの配置、各入力値を保持する、タブ部分をタブとして機能させる、など修正が必要な箇所はありますが、ここまでやってくれると助かります。

<details><summary>生成されたコード</summary>
エラーは特に発生しなかったので、修正箇所はありません。

```dart
import 'package:flutter/material.dart';

class SMBCLoginScreen extends StatefulWidget {
  @override
  _SMBCLoginScreenState createState() => _SMBCLoginScreenState();
}

class _SMBCLoginScreenState extends State<SMBCLoginScreen> {
  bool _saveCredentials = false;
  final _formKey = GlobalKey<FormState>();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.arrow_back),
          onPressed: () => Navigator.pop(context),
        ),
        title: Text('ログイン'),
        actions: [
          IconButton(
            icon: Icon(Icons.help_outline),
            onPressed: () {
              // ヘルプ機能の実装
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // SMBC ID と口座情報のタブ
                Row(
                  children: [
                    Expanded(
                      child: Container(
                        padding: EdgeInsets.all(8),
                        decoration: BoxDecoration(
                          border: Border(
                            bottom: BorderSide(
                              color: Colors.green[700]!,
                              width: 2,
                            ),
                          ),
                        ),
                        child: Text(
                          'SMBC ID',
                          style: TextStyle(
                            color: Colors.green[700],
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                    Expanded(
                      child: Container(
                        padding: EdgeInsets.all(8),
                        child: Text('口座情報'),
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 24),
                Text(
                  '契約者番号でログイン',
                  style: TextStyle(
                    color: Colors.green[700],
                    fontSize: 16,
                  ),
                ),
                SizedBox(height: 16),
                // 店番号入力フィールド
                TextFormField(
                  decoration: InputDecoration(
                    labelText: '店番号',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.number,
                ),
                SizedBox(height: 16),
                // 口座番号入力フィールド
                TextFormField(
                  decoration: InputDecoration(
                    labelText: '口座番号',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.number,
                ),
                SizedBox(height: 16),
                // ログイン暗証入力フィールド
                TextFormField(
                  decoration: InputDecoration(
                    labelText: 'ログイン暗証',
                    border: OutlineInputBorder(),
                  ),
                  obscureText: true,
                ),
                SizedBox(height: 16),
                // 店・口座番号保存スイッチ
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text('店・口座番号保存'),
                    Switch(
                      value: _saveCredentials,
                      onChanged: (bool value) {
                        setState(() {
                          _saveCredentials = value;
                        });
                      },
                      activeColor: Colors.green[700],
                    ),
                  ],
                ),
                SizedBox(height: 24),
                // ログインボタン
                ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.validate()) {
                      // ログイン処理の実装
                    }
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.grey[200],
                    padding: EdgeInsets.symmetric(vertical: 16),
                  ),
                  child: Text(
                    'ログイン',
                    style: TextStyle(
                      color: Colors.grey[600],
                      fontSize: 16,
                    ),
                  ),
                ),
                SizedBox(height: 24),
                // 暗証忘れの場合のリンク
                TextButton(
                  onPressed: () {
                    // 暗証忘れ処理の実装
                  },
                  child: Text(
                    'ログイン暗証をお忘れの場合',
                    style: TextStyle(
                      color: Colors.green[700],
                    ),
                  ),
                ),
                // 下部のナビゲーションバー
                Padding(
                  padding: EdgeInsets.only(top: 100),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _buildBottomNavItem(Icons.lock, 'ワンタイム\nパスワード'),
                      _buildBottomNavItem(Icons.credit_card, 'My通帳'),
                      _buildBottomNavItem(Icons.account_balance, 'サービス一覧'),
                      _buildBottomNavItem(Icons.settings, '設定'),
                    ],
                  ),
                ),
                SizedBox(height: 16),
                // システムメンテナンス通知
                Container(
                  padding: EdgeInsets.all(8),
                  color: Colors.yellow[100],
                  child: Text(
                    '1月12日(日)21時〜1月13日(月・祝)12時\n新システムへ移行のため、サービスを休止いたします。',
                    style: TextStyle(fontSize: 12),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildBottomNavItem(IconData icon, String label) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, color: Colors.green[700]),
        Text(
          label,
          textAlign: TextAlign.center,
          style: TextStyle(
            fontSize: 12,
            color: Colors.green[700],
          ),
        ),
      ],
    );
  }
}

```
</details>

### UI Pocketの画像を見て、実際にコード実装しながら、適宜ネットやClaudeを利用
**Flutter(Dart)の各APIの機能を理解しつつ、開発する能力向上**
まだこの方法一番早く進められる状況です。Flutterを少しだけ理解している段階にあるから、かもしれません。
Claudeで出力されたものを実物に近づける作業の方が、苦労するレベルかと思います。

画像を見て、基本的には私自身でコードを書き進めます。
この部分のUIはどう実装するんだろう？となった場合に、ネットで情報収集をしたり、Claudeを利用したりします。（比較的原始的な方法？）

#### 成果物（画面）
実際にコードを書いて作成したものです。
左：表示時
右：情報入力後
アイコン部分などは、これから実装、といった段階です。
**改めて、時間は要しても、この方法が一番収穫の多いやり方、と感じられますね。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c9105db5-f5a6-0da6-11fb-dac8aa056bd2.png)

#### 成果物（コード）
これから改善する箇所もありますが、[こちらのページ](https://gist.github.com/eno-conan/23a918bd948b024cde1734648b9e762f)からコードは確認いただけます。
ちなみに、`custom_color.dart`で実装した各要素の色は、以下の手順で設定しています。

１．*Microsoft PowerToys*の`Color Picker`を用いて、知りたい箇所の色のHEXを取得

https://learn.microsoft.com/ja-jp/windows/powertoys/color-picker

２．*Flutter Color from hex generator*に1で取得した色を貼り付け、変換後の値を設定

https://jonas-rodehorst.dev/tools/flutter-color-from-hex

## コード作成後
3つ目の方法の最後に記載しましたが、以下では困っちゃいます。
- UIがかっこよく見えても、スクロールしたらガタガタ
- 別の端末にしたら、OVERFLOWなどのエラーになる

[Flutter and Dart DevTools](https://docs.flutter.dev/tools/devtools)も使って、パフォーマンスを改善、レスポンシブデザイン対応、など行います。（まだまだ勉強中です！！）
初歩的な内容かとなりますが、ご容赦ください！
#### パフォーマンス改善
- 各Widgetのrebuildを防ぐために`const`を設定
- `const`を設定可能な構造への変更、Widgetを採用(ContainerからSizedBoxへの切り替え、など)
#### Responsive, Adaptiveデザインの適用
- `MediaQuery.sizeOf(context)`や`LayoutBuilder`を用いた改善
- [flutter_platform_widgets](https://pub.dev/packages/flutter_platform_widgets)の採用

## まとめ
今回、UI Pocketというサービスを起点にFlutter学習を進めている話をさせていただきました。
冒頭にも記載しましたが、
UI Pocketを通じて、以下の理解を深められると感じている最中です。
- Flutter
- Figma
- デザイン

今後も活用して、スキルアップできればと思います。

ありがとうございました。