-
Notifications
You must be signed in to change notification settings - Fork 0
DNET_FlutterSecondStep2
-
戻る
- ...Flutterの5thステップ
- Flutterのファースト・ステップ2
- Flutterのセカンド・ステップ2(本ページ)
- Flutterのサード・ステップ2
WebAPIを経由した簡単なCRUD処理の実装
プロンプト
添付のReactプロジェクトのコードをFlutterに移植します。
なお、作成したScreenは以下からリンクします。
- 添付のReactプロジェクトのコード:https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/SPA/React/vite-react-ts/src/pages/FetchData.tsx
- 「以下からリンクします。」の以下:https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/layout/main_layout.dart
Reactの、fetch相当をインストール(http)
-
プロンプト
pubspec.yamlはコマンドではなく手書き? -
出力
flutter pub add http
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(),
],
),
);
}
}-
プロンプト
Flutterでapp.configのようなパラメタ設定ファイルはありますか? -
出力
lib/config/app_config.dartclass 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
プロンプト
添付のReactプロジェクトのコードをFlutterに移植します。
※ 添付のReactプロジェクトのコード:以下の「移行元」のファイル
先ずは以下のファイルを移行(AIで旧記法を修正して移行)
-
移行元
-
移行先
main_layout.dartからリンクするという記載を怠ったため「appBar: AppBar()」、「drawer: MyDrawer()」を手動で削除するハメに。
- 移行元:https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/old/flutter_template/lib/components/crud_page.dart
- 移行先:https://github.com/OpenTouryoProject/FrontendTemplates/blob/develop/UI/XPlat/flutter_template/lib/screens/crud_sample.dart
-
プロンプト
以下のコードを書き換えます ```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もマークアップ系のネストは苦手である模様。
※ 最終的には修正方法を出力させて人間の頭で理解しながら修正する必要がある模様。
- FrontendTemplates/UI/XPlat at develop · OpenTouryoProject/FrontendTemplates
https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/XPlat/
移行メモ
- 「戻る」の並びにある「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活用
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。