エラーハンドリング: 転んだときの受け身 🤸
認証: 家の鍵 🔐
アプリを安全で壊れにくくします!
目次
エラーハンドリングって何?
エラーの種類:
| ネットワークエラー | 通信失敗 | Wi-Fiが切れた |
| タイムアウト | 応答が遅い | サーバーが重い |
| 404 Not Found | データがない | 削除済みの投稿 |
| 401 Unauthorized | 認証エラー | ログインが必要 |
| 500 Server Error | サーバー側の問題 | サーバーダウン |
| JSONパースエラー | データ形式が違う | 予期しない形式 |
基本の try-catch
Future<void> fetchData() async {
try {
// ========================================
// API リクエスト
// ========================================
final url = Uri.parse('https://api.example.com/data');
final response = await http.get(url);
if (response.statusCode == 200) {
final data = json.decode(response.body);
print(data);
} else {
throw Exception('エラー: ${response.statusCode}');
}
} on SocketException {
// ========================================
// ネットワークエラー
// ========================================
print('ネットワーク接続がありません');
} on TimeoutException {
// ========================================
// タイムアウト
// ========================================
print('タイムアウトしました');
} on FormatException {
// ========================================
// JSONパースエラー
// ========================================
print('データ形式が正しくありません');
} catch (e) {
// ========================================
// その他のエラー
// ========================================
print('予期しないエラー: $e');
}
}
例1: エラーハンドリング付き API サービス
import 'dart:io';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
class ApiService {
static const String baseUrl = 'https://api.example.com';
// ========================================
// GET リクエスト (エラーハンドリング完全版)
// ========================================
Future<dynamic> get(String endpoint) async {
try {
final url = Uri.parse('$baseUrl$endpoint');
// タイムアウト設定
final response = await http.get(url).timeout(
Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('リクエストがタイムアウトしました');
},
);
// ステータスコード別処理
return _handleResponse(response);
} on SocketException {
throw NetworkException('インターネット接続を確認してください');
} on TimeoutException {
throw NetworkException('サーバーの応答が遅すぎます');
} on FormatException {
throw ApiException('データ形式が正しくありません');
} catch (e) {
throw ApiException('予期しないエラー: $e');
}
}
// ========================================
// レスポンス処理
// ========================================
dynamic _handleResponse(http.Response response) {
switch (response.statusCode) {
case 200:
case 201:
// 成功
try {
return json.decode(response.body);
} catch (e) {
throw ApiException('JSONパースエラー');
}
case 400:
throw ApiException('リクエストが不正です', statusCode: 400);
case 401:
throw UnauthorizedException('認証が必要です');
case 403:
throw UnauthorizedException('アクセス権限がありません');
case 404:
throw ApiException('データが見つかりません', statusCode: 404);
case 500:
case 502:
case 503:
throw ApiException('サーバーエラーが発生しました', statusCode: response.statusCode);
default:
throw ApiException(
'エラーが発生しました',
statusCode: response.statusCode,
);
}
}
// ========================================
// POST リクエスト
// ========================================
Future<dynamic> post(String endpoint, Map<String, dynamic> data) async {
try {
final url = Uri.parse('$baseUrl$endpoint');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode(data),
).timeout(Duration(seconds: 10));
return _handleResponse(response);
} on SocketException {
throw NetworkException('インターネット接続を確認してください');
} on TimeoutException {
throw NetworkException('サーバーの応答が遅すぎます');
} catch (e) {
throw ApiException('予期しないエラー: $e');
}
}
}
例2: UI でのエラー表示
class StudentListPage extends StatefulWidget {
@override
_StudentListPageState createState() => _StudentListPageState();
}
class _StudentListPageState extends State<StudentListPage> {
final ApiService _apiService = ApiService();
List<Student> _students = [];
bool _isLoading = false;
String? _errorMessage;
// ========================================
// データ取得 (エラーハンドリング付き)
// ========================================
Future<void> fetchStudents() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final data = await _apiService.get('/students');
setState(() {
_students = (data as List)
.map((json) => Student.fromJson(json))
.toList();
_isLoading = false;
});
} on NetworkException catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
_showErrorSnackBar('ネットワークエラー: ${e.message}');
} on UnauthorizedException catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
_showErrorSnackBar('認証エラー: ${e.message}');
// ログイン画面へ遷移
_navigateToLogin();
} on ApiException catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
_showErrorSnackBar(e.message);
} catch (e) {
setState(() {
_errorMessage = '予期しないエラーが発生しました';
_isLoading = false;
});
_showErrorSnackBar('エラー: $e');
}
}
// ========================================
// エラー表示
// ========================================
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
action: SnackBarAction(
label: '再試行',
textColor: Colors.white,
onPressed: fetchStudents,
),
),
);
}
void _navigateToLogin() {
Navigator.pushReplacementNamed(context, '/login');
}
@override
void initState() {
super.initState();
fetchStudents();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('生徒一覧')),
body: _buildBody(),
);
}
// ========================================
// 状態別UI
// ========================================
Widget _buildBody() {
// ローディング中
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
// エラー表示
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 60, color: Colors.red),
SizedBox(height: 16),
Text(
_errorMessage!,
style: TextStyle(fontSize: 16, color: Colors.grey[700]),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
ElevatedButton.icon(
icon: Icon(Icons.refresh),
label: Text('再試行'),
onPressed: fetchStudents,
),
],
),
);
}
// データ表示
if (_students.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 60, color: Colors.grey),
SizedBox(height: 16),
Text('生徒がいません'),
],
),
);
}
return ListView.builder(
itemCount: _students.length,
itemBuilder: (context, index) {
return StudentCard(student: _students[index]);
},
);
}
}
例3: リトライ機能
class ApiService {
// ========================================
// リトライ付きリクエスト
// ========================================
Future<dynamic> getWithRetry(
String endpoint, {
int maxRetries = 3,
Duration retryDelay = const Duration(seconds: 2),
}) async {
int attempt = 0;
while (attempt < maxRetries) {
try {
return await get(endpoint);
} on NetworkException catch (e) {
attempt++;
if (attempt >= maxRetries) {
rethrow; // 最後の試行で失敗したら、エラーを投げる
}
print('リトライ $attempt/$maxRetries...');
await Future.delayed(retryDelay);
} on ApiException catch (e) {
// APIエラーはリトライしない
rethrow;
}
}
throw NetworkException('リトライ回数を超えました');
}
}
// 使い方
try {
final data = await apiService.getWithRetry('/students');
print(data);
} catch (e) {
print('全てのリトライが失敗: $e');
}
認証 (Authentication)
| 方式 | 説明 | 用途 |
|---|---|---|
| API キー | 固定の文字列 | 簡易API |
| Basic 認証 | ユーザー名+パスワード | WordPress |
| Bearer トークン | トークン文字列 | REST API |
| OAuth 2.0 | 外部サービス連携 | Google, Facebook |
| JWT | JSON Web Token | モダンAPI |
🔑 1. API キー認証
class ApiService {
static const String apiKey = 'YOUR_API_KEY_HERE';
Future<dynamic> get(String endpoint) async {
final url = Uri.parse('https://api.example.com$endpoint');
final response = await http.get(
url,
headers: {
'X-API-Key': apiKey, // ← API キー
'Content-Type': 'application/json',
},
);
return _handleResponse(response);
}
}
注意: API キーはコードに直接書かない!
🔑 2. Basic 認証 (WordPress)
import 'dart:convert';
class WordPressService {
static const String baseUrl = 'https://your-site.com';
static const String username = 'your-username';
static const String appPassword = 'xxxx xxxx xxxx xxxx'; // アプリケーションパスワード
// ========================================
// Basic認証ヘッダー
// ========================================
Map<String, String> get authHeaders {
String credentials = '$username:$appPassword';
String encoded = base64Encode(utf8.encode(credentials));
return {
'Content-Type': 'application/json',
'Authorization': 'Basic $encoded',
};
}
// ========================================
// 投稿を取得
// ========================================
Future<List<dynamic>> getPosts() async {
final url = Uri.parse('$baseUrl/wp-json/wp/v2/posts');
final response = await http.get(
url,
headers: authHeaders, // ← Basic認証
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else if (response.statusCode == 401) {
throw UnauthorizedException('認証に失敗しました');
} else {
throw ApiException('エラー: ${response.statusCode}');
}
}
// ========================================
// 投稿を作成
// ========================================
Future<dynamic> createPost({
required String title,
required String content,
}) async {
final url = Uri.parse('$baseUrl/wp-json/wp/v2/posts');
final response = await http.post(
url,
headers: authHeaders,
body: json.encode({
'title': title,
'content': content,
'status': 'publish',
}),
);
if (response.statusCode == 201) {
return json.decode(response.body);
} else if (response.statusCode == 401) {
throw UnauthorizedException('認証に失敗しました');
} else {
throw ApiException('投稿作成失敗: ${response.statusCode}');
}
}
}
🔑 3. Bearer トークン認証
class ApiService {
String? _accessToken;
// ========================================
// トークンを設定
// ========================================
void setAccessToken(String token) {
_accessToken = token;
}
// ========================================
// 認証ヘッダー
// ========================================
Map<String, String> get authHeaders {
if (_accessToken == null) {
throw UnauthorizedException('トークンが設定されていません');
}
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_accessToken', // ← Bearer トークン
};
}
// ========================================
// ログイン
// ========================================
Future<void> login(String email, String password) async {
final url = Uri.parse('https://api.example.com/auth/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode({
'email': email,
'password': password,
}),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
_accessToken = data['token']; // トークンを保存
// SharedPreferences に保存
final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', _accessToken!);
} else {
throw UnauthorizedException('ログイン失敗');
}
}
// ========================================
// 保存されたトークンを読み込み
// ========================================
Future<void> loadToken() async {
final prefs = await SharedPreferences.getInstance();
_accessToken = prefs.getString('access_token');
}
// ========================================
// ログアウト
// ========================================
Future<void> logout() async {
_accessToken = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('access_token');
}
// ========================================
// 認証が必要なリクエスト
// ========================================
Future<dynamic> getProfile() async {
final url = Uri.parse('https://api.example.com/user/profile');
final response = await http.get(
url,
headers: authHeaders, // ← Bearer トークン
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else if (response.statusCode == 401) {
// トークンが無効
await logout();
throw UnauthorizedException('セッションが切れました。再ログインしてください。');
} else {
throw ApiException('エラー: ${response.statusCode}');
}
}
}
🔑 4. OAuth 2.0 (Google)
import 'package:google_sign_in/google_sign_in.dart';
class GoogleAuthService {
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [
'email',
'https://www.googleapis.com/auth/calendar',
],
);
// ========================================
// ログイン
// ========================================
Future<GoogleSignInAccount?> signIn() async {
try {
final account = await _googleSignIn.signIn();
if (account != null) {
// 認証成功
final auth = await account.authentication;
print('Access Token: ${auth.accessToken}');
print('ID Token: ${auth.idToken}');
return account;
}
return null;
} catch (e) {
print('ログインエラー: $e');
throw UnauthorizedException('Googleログインに失敗しました');
}
}
// ========================================
// ログアウト
// ========================================
Future<void> signOut() async {
await _googleSignIn.signOut();
}
// ========================================
// 認証ヘッダーを取得
// ========================================
Future<Map<String, String>> getAuthHeaders() async {
final account = _googleSignIn.currentUser;
if (account == null) {
throw UnauthorizedException('ログインしていません');
}
final auth = await account.authentication;
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${auth.accessToken}',
};
}
}
🔑 5. JWT (JSON Web Token)
import 'package:jwt_decoder/jwt_decoder.dart';
class JwtService {
String? _token;
// ========================================
// トークンを保存
// ========================================
Future<void> saveToken(String token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('jwt_token', token);
}
// ========================================
// トークンを読み込み
// ========================================
Future<void> loadToken() async {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('jwt_token');
}
// ========================================
// トークンが有効か確認
// ========================================
bool isTokenValid() {
if (_token == null) return false;
try {
// トークンの有効期限を確認
bool isExpired = JwtDecoder.isExpired(_token!);
return !isExpired;
} catch (e) {
return false;
}
}
// ========================================
// トークンをデコード
// ========================================
Map<String, dynamic>? decodeToken() {
if (_token == null) return null;
try {
return JwtDecoder.decode(_token!);
} catch (e) {
return null;
}
}
// ========================================
// ユーザーIDを取得
// ========================================
String? getUserId() {
final decoded = decodeToken();
return decoded?['userId'];
}
// ========================================
// 認証ヘッダー
// ========================================
Map<String, String> getAuthHeaders() {
if (_token == null || !isTokenValid()) {
throw UnauthorizedException('トークンが無効です');
}
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_token',
};
}
// ========================================
// トークンを削除
// ========================================
Future<void> clearToken() async {
_token = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('jwt_token');
}
}
セキュリティ対策
1)トークンを安全に保存
// ❌ 危険
String token = 'abc123'; // コードに直接書く
// ✅ 安全
// SharedPreferences に暗号化して保存
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
final _storage = FlutterSecureStorage();
Future<void> saveToken(String token) async {
await _storage.write(key: 'access_token', value: token);
}
Future<String?> getToken() async {
return await _storage.read(key: 'access_token');
}
Future<void> deleteToken() async {
await _storage.delete(key: 'access_token');
}
}
2)環境変数を使う
# pubspec.yaml
dependencies:
flutter_dotenv: ^5.1.0
# .env ファイル
API_KEY=your_api_key_here
API_SECRET=your_secret_here
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future<void> main() async {
await dotenv.load(); // .env を読み込む
runApp(MyApp());
}
// 使い方
String apiKey = dotenv.env['API_KEY'] ?? '';
注意: .env ファイルは .gitignore に追加!
3)HTTPS を使う
// ❌ 危険
final url = Uri.parse('http://api.example.com/data'); // HTTP
// ✅ 安全
final url = Uri.parse('https://api.example.com/data'); // HTTPS
まとめ: エラーハンドリング + 認証
エラーハンドリング:
- try-catch で種類別に処理
- カスタム例外を作る
- ユーザーにわかりやすいメッセージ
- リトライ機能
認証:
| 方式 | 使い所 |
|---|---|
| API キー | 簡易API |
| Basic | WordPress |
| Bearer | REST API |
| OAuth | Google, Facebook |
| JWT | モダンAPI |
セキュリティ:
- トークンは暗号化して保存
- 環境変数を使う
- HTTPS を使う
- .env は Git に入れない
一言まとめ:
エラーハンドリングでアプリを壊れにくく!
try-catch で種類別処理、カスタム例外でわかりやすく。
認証はAPI キー、Basic、Bearer、OAuth など用途に応じて選択。
トークンは暗号化して保存、HTTPS を使う!
