API の基本 + GET リクエスト

目次

API って何?

API (Application Programming Interface) は、アプリとサーバーが会話するための仕組み!
比喩で言うと: レストランのようなもの

  1. あなた(アプリ): 料理を注文
  2. ウェイター(API): 注文を厨房に伝える
  3. 厨房(サーバー): 料理を作る
  4. ウェイター(API): 料理を運んでくる

アプリ → API → サーバー → API → アプリ の流れ!

HTTP リクエストの種類

GET→データを取得(ブログ記事を読む)
POST→データを作成(新規記事を投稿)
PUT→データを更新(記事を編集)
DELETE→データを削除(記事を削除)

GET を覚えよう

http パッケージのインストール

dependencies:
  flutter:
    sdk: flutter
  
  http: ^1.1.0

超シンプルな GET リクエスト

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

class SimpleApiDemo extends StatefulWidget {
  @override
  _SimpleApiDemoState createState() => _SimpleApiDemoState();
}

class _SimpleApiDemoState extends State<SimpleApiDemo> {
  String _result = '取得前';
  
  // ========================================
  // GET リクエスト
  // ========================================
  Future<void> fetchData() async {
    // URLを指定
    final url = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
    
    // GETリクエストを送信
    final response = await http.get(url);
    
    // ステータスコード確認
    if (response.statusCode == 200) {
      // 成功! JSONをデコード
      final data = json.decode(response.body);
      
      setState(() {
        _result = data['title'];  // タイトルを取得
      });
    } else {
      // 失敗
      setState(() {
        _result = 'エラー: ${response.statusCode}';
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Simple API')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_result, style: TextStyle(fontSize: 18)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: fetchData,
              child: Text('データ取得'),
            ),
          ],
        ),
      ),
    );
  }
}

基本の流れ(3ステップ)

ステップ1: URL を作る

final url = Uri.parse('https://api.example.com/data');

ステップ2: GET リクエスト

final response = await http.get(url);

ステップ3: レスポンスを処理

if (response.statusCode == 200) {
  final data = json.decode(response.body);
  print(data);
}

HTTP ステータスコード

200成功データ取得成功 ✅
201作成成功新規投稿成功
400リクエストエラーパラメータが間違い
401認証エラーログインが必要
404Not Foundデータが見つからない
500サーバーエラーサーバー側の問題

ヘッダーを追加

APIによっては、ヘッダー(認証トークンなど)が必要!

Future<void> fetchWithHeaders() async {
  final url = Uri.parse('https://api.example.com/data');
  
  final response = await http.get(
    url,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer YOUR_TOKEN_HERE',
      'Accept': 'application/json',
    },
  );
  
  if (response.statusCode == 200) {
    final data = json.decode(response.body);
    print(data);
  }
}

タイムアウト設定

Future<void> fetchWithTimeout() async {
  final url = Uri.parse('https://api.example.com/data');
  
  try {
    final response = await http.get(url).timeout(
      Duration(seconds: 10),  // 10秒でタイムアウト
      onTimeout: () {
        throw Exception('タイムアウトしました');
      },
    );
    
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      print(data);
    }
  } catch (e) {
    print('エラー: $e');
  }
}

実践例: パラメータ付き GET リクエスト

class SearchDemo extends StatefulWidget {
  @override
  _SearchDemoState createState() => _SearchDemoState();
}

class _SearchDemoState extends State<SearchDemo> {
  List<dynamic> _posts = [];
  bool _isLoading = false;
  
  // ========================================
  // ユーザーIDで絞り込み
  // ========================================
  Future<void> fetchPostsByUserId(int userId) async {
    setState(() {
      _isLoading = true;
    });
    
    try {
      // パラメータ付きURL
      final url = Uri.parse(
        'https://jsonplaceholder.typicode.com/posts?userId=$userId'
      );
      
      // または、queryParametersを使う(推奨)
      // final url = Uri.https(
      //   'jsonplaceholder.typicode.com',
      //   '/posts',
      //   {'userId': userId.toString()},
      // );
      
      final response = await http.get(url);
      
      if (response.statusCode == 200) {
        final List<dynamic> data = json.decode(response.body);
        
        setState(() {
          _posts = data;
          _isLoading = false;
        });
      }
    } catch (e) {
      print('エラー: $e');
      setState(() {
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('検索デモ')),
      body: Column(
        children: [
          // ========================================
          // ユーザー選択
          // ========================================
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () => fetchPostsByUserId(1),
                  child: Text('User 1'),
                ),
                ElevatedButton(
                  onPressed: () => fetchPostsByUserId(2),
                  child: Text('User 2'),
                ),
                ElevatedButton(
                  onPressed: () => fetchPostsByUserId(3),
                  child: Text('User 3'),
                ),
              ],
            ),
          ),
          
          Divider(),
          
          // ========================================
          // リスト表示
          // ========================================
          Expanded(
            child: _isLoading
                ? Center(child: CircularProgressIndicator())
                : _posts.isEmpty
                    ? Center(child: Text('ユーザーを選択してください'))
                    : 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),
                            ),
                          );
                        },
                      ),
          ),
        ],
      ),
    );
  }
}

パラメータの書き方
方法1: 文字列で直接書く

final url = Uri.parse('https://api.example.com/posts?userId=1&limit=10');

方法2: Uri.https を使う

final url = Uri.https(
  'api.example.com',     // ホスト
  '/posts',              // パス
  {                      // パラメータ
    'userId': '1',
    'limit': '10',
    'sort': 'desc',
  },
);

// 結果: https://api.example.com/posts?userId=1&limit=10&sort=desc

コツ

try-catch で囲む

// ❌ エラーでアプリがクラッシュ
Future<void> fetchData() async {
  final response = await http.get(url);
  final data = json.decode(response.body);
}

// ✅ 安全
Future<void> fetchData() async {
  try {
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
    }
  } catch (e) {
    print('エラー: $e');
    // ユーザーにエラー表示
  }
}

ローディング状態を管理

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  bool _isLoading = false;
  List<dynamic> _data = [];
  String? _error;
  
  Future<void> fetchData() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });
    
    try {
      final response = await http.get(url);
      if (response.statusCode == 200) {
        setState(() {
          _data = json.decode(response.body);
          _isLoading = false;
        });
      } else {
        throw Exception('エラー: ${response.statusCode}');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_isLoading) return CircularProgressIndicator();
    if (_error != null) return Text('エラー: $_error');
    if (_data.isEmpty) return Text('データがありません');
    
    return ListView.builder(...);
  }
}

Service クラスで整理

// api_service.dart
class ApiService {
  static const String baseUrl = 'https://api.example.com';
  
  Future<List<dynamic>> fetchPosts() 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('Failed to load posts');
    }
  }
  
  Future<dynamic> fetchPostById(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('Failed to load post');
    }
  }
}

// 使う
final apiService = ApiService();
final posts = await apiService.fetchPosts();

まとめ: GET リクエスト

基本の流れ:

  • Uri.parse() で URL を作る
  • http.get() でリクエスト
  • response.statusCode で確認
  • json.decode() でデコード

ポイント:

  • FutureBuilder で簡潔に書ける
  • パラメータは Uri.https() が安全
  • try-catch でエラー処理
  • ローディング状態を管理
  • Service クラスで整理

一言まとめ:
API の GET リクエストはhttp
パッケージで簡単! http.get(url) でデータ取得、
json.decode() でJSON変換。
FutureBuilder でローディング・エラー処理が自動。
パラメータは Uri.https() で安全に!

目次