Skip to content

DNET_FlutterSecondStep2

nishi_74322014 edited this page Sep 11, 2026 · 1 revision

Flutterのセカンド・ステップ2

概要

WebAPIを経由した簡単なCRUD処理の実装

詳細

WebAPI

プロンプト

添付のReactプロジェクトのコードをFlutterに移植します。
なお、作成したScreenは以下からリンクします。

インストール

Reactの、fetch相当をインストール(http)

  • プロンプト

    pubspec.yamlはコマンドではなく手書き?
    
  • 出力

    flutter pub add http
    

FetchData

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

// --- モデル ---
class WeatherForecast {
  final String dateFormatted;
  final int temperatureC;
  final int temperatureF;
  final String summary;

  const WeatherForecast({
    required this.dateFormatted,
    required this.temperatureC,
    required this.temperatureF,
    required this.summary,
  });

  factory WeatherForecast.fromJson(Map<String, dynamic> json) {
    return WeatherForecast(
      dateFormatted: json['dateFormatted'] as String,
      temperatureC: json['temperatureC'] as int,
      temperatureF: json['temperatureF'] as int,
      summary: json['summary'] as String,
    );
  }
}

// --- 定数 (constants.dart の FetchDataRootUrl に相当) ---
// 実際の URL に合わせて変更してください。
const String _fetchDataRootUrl =
    'https://localhost:44335/api/SampleData/WeatherForecasts?';

// --- Screen ---
class ScreenFetchData extends StatefulWidget {
  const ScreenFetchData({super.key});

  @override
  State<ScreenFetchData> createState() => _ScreenFetchDataState();
}

class _ScreenFetchDataState extends State<ScreenFetchData> {
  List<WeatherForecast> _forecasts = [];
  bool _loading = true;
  int _currentPage = 1;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _fetchForecasts(1);
  }

  Future<void> _fetchForecasts(int page) async {
    setState(() {
      _loading = true;
      _errorMessage = null;
    });

    final uri = Uri.parse('${_fetchDataRootUrl}startDateIndex=$page');

    try {
      // oauth_oidc.createHttpRequestHeader(false) に相当するヘッダーを設定してください。
      final response = await http.get(
        uri,
        headers: {
          'Content-Type': 'application/json',
          // 'Authorization': 'Bearer <token>',  // 認証トークンが必要な場合
        },
      );

      if (response.statusCode == 200) {
        final List<dynamic> jsonList =
            jsonDecode(response.body) as List<dynamic>;
        final forecasts = jsonList
            .map((e) => WeatherForecast.fromJson(e as Map<String, dynamic>))
            .toList();
        setState(() {
          _forecasts = forecasts;
          _currentPage = page;
          _loading = false;
        });
      } else {
        throw Exception('HTTP ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('fetch error: $e');
      setState(() {
        _errorMessage = e.toString();
        _loading = false;
      });
    }
  }

  void _handlePrev() {
    if (_currentPage > 1) {
      _fetchForecasts(_currentPage - 1);
    }
  }

  void _handleNext() {
    _fetchForecasts(_currentPage + 1);
  }

  // --- テーブル描画 ---
  Widget _buildForecastTable() {
    const headerStyle = TextStyle(fontWeight: FontWeight.bold);

    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: DataTable(
        columns: const [
          DataColumn(label: Text('Date', style: headerStyle)),
          DataColumn(label: Text('Temp. (C)', style: headerStyle)),
          DataColumn(label: Text('Temp. (F)', style: headerStyle)),
          DataColumn(label: Text('Summary', style: headerStyle)),
        ],
        rows: _forecasts
            .map(
              (f) => DataRow(cells: [
                DataCell(Text(f.dateFormatted)),
                DataCell(Text('${f.temperatureC}')),
                DataCell(Text('${f.temperatureF}')),
                DataCell(Text(f.summary)),
              ]),
            )
            .toList(),
      ),
    );
  }

  // --- ページネーション ---
  Widget _buildPagination() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
          onPressed: (_loading || _currentPage <= 1) ? null : _handlePrev,
          child: const Text('« Prev'),
        ),
        const SizedBox(width: 12),
        Text('Page $_currentPage'),
        const SizedBox(width: 12),
        ElevatedButton(
          onPressed: _loading ? null : _handleNext,
          child: const Text('Next »'),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Weather forecast',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text('This component demonstrates fetching data from the server.'),
          const SizedBox(height: 16),

          // コンテンツ
          Expanded(
            child: Center(
              child: _loading
                  ? const CircularProgressIndicator()
                  : _errorMessage != null
                      ? Text(
                          'Error: $_errorMessage',
                          style: const TextStyle(color: Colors.red),
                        )
                      : _buildForecastTable(),
            ),
          ),

          const SizedBox(height: 12),
          _buildPagination(),
        ],
      ),
    );
  }
}

AppConfig

  • プロンプト

    Flutterでapp.configのようなパラメタ設定ファイルはありますか?
    
  • 出力
    lib/config/app_config.dart

    class AppConfig {
      // 基本URL
      static const String authServerRootUrl = String.fromEnvironment(
        'AUTH_SERVER_ROOT_URL',
        defaultValue: 'https://localhost:44300/MultiPurposeAuthSite',
      );
      static const String resourcesServerRootUrl = String.fromEnvironment(
        'RESOURCES_SERVER_ROOT_URL',
        defaultValue: 'https://localhost:44335',
      );
    
      // クライアント設定
      static const String baseUrl = '~/';
      static const String clientId = String.fromEnvironment(
        'CLIENT_ID',
        defaultValue: 'f374a155909d486a9234693c34e94479',
      );
    
      // 派生URL(文字列補間はconstにできないためstaticゲッターで定義)
      static String get authRequestUrl => '$authServerRootUrl/authorize';
      static String get tokenRequestUrl => '$authServerRootUrl/token';
      static String get userInfoRequestUrl => '$authServerRootUrl/userinfo';
      static String get fetchDataRootUrl => '$resourcesServerRootUrl/api/sampledata/weatherforecasts?';
      static String get crudSampleRootUrl => '$resourcesServerRootUrl/api/json/';
    }
  • 使い方

    • 定義

      my_app/
      ├── config.json   
      ├── lib/
      │     └── config/
      │            └── app_config.dart   ← ここが一般的
      
    • 使用

      dartimport 'package:my_app/config/app_config.dart';
      
      // 使用例
      final clientId = AppConfig.clientId;
      final authUrl = AppConfig.authRequestUrl;
      final fetchUrl = AppConfig.fetchDataRootUrl;
      
    • 変更
      defaultValueではなく.envの値を使用する場合

      flutter run --dart-define-from-file=config.json
      

リファクタリング

  • 共通関数抜き出し

    • プロンプト

      以下はReactの実装です
      ```
        fetchForecasts(page: number) {
          this.setState({ loading: true });
          common.getFetch(
            constants.FetchDataRootUrl + `startDateIndex=${page}`,
            oauth_oidc.createHttpRequestHeader(false),
            (data) => {
              this.setState({
                forecasts: data as unknown as WeatherForecast[],
                loading: false, currentPage: page });
            },
            (msg) => {
              this.setState({ loading: false });
              console.error('fetch error', msg);
            },
          );
        }
      ```
      以下はFlutterに移植したコードです
      ```dart
        Future<void> _fetchForecasts(int page) async {
          setState(() {
            _loading = true;
            _errorMessage = null;
          });
          final uri = Uri.parse('${_fetchDataRootUrl}startDateIndex=$page');
          try {
            final response = await http.get(
              uri,
              headers: {
                'Content-Type': 'application/json',
                // 'Authorization': 'Bearer <token>',  // 認証トークンが必要な場合
              },
            );
            if (response.statusCode == 200) {
              final List<dynamic> jsonList =
                  jsonDecode(response.body) as List<dynamic>;
              final forecasts = jsonList
                  .map((e) => WeatherForecast.fromJson(e as Map<String, dynamic>))
                  .toList();
              setState(() {
                _forecasts = forecasts;
                _currentPage = page;
                _loading = false;
              });
            } else {
              throw Exception('HTTP ${response.statusCode}');
            }
          } catch (e) {
            debugPrint('fetch error: $e');
            setState(() {
              _errorMessage = e.toString();
              _loading = false;
            });
          }
        }
      ```
      Flutterに容易スべき共通関数はありますか?
      

※ なお、共通関数を実装した関連ファイルを添付してある。
 ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/SPA/React/vite-react-ts/src/common.ts\  ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/SPA/React/vite-react-ts/src/touryo/common.ts\  ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/SPA/React/vite-react-ts/src/touryo/oauth_oidc.ts\ ※ 追加で「fetchStatusHandlerも作成します。」と問い合わせた。

  • 出力

    Future<void> postFetch(
      String url,
      Map<String, String> headers,
      Object body,
      void Function(Map<String, dynamic> data) onSuccess,
      void Function(String msg) setMessage,
    ) async {
      setMessage('');
      try {
        final response = fetchStatusHandler(
          await http.post(Uri.parse(url), headers: headers, body: body),
        );
        final data = jsonDecode(response.body) as Map<String, dynamic>;
        if (data['errorMSG'] != null) {
          setMessage(jsonEncode(data['errorMSG']));
        } else if (data['exceptionMSG'] != null) {
          setMessage(jsonEncode(data['exceptionMSG']));
        } else {
          onSuccess(data);
        }
      } catch (e) {
        setMessage(e.toString());
      }
    }
    
    Future<void> getFetch(
      String url,
      Map<String, String> headers,
      void Function(Map<String, dynamic> data) onSuccess,
      void Function(String msg) setMessage,
    ) async {
      setMessage('');
      try {
        final response = fetchStatusHandler(
          await http.get(Uri.parse(url), headers: headers),
        );
        final data = jsonDecode(response.body) as Map<String, dynamic>;
        if (data['errorMSG'] != null) {
          setMessage(jsonEncode(data['errorMSG']));
        } else if (data['exceptionMSG'] != null) {
          setMessage(jsonEncode(data['exceptionMSG']));
        } else {
          onSuccess(data);
        }
      } catch (e) {
        setMessage(e.toString());
      }
    }
    
    http.Response fetchStatusHandler(http.Response response) {
      if (response.statusCode == 200) {
        return response;
      } else {
        throw Exception(response.reasonPhrase ?? 'HTTP ${response.statusCode}');
      }
    }
    
    Map<String, String> createHttpRequestHeader({bool isJsonRpc = false}) {
      final headers = <String, String>{
        'Accept': 'application/json',
        'Content-Type': isJsonRpc
            ? 'application/json'
            : 'application/x-www-form-urlencoded',
      };
      final token = getAccessToken(); // SharedPreferences等から取得
      if (token != null) {
        headers['Authorization'] = 'Bearer $token';
      }
      return headers;
    }
    
    Future<String?> getAccessToken() async {
      final prefs = await SharedPreferences.getInstance();
      return prefs.getString('access_token');
    }
    
    Future<void> setAccessToken(String token) async {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('access_token', token);
    }

※ パスについて別途質問し、共通関数のファイルパスは変更無しとした。
 ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/common.dart\  ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/touryo/common.dart\  ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/touryo/oauth_oidc.dart\ ※ 他にも、以下の必要になるファイルを移行した。
 ・https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/importer.dart\ ※ 「SharedPreferences」の導入には以下の手順が必要だった。\

```
start ms-settings:developers
Developer Mode の有効化
flutter pub add shared_preferences
```
  • 共通関数の利用

    • プロンプト

      ```dart
        Future<void> _fetchForecasts(int page) async {
          setState(() {
            _loading = true;
            _errorMessage = null;
          });
          final uri = Uri.parse('${_fetchDataRootUrl}startDateIndex=$page');
          try {
            final response = await http.get(
              uri,
              headers: {
                'Content-Type': 'application/json',
                // 'Authorization': 'Bearer <token>',  // 認証トークンが必要な場合
              },
            );
            if (response.statusCode == 200) {
              final List<dynamic> jsonList =
                  jsonDecode(response.body) as List<dynamic>;
              final forecasts = jsonList
                  .map((e) => WeatherForecast.fromJson(e as Map<String, dynamic>))
                  .toList();
              setState(() {
                _forecasts = forecasts;
                _currentPage = page;
                _loading = false;
              });
            } else {
              throw Exception('HTTP ${response.statusCode}');
            }
          } catch (e) {
            debugPrint('fetch error: $e');
            setState(() {
              _errorMessage = e.toString();
              _loading = false;
            });
          }
        }
      ```
      を、以下を使用するように書き換えて下さい
      ```dart
      Future<void> getFetch(
        String url,
        Map<String, String> headers,
        void Function(dynamic data) onSuccess,
        void Function(String msg) setMessage,
      ) async {
        setMessage('');
        try {
          final response = fetchStatusHandler(
            await http.get(Uri.parse(url), headers: headers),
          );
          final data = jsonDecode(response.body) as Map<String, dynamic>;
          if (data['errorMSG'] != null) {
            setMessage(jsonEncode(data['errorMSG']));
          } else if (data['exceptionMSG'] != null) {
            setMessage(jsonEncode(data['exceptionMSG']));
          } else {
            onSuccess(data);
          }
        } catch (e) {
          setMessage(e.toString());
        }
      }
      ```
      
    • 出力

      Future<void> _fetchForecasts(int page) async {
        setState(() {
          _loading = true;
          _errorMessage = null;
        });
      
        await getFetch(
          '${_fetchDataRootUrl}startDateIndex=$page',
          {'Content-Type': 'application/json'},
          (data) {
            final jsonList = data as List<dynamic>;
            final forecasts = jsonList
                .map((e) => WeatherForecast.fromJson(e as Map<String, dynamic>))
                .toList();
            setState(() {
              _forecasts = forecasts;
              _currentPage = page;
            });
          },
          (msg) {
            setState(() {
              _errorMessage = msg;
            });
          },
        );
      
        setState(() {
          _loading = false;
        });
      }

※ ココからいくらか手直しをした。詳しくは下記とのDiffで確認できる。
https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/screens/fetch_data.dart

CRUD

プロンプト

添付のReactプロジェクトのコードをFlutterに移植します。

※ 添付のReactプロジェクトのコード:以下の「移行元」のファイル

共通部品の移行

先ずは以下のファイルを移行(AIで旧記法を修正して移行)

CRUDページの移行

main_layout.dartからリンクするという記載を怠ったため「appBar: AppBar()」、「drawer: MyDrawer()」を手動で削除するハメに。

共通関数の利用

  • プロンプト

    以下のコードを書き換えます
    ```dart
      Future<void> _selectCount() async {
        var url = Uri.http(AppConfig.serverFqdn,
          'ASPNETWebService/api/json/SelectCount',);
        var response = await http.post(url,
          headers: {
            "Accept": "application/json",
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": "Bearer ${AppAuth.accessToken}",
          },
          body: {
            "ddlDap" : this._ddlDap,
            "ddlMode1" : this._ddlMode1,
            "ddlMode2": this._ddlMode2,
            "ddlExRollback" : this._ddlExRollback,
          }
        );
        if (response.statusCode == 200) {
          Map<String, dynamic> jsonResponse
            = jsonDecode(response.body) as Map<String, dynamic>;
          setState(() {
            this._display = jsonResponse['message'];
          });
        } else {
          print('Request failed with status: ${response.statusCode}.');
        }
      }
    ```
    以下の関数を使用します
    ```dart
    Future<void> postFetch(
      String url,
      Map<String, String> headers,
      Object body,
      void Function(dynamic data) onSuccess,
      void Function(String msg) setMessage,
    ) async {
      try {
        final response = fetchStatusHandler(
          await http.post(Uri.parse(url), headers: headers, body: body),
        );
        final decoded = jsonDecode(response.body);
        // Map の場合のみエラーキーをチェック
        if (decoded is Map<String, dynamic>) {
          if (decoded['errorMSG'] != null) {
            setMessage(jsonEncode(decoded['errorMSG']));
          } else if (decoded['exceptionMSG'] != null) {
            setMessage(jsonEncode(decoded['exceptionMSG']));
          } else {
            onSuccess(decoded);
          }
        } else {
          // List など Map 以外はそのまま onSuccess へ
          onSuccess(decoded);
        }
      } catch (e) {
        setMessage(e.toString());
      }
    }
    ```
    ```dart
    Future<Map<String, String>> createHttpRequestHeader({bool isJsonRpc = false}) async {
      final headers = <String, String>{
        'Accept': 'application/json',
        'Content-Type': isJsonRpc
            ? 'application/json'
            : 'application/x-www-form-urlencoded',
      };
      final token = await getAccessToken(); // ← await を追加
      if (token != null) {
        headers['Authorization'] = 'Bearer $token';
      }
      return headers;
    }
    
    Future<String?> getAccessToken() async {
      final prefs = await SharedPreferences.getInstance();
      return prefs.getString('access_token');
    }
    
    Future<void> setAccessToken(String token) async {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('access_token', token);
    }
    ```
    
  • 出力

    Future<void> _selectCount() async {
      final url = Uri.http(
        AppConfig.serverFqdn,
        'ASPNETWebService/api/json/SelectCount',
      ).toString();
    
      final headers = await createHttpRequestHeader();
    
      final body = {
        "ddlDap": this._ddlDap,
        "ddlMode1": this._ddlMode1,
        "ddlMode2": this._ddlMode2,
        "ddlExRollback": this._ddlExRollback,
      };
    
      await postFetch(
        url,
        headers,
        body,
        (data) {
          setState(() {
            this._display = (data as Map<String, dynamic>)['message'];
          });
        },
        (msg) {
          setState(() {
            this._display = msg;
          });
        },
      );
    }

※ 同様に「_selectAll」「_crud」も書き換え。
※ Uri.http は扱い辛かったので Uri.parse に変更。

デザインの修正

  • プロンプト
    build の body部のコードを添付

    添付で、DataTable を MyDropdownButtonの並びの右側のSpaceに配置するには?
    
  • 出力

    • この生成は、それっぽい生成は出来たが貼り付けるとエラーになった。
    • 最終的には、Column(MyDropdownButtons)を「Column→Row」で囲い
      →Expanded(Column(DataTable))と横並びにする事で上手く行った。
  • その他、以下のようなプロンプトで修正を行った

    • 添付のDataTableを中央揃えしたいが左寄りになる。コレは、領域が画面端まで伸びず左寄りになっているためだろうか?
    • Flutterで「Column(」に続けて左寄せするにはどのようにしたらよいか?(MyDropdownButtons)

※ 詳細は割愛しているが、カナリ苦戦。生成AIもマークアップ系のネストは苦手である模様。
※ 最終的には修正方法を出力させて人間の頭で理解しながら修正する必要がある模様。

参考

移行メモ

  • 「戻る」の並びにある「Flutterのセカンド・ステップ2」は自ページのため、 その旨を補ってプレーン・テキストとした。
  • 元 Wiki の行頭空白によるプレフォーマット・ブロックは、 フェンス付きコードブロックにした(Dart のコードには dart を指定)。
  • 「AppConfig」の「使用」のコード先頭にある dartimport 'package:... は 原文ママ(生成 AI の出力を貼った際に言語名 dart が混入したものと思われる。 正しくは import 'package:my_app/config/app_config.dart';)。
  • PukiWiki のページ内アンカ(#xxxxxxxx)は GitHub Wiki では再現できないため、 本ページには該当箇所が無い。
  • リンク先が未移行のページは、移行先のファイル名を `DNET_○○.md` の形で示し TODO.md に登録した。

Tags: 移行, Flutter, Flutterデスクトップ, WebAPI, CRUD, DataTable, AppConfig, リファクタリング, AI活用

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally