API の基本 + POST / PUT / DELETE リクエスト

昨日は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() で変換。

目次