昨日はGET(データ取得)を書きました。今回はデータを作成・更新・削除です。
目次
復習: HTTP メソッド
- GET→データを取得(ブログ記事を読む)
- POST→データを作成(新規記事を投稿)
- PUT→データを更新(記事を編集)
- DELETE→データを削除(記事を削除)
1️⃣ POST リクエスト – データを作成!
基本形:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> createPost() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
// ========================================
// POST リクエスト
// ========================================
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json', // JSON を送る
},
body: json.encode({
'title': '新しい投稿',
'body': 'これは本文です',
'userId': 1,
}),
);
if (response.statusCode == 201) { // 201 = Created
print('作成成功!');
final data = json.decode(response.body);
print('ID: ${data['id']}');
} else {
print('失敗: ${response.statusCode}');
}
}
ポイント:
http.post() を使う
headers で Content-Type を指定
body に送るデータを JSON 形式で
成功すると 201 が返る
実践例1: フォームから投稿
class CreatePostPage extends StatefulWidget {
@override
_CreatePostPageState createState() => _CreatePostPageState();
}
class _CreatePostPageState extends State<CreatePostPage> {
final _titleController = TextEditingController();
final _bodyController = TextEditingController();
bool _isLoading = false;
// ========================================
// POST リクエスト
// ========================================
Future<void> createPost() async {
setState(() {
_isLoading = true;
});
try {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'title': _titleController.text,
'body': _bodyController.text,
'userId': 1,
}),
);
if (response.statusCode == 201) {
// 成功!
final data = json.decode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('投稿成功! ID: ${data['id']}')),
);
// フォームをクリア
_titleController.clear();
_bodyController.clear();
} else {
throw Exception('投稿失敗: ${response.statusCode}');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラー: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
void dispose() {
_titleController.dispose();
_bodyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('新規投稿')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// タイトル入力
TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'タイトル',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
// 本文入力
TextField(
controller: _bodyController,
decoration: InputDecoration(
labelText: '本文',
border: OutlineInputBorder(),
),
maxLines: 5,
),
SizedBox(height: 24),
// 投稿ボタン
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : createPost,
child: _isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('投稿する'),
),
),
],
),
),
);
}
}
2️⃣ PUT リクエスト – データを更新!
基本形:
Future<void> updatePost(int id) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
// ========================================
// PUT リクエスト
// ========================================
final response = await http.put(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'id': id,
'title': '更新されたタイトル',
'body': '更新された本文',
'userId': 1,
}),
);
if (response.statusCode == 200) { // 200 = OK
print('更新成功!');
final data = json.decode(response.body);
print(data);
} else {
print('失敗: ${response.statusCode}');
}
}
ポイント:
http.put() を使う
URL に更新対象の ID を含める
全フィールドを送る(部分更新は PATCH)
実践例2: 編集画面
class EditPostPage extends StatefulWidget {
final int postId;
final String initialTitle;
final String initialBody;
const EditPostPage({
required this.postId,
required this.initialTitle,
required this.initialBody,
});
@override
_EditPostPageState createState() => _EditPostPageState();
}
class _EditPostPageState extends State<EditPostPage> {
late TextEditingController _titleController;
late TextEditingController _bodyController;
bool _isLoading = false;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.initialTitle);
_bodyController = TextEditingController(text: widget.initialBody);
}
// ========================================
// PUT リクエスト
// ========================================
Future<void> updatePost() async {
setState(() {
_isLoading = true;
});
try {
final url = Uri.parse(
'https://jsonplaceholder.typicode.com/posts/${widget.postId}'
);
final response = await http.put(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'id': widget.postId,
'title': _titleController.text,
'body': _bodyController.text,
'userId': 1,
}),
);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更新成功!')),
);
Navigator.pop(context, true); // 更新成功を通知
} else {
throw Exception('更新失敗: ${response.statusCode}');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラー: $e')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
void dispose() {
_titleController.dispose();
_bodyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('投稿を編集')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'タイトル',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
TextField(
controller: _bodyController,
decoration: InputDecoration(
labelText: '本文',
border: OutlineInputBorder(),
),
maxLines: 5,
),
SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : updatePost,
child: _isLoading
? CircularProgressIndicator()
: Text('更新する'),
),
),
],
),
),
);
}
}
3️⃣DELETE リクエスト – データを削除!
基本形:
Future<void> deletePost(int id) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
// ========================================
// DELETE リクエスト
// ========================================
final response = await http.delete(url);
if (response.statusCode == 200) { // 200 = OK
print('削除成功!');
} else {
print('失敗: ${response.statusCode}');
}
}
ポイント:
- http.delete() を使う
- URL に削除対象の ID を含める
- body は不要
実践例3: 削除確認ダイアログ付き
class PostListPage extends StatefulWidget {
@override
_PostListPageState createState() => _PostListPageState();
}
class _PostListPageState extends State<PostListPage> {
List<dynamic> _posts = [];
// データ取得
Future<void> fetchPosts() async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
final response = await http.get(url);
if (response.statusCode == 200) {
setState(() {
_posts = json.decode(response.body);
});
}
}
// ========================================
// DELETE リクエスト
// ========================================
Future<void> deletePost(int id) async {
try {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
final response = await http.delete(url);
if (response.statusCode == 200) {
// リストから削除
setState(() {
_posts.removeWhere((post) => post['id'] == id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('削除しました')),
);
} else {
throw Exception('削除失敗: ${response.statusCode}');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラー: $e')),
);
}
}
// ========================================
// 削除確認ダイアログ
// ========================================
Future<void> showDeleteConfirmation(int id, String title) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('削除確認'),
content: Text('「$title」を削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('キャンセル'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text('削除'),
),
],
),
);
if (confirmed == true) {
await deletePost(id);
}
}
@override
void initState() {
super.initState();
fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('投稿リスト')),
body: _posts.isEmpty
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
title: Text(post['title']),
subtitle: Text(post['body'], maxLines: 2),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 編集ボタン
IconButton(
icon: Icon(Icons.edit, color: Colors.blue),
onPressed: () {
// 編集画面へ
},
),
// 削除ボタン
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
showDeleteConfirmation(
post['id'],
post['title'],
);
},
),
],
),
),
);
},
),
);
}
}
PATCH リクエスト – 部分更新
PUT は全フィールドを送る必要がありますが、PATCH は一部だけ更新!
Future<void> patchPost(int id) async {
final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/$id');
// ========================================
// PATCH リクエスト (一部だけ更新)
// ========================================
final response = await http.patch(
url,
headers: {
'Content-Type': 'application/json',
},
body: json.encode({
'title': '新しいタイトル', // タイトルだけ更新
// body や userId は送らなくてOK!
}),
);
if (response.statusCode == 200) {
print('部分更新成功!');
}
}
CRUD 完全版!
CRUD = Create (POST), Read (GET), Update (PUT), Delete (DELETE)
class ApiService {
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
// ========================================
// Create (POST)
// ========================================
Future<dynamic> create(Map<String, dynamic> data) async {
final url = Uri.parse('$baseUrl/posts');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode(data),
);
if (response.statusCode == 201) {
return json.decode(response.body);
} else {
throw Exception('作成失敗');
}
}
// ========================================
// Read (GET)
// ========================================
Future<List<dynamic>> getAll() async {
final url = Uri.parse('$baseUrl/posts');
final response = await http.get(url);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('取得失敗');
}
}
Future<dynamic> getById(int id) async {
final url = Uri.parse('$baseUrl/posts/$id');
final response = await http.get(url);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('取得失敗');
}
}
// ========================================
// Update (PUT)
// ========================================
Future<dynamic> update(int id, Map<String, dynamic> data) async {
final url = Uri.parse('$baseUrl/posts/$id');
final response = await http.put(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode(data),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('更新失敗');
}
}
// ========================================
// Delete (DELETE)
// ========================================
Future<void> delete(int id) async {
final url = Uri.parse('$baseUrl/posts/$id');
final response = await http.delete(url);
if (response.statusCode != 200) {
throw Exception('削除失敗');
}
}
}
コツ
1)認証情報を安全に管理
// ❌ 危険 - コードに直接書く
const username = 'admin';
const password = 'password123';
// ✅ 安全 - 環境変数や設定ファイル
// .env ファイルに保存
// WP_USERNAME=admin
// WP_PASSWORD=app_password_here
// flutter_dotenv パッケージで読み込む
import 'package:flutter_dotenv/flutter_dotenv.dart';
final username = dotenv.env['WP_USERNAME'];
final password = dotenv.env['WP_PASSWORD'];
2)レスポンスを確認
Future<void> createPost() async {
final response = await http.post(url, ...);
print('Status Code: ${response.statusCode}');
print('Headers: ${response.headers}');
print('Body: ${response.body}');
// デバッグに便利!
}
3)共通ヘッダーをまとめる
class ApiClient {
final String baseUrl;
final Map<String, String> defaultHeaders;
ApiClient({
required this.baseUrl,
required this.defaultHeaders,
});
Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.post(
url,
headers: defaultHeaders, // 共通ヘッダー
body: json.encode(data),
);
return _handleResponse(response);
}
dynamic _handleResponse(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return json.decode(response.body);
} else {
throw Exception('API Error: ${response.statusCode}');
}
}
}
// 使い方
final client = ApiClient(
baseUrl: 'https://api.example.com',
defaultHeaders: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
},
);
await client.post('/posts', {'title': 'Hello'});
⚠️ 注意点
1)ステータスコードを確認
// ❌ 危険 - 常に成功すると思い込む
final response = await http.post(url, ...);
final data = json.decode(response.body); // エラーかも!
// ✅ 安全
final response = await http.post(url, ...);
if (response.statusCode == 201) {
final data = json.decode(response.body);
} else {
print('エラー: ${response.statusCode}');
}
2)削除は慎重に
// 必ず確認ダイアログを表示
Future<void> deleteWithConfirmation(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('削除確認'),
content: Text('本当に削除しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('キャンセル'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('削除'),
),
],
),
);
if (confirmed == true) {
await deletePost(id);
}
}
まとめ
| メソッド | 用途 | ステータスコード |
|---|---|---|
| POST | 作成 | 201 (Created) |
| PUT | 更新(全体) | 200 (OK) |
| PATCH | 更新(一部) | 200 (OK) |
| DELETE | 削除 | 200 (OK) |
ポイント:
- Content-Type: application/json を忘れずに
- body は json.encode() で変換
- ステータスコードで成功/失敗を判定
- 削除は確認ダイアログ必須
一言まとめ:
POST でデータ作成 (http.post())、
PUT でデータ更新 (http.put())、
DELETE でデータ削除 (http.delete())。
headers に Content-Type: application/json を設定、
body は json.encode() で変換。
