Flutter
モバイルプラットフォーム
Flutter
概要
Flutterは、Googleが開発する革新的なクロスプラットフォームUIフレームワークとして、単一のコードベースからiOS、Android、Web、Windows、macOS、Linuxアプリケーションを構築できる包括的な開発プラットフォームです。Dart言語による型安全な開発、独自のSkiaレンダリングエンジンによる60fps での滑らかなアニメーション、そして「Write Once, Run Anywhere」を超えた「Write Once, Run Natively Everywhere」の実現により、ネイティブパフォーマンスとクロスプラットフォーム開発効率の両立を実現しています。2025年現在、Flutter 3.16以降のImpeller レンダリングエンジン安定化、WebAssembly(WASM)サポート、Enhanced Hot Reload、そして AI 統合機能により、次世代マルチプラットフォーム開発の最前線に位置しています。
詳細
Flutter 2025年版は、Impeller レンダリングエンジンの完全安定化により、従来のSkiaベースの制限を突破し、Metal(iOS)とVulkan(Android)による真のネイティブグラフィックスパフォーマンスを実現しています。特に注目すべきは、WebAssembly(WASM)サポートによりWebアプリケーションでのパフォーマンスが大幅向上し、Element Embedding による他プラットフォームとの深い統合が可能になったことです。Dart 3.3の非同期処理強化、Package Ecosystem の成熟、DevTools の AI アシスタント統合、そして Material Design 3(Material You)の完全対応により、開発者の生産性とアプリケーション品質を同時に向上させる包括的な開発エコシステムを提供しています。
主な特徴
- ユニバーサルプラットフォーム: 6つのプラットフォーム対応(iOS/Android/Web/Windows/macOS/Linux)
- Impeller レンダリング: Metal/Vulkan による高速グラフィックス処理
- Hot Reload: 瞬時のコード変更反映とステート保持
- Dart言語: 型安全で高性能なモダン言語
- 豊富なウィジェット: Material Design、Cupertino、カスタムウィジェット
- ネイティブ統合: プラットフォーム固有機能への直接アクセス
2025年の最新機能
- Impeller エンジン安定版: iOS/Android での Metal/Vulkan ネイティブレンダリング
- WebAssembly サポート: Web アプリケーションの大幅なパフォーマンス向上
- AI 統合機能: Gemini AI API との統合とコード生成支援
- Enhanced Hot Reload: より高速で安定したホットリロード
- Material Design 3: Material You の完全実装
- Element Embedding: 他フレームワークとの統合強化
メリット・デメリット
メリット
- 単一コードベースから6つのプラットフォーム対応
- ネイティブレベルのパフォーマンスと滑らかなアニメーション
- Hot Reload による極めて高速な開発サイクル
- 豊富で美しいウィジェットライブラリ
- Dart言語による型安全で読みやすいコード
- Google による継続的な開発と大規模企業での実績
- 活発なコミュニティと豊富なパッケージエコシステム
デメリット
- Dart言語の学習コストと採用障壁
- ネイティブ開発に比べてアプリサイズが大きくなる傾向
- プラットフォーム固有の深い機能へのアクセス制限
- Android/iOS以外のプラットフォームではエコシステムが発展途上
- 複雑なネイティブ統合が必要な場合の開発コスト
- Web での SEO やアクセシビリティの制約
参考ページ
書き方の例
セットアップと基本設定
# Flutter SDK のインストール(推奨方法)
# 1. Flutter公式サイトからSDKをダウンロード
# 2. パスを設定
export PATH="$PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]/flutter/bin"
# Flutter のインストール確認
flutter --version
# 開発環境のチェック
flutter doctor
# 新しいプロジェクトの作成
flutter create my_awesome_app
cd my_awesome_app
# 依存関係の取得
flutter pub get
# iOS シミュレータで実行(macOSのみ)
flutter run -d ios
# Android エミュレータで実行
flutter run -d android
# Web ブラウザで実行
flutter run -d web-server --web-hostname localhost --web-port 8080
# デスクトップアプリとして実行(macOS)
flutter run -d macos
# デスクトップアプリとして実行(Windows)
flutter run -d windows
# デスクトップアプリとして実行(Linux)
flutter run -d linux
# 依存関係の更新
flutter pub upgrade
# プロジェクトのクリーン
flutter clean
# 必須パッケージの追加
flutter pub add provider
flutter pub add http
flutter pub add shared_preferences
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutter pub add image_picker
flutter pub add path_provider
// lib/main.dart - メインアプリケーション
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'services/auth_service.dart';
import 'services/api_service.dart';
import 'providers/app_state_provider.dart';
import 'screens/splash_screen.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
import 'utils/app_router.dart';
import 'utils/app_theme.dart';
void main() async {
// Flutter バインディングの初期化
WidgetsFlutterBinding.ensureInitialized();
// Firebase の初期化
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// サービスの初期化
await AuthService.instance.initialize();
await ApiService.instance.initialize();
runApp(const MyAwesomeApp());
}
class MyAwesomeApp extends StatelessWidget {
const MyAwesomeApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AppStateProvider()),
Provider<AuthService>.value(value: AuthService.instance),
Provider<ApiService>.value(value: ApiService.instance),
],
child: Consumer<AppStateProvider>(
builder: (context, appState, child) {
return MaterialApp(
title: 'My Awesome App',
debugShowCheckedModeBanner: false,
// テーマ設定
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: appState.themeMode,
// 国際化設定
locale: appState.locale,
supportedLocales: const [
Locale('en', ''), // English
Locale('ja', ''), // Japanese
],
// ルーティング設定
initialRoute: AppRouter.initialRoute,
onGenerateRoute: AppRouter.generateRoute,
// ホーム画面
home: const AppWrapper(),
);
},
),
);
}
}
class AppWrapper extends StatefulWidget {
const AppWrapper({super.key});
@override
State<AppWrapper> createState() => _AppWrapperState();
}
class _AppWrapperState extends State<AppWrapper> {
bool _isInitialized = false;
bool _hasError = false;
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
try {
// アプリケーションの初期化処理
final appState = Provider.of<AppStateProvider>(context, listen: false);
await appState.initialize();
setState(() {
_isInitialized = true;
});
} catch (error) {
debugPrint('App initialization error: $error');
setState(() {
_hasError = true;
_isInitialized = true;
});
}
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return const SplashScreen();
}
if (_hasError) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'アプリケーションの初期化に失敗しました',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_isInitialized = false;
_hasError = false;
});
_initializeApp();
},
child: const Text('再試行'),
),
],
),
),
);
}
return Consumer<AuthService>(
builder: (context, authService, child) {
if (authService.isLoggedIn) {
return const HomeScreen();
} else {
return const LoginScreen();
}
},
);
}
}
認証とユーザー管理
// lib/services/auth_service.dart - 認証サービス
import 'package:flutter/foundation.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthUser {
final String uid;
final String email;
final String displayName;
final String? photoURL;
final bool emailVerified;
final DateTime createdAt;
final DateTime lastSignInAt;
const AuthUser({
required this.uid,
required this.email,
required this.displayName,
this.photoURL,
required this.emailVerified,
required this.createdAt,
required this.lastSignInAt,
});
factory AuthUser.fromFirebaseUser(User user) {
return AuthUser(
uid: user.uid,
email: user.email ?? '',
displayName: user.displayName ?? 'Unknown User',
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdAt: user.metadata.creationTime ?? DateTime.now(),
lastSignInAt: user.metadata.lastSignInTime ?? DateTime.now(),
);
}
Map<String, dynamic> toJson() {
return {
'uid': uid,
'email': email,
'displayName': displayName,
'photoURL': photoURL,
'emailVerified': emailVerified,
'createdAt': createdAt.toIso8601String(),
'lastSignInAt': lastSignInAt.toIso8601String(),
};
}
factory AuthUser.fromJson(Map<String, dynamic> json) {
return AuthUser(
uid: json['uid'],
email: json['email'],
displayName: json['displayName'],
photoURL: json['photoURL'],
emailVerified: json['emailVerified'],
createdAt: DateTime.parse(json['createdAt']),
lastSignInAt: DateTime.parse(json['lastSignInAt']),
);
}
}
class AuthService extends ChangeNotifier {
static final AuthService _instance = AuthService._internal();
static AuthService get instance => _instance;
AuthService._internal();
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();
AuthUser? _currentUser;
bool _isInitialized = false;
AuthUser? get currentUser => _currentUser;
bool get isLoggedIn => _currentUser != null;
bool get isInitialized => _isInitialized;
// サービスの初期化
Future<void> initialize() async {
if (_isInitialized) return;
try {
// Firebase Auth の状態リスナー設定
_firebaseAuth.authStateChanges().listen(_onAuthStateChanged);
// 保存された認証状態の復元
await _restoreAuthState();
_isInitialized = true;
notifyListeners();
} catch (error) {
debugPrint('Auth service initialization error: $error');
rethrow;
}
}
// 認証状態変更リスナー
void _onAuthStateChanged(User? firebaseUser) async {
if (firebaseUser != null) {
_currentUser = AuthUser.fromFirebaseUser(firebaseUser);
await _saveAuthState();
} else {
_currentUser = null;
await _clearAuthState();
}
notifyListeners();
}
// 認証状態の保存
Future<void> _saveAuthState() async {
if (_currentUser != null) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_user', _currentUser!.toJson().toString());
}
}
// 認証状態の復元
Future<void> _restoreAuthState() async {
try {
final prefs = await SharedPreferences.getInstance();
final userString = prefs.getString('auth_user');
if (userString != null && _firebaseAuth.currentUser != null) {
_currentUser = AuthUser.fromFirebaseUser(_firebaseAuth.currentUser!);
}
} catch (error) {
debugPrint('Restore auth state error: $error');
}
}
// 認証状態のクリア
Future<void> _clearAuthState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_user');
}
// メール・パスワード登録
Future<AuthUser> registerWithEmailAndPassword({
required String email,
required String password,
required String displayName,
}) async {
try {
// パスワードの検証
if (password.length < 8) {
throw Exception('パスワードは8文字以上である必要があります');
}
// ユーザー作成
final userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// プロフィール更新
await userCredential.user!.updateDisplayName(displayName);
// メール確認の送信
await userCredential.user!.sendEmailVerification();
return AuthUser.fromFirebaseUser(userCredential.user!);
} on FirebaseAuthException catch (e) {
throw Exception(_getFirebaseErrorMessage(e.code));
} catch (error) {
throw Exception('登録に失敗しました: $error');
}
}
// メール・パスワードログイン
Future<AuthUser> signInWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
final userCredential = await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
return AuthUser.fromFirebaseUser(userCredential.user!);
} on FirebaseAuthException catch (e) {
throw Exception(_getFirebaseErrorMessage(e.code));
} catch (error) {
throw Exception('ログインに失敗しました: $error');
}
}
// Google ログイン
Future<AuthUser> signInWithGoogle() async {
try {
// Google Sign-In フロー
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
throw Exception('Googleログインがキャンセルされました');
}
// Google 認証情報の取得
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// Firebase 認証情報の作成
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// Firebase にサインイン
final userCredential = await _firebaseAuth.signInWithCredential(credential);
return AuthUser.fromFirebaseUser(userCredential.user!);
} catch (error) {
throw Exception('Googleログインに失敗しました: $error');
}
}
// Apple ログイン(iOS専用)
Future<AuthUser> signInWithApple() async {
try {
// Apple ID でのサインイン
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
// OAuthCredential の作成
final oauthCredential = OAuthProvider("apple.com").credential(
idToken: appleCredential.identityToken,
accessToken: appleCredential.authorizationCode,
);
// Firebase にサインイン
final userCredential = await _firebaseAuth.signInWithCredential(oauthCredential);
return AuthUser.fromFirebaseUser(userCredential.user!);
} catch (error) {
throw Exception('Apple ログインに失敗しました: $error');
}
}
// パスワードリセット
Future<void> sendPasswordResetEmail(String email) async {
try {
await _firebaseAuth.sendPasswordResetEmail(email: email);
} on FirebaseAuthException catch (e) {
throw Exception(_getFirebaseErrorMessage(e.code));
} catch (error) {
throw Exception('パスワードリセットに失敗しました: $error');
}
}
// メール確認の再送信
Future<void> sendEmailVerification() async {
try {
final user = _firebaseAuth.currentUser;
if (user != null && !user.emailVerified) {
await user.sendEmailVerification();
}
} catch (error) {
throw Exception('メール確認の送信に失敗しました: $error');
}
}
// プロフィール更新
Future<void> updateProfile({
String? displayName,
String? photoURL,
}) async {
try {
final user = _firebaseAuth.currentUser;
if (user != null) {
if (displayName != null) {
await user.updateDisplayName(displayName);
}
if (photoURL != null) {
await user.updatePhotoURL(photoURL);
}
// ローカル状態の更新
_currentUser = AuthUser.fromFirebaseUser(user);
await _saveAuthState();
notifyListeners();
}
} catch (error) {
throw Exception('プロフィール更新に失敗しました: $error');
}
}
// パスワード変更
Future<void> updatePassword(String newPassword) async {
try {
final user = _firebaseAuth.currentUser;
if (user != null) {
await user.updatePassword(newPassword);
}
} on FirebaseAuthException catch (e) {
throw Exception(_getFirebaseErrorMessage(e.code));
} catch (error) {
throw Exception('パスワード変更に失敗しました: $error');
}
}
// アカウント削除
Future<void> deleteAccount() async {
try {
final user = _firebaseAuth.currentUser;
if (user != null) {
await user.delete();
_currentUser = null;
await _clearAuthState();
notifyListeners();
}
} on FirebaseAuthException catch (e) {
if (e.code == 'requires-recent-login') {
throw Exception('セキュリティのため、再度ログインしてからアカウントを削除してください');
}
throw Exception(_getFirebaseErrorMessage(e.code));
} catch (error) {
throw Exception('アカウント削除に失敗しました: $error');
}
}
// サインアウト
Future<void> signOut() async {
try {
// Google Sign-In からもサインアウト
if (await _googleSignIn.isSignedIn()) {
await _googleSignIn.signOut();
}
// Firebase からサインアウト
await _firebaseAuth.signOut();
// ローカル状態をクリア
_currentUser = null;
await _clearAuthState();
notifyListeners();
} catch (error) {
throw Exception('サインアウトに失敗しました: $error');
}
}
// Firebase エラーメッセージの日本語化
String _getFirebaseErrorMessage(String errorCode) {
switch (errorCode) {
case 'user-not-found':
return 'ユーザーが見つかりません';
case 'wrong-password':
return 'パスワードが間違っています';
case 'email-already-in-use':
return 'このメールアドレスは既に使用されています';
case 'weak-password':
return 'パスワードが弱すぎます';
case 'invalid-email':
return 'メールアドレスの形式が正しくありません';
case 'user-disabled':
return 'このアカウントは無効になっています';
case 'too-many-requests':
return 'リクエストが多すぎます。しばらく待ってから再試行してください';
case 'operation-not-allowed':
return 'この操作は許可されていません';
case 'requires-recent-login':
return 'セキュリティのため、再度ログインが必要です';
default:
return '認証エラーが発生しました($errorCode)';
}
}
}
バックエンド統合とAPIアクセス
// lib/services/api_service.dart - API サービス
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class ApiResponse<T> {
final T data;
final int statusCode;
final String? message;
final bool success;
const ApiResponse({
required this.data,
required this.statusCode,
this.message,
required this.success,
});
factory ApiResponse.success(T data, {int statusCode = 200, String? message}) {
return ApiResponse(
data: data,
statusCode: statusCode,
message: message,
success: true,
);
}
factory ApiResponse.error(String message, {int statusCode = 500}) {
return ApiResponse(
data: null as T,
statusCode: statusCode,
message: message,
success: false,
);
}
}
class PaginatedResponse<T> {
final List<T> data;
final int page;
final int limit;
final int total;
final int pages;
const PaginatedResponse({
required this.data,
required this.page,
required this.limit,
required this.total,
required this.pages,
});
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
) {
return PaginatedResponse(
data: (json['data'] as List).map((item) => fromJsonT(item)).toList(),
page: json['page'],
limit: json['limit'],
total: json['total'],
pages: json['pages'],
);
}
}
class ApiService {
static final ApiService _instance = ApiService._internal();
static ApiService get instance => _instance;
ApiService._internal();
static const String _baseUrl = kDebugMode
? 'http://localhost:3000/api'
: 'https://api.yourapp.com';
static const Duration _timeout = Duration(seconds: 30);
String? _authToken;
late http.Client _httpClient;
// サービスの初期化
Future<void> initialize() async {
_httpClient = http.Client();
await _loadAuthToken();
}
// 認証トークンの読み込み
Future<void> _loadAuthToken() async {
try {
final prefs = await SharedPreferences.getInstance();
_authToken = prefs.getString('auth_token');
} catch (error) {
debugPrint('Load auth token error: $error');
}
}
// 認証トークンの設定
Future<void> setAuthToken(String? token) async {
_authToken = token;
try {
final prefs = await SharedPreferences.getInstance();
if (token != null) {
await prefs.setString('auth_token', token);
} else {
await prefs.remove('auth_token');
}
} catch (error) {
debugPrint('Save auth token error: $error');
}
}
// リクエストヘッダーの構築
Map<String, String> _buildHeaders({Map<String, String>? additionalHeaders}) {
final headers = <String, String>{
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (_authToken != null) {
headers['Authorization'] = 'Bearer $_authToken';
}
if (additionalHeaders != null) {
headers.addAll(additionalHeaders);
}
return headers;
}
// HTTPリクエストの実行
Future<ApiResponse<T>> _executeRequest<T>(
String method,
String endpoint,
{
Map<String, dynamic>? body,
Map<String, String>? headers,
T Function(dynamic)? fromJson,
}
) async {
try {
final uri = Uri.parse('$_baseUrl$endpoint');
final requestHeaders = _buildHeaders(additionalHeaders: headers);
http.Response response;
switch (method.toUpperCase()) {
case 'GET':
response = await _httpClient
.get(uri, headers: requestHeaders)
.timeout(_timeout);
break;
case 'POST':
response = await _httpClient
.post(
uri,
headers: requestHeaders,
body: body != null ? jsonEncode(body) : null,
)
.timeout(_timeout);
break;
case 'PUT':
response = await _httpClient
.put(
uri,
headers: requestHeaders,
body: body != null ? jsonEncode(body) : null,
)
.timeout(_timeout);
break;
case 'PATCH':
response = await _httpClient
.patch(
uri,
headers: requestHeaders,
body: body != null ? jsonEncode(body) : null,
)
.timeout(_timeout);
break;
case 'DELETE':
response = await _httpClient
.delete(uri, headers: requestHeaders)
.timeout(_timeout);
break;
default:
throw Exception('Unsupported HTTP method: $method');
}
return _handleResponse<T>(response, fromJson);
} on SocketException {
return ApiResponse.error('ネットワーク接続エラーが発生しました');
} on HttpException {
return ApiResponse.error('HTTPエラーが発生しました');
} on FormatException {
return ApiResponse.error('データの形式が正しくありません');
} catch (error) {
return ApiResponse.error('予期しないエラーが発生しました: $error');
}
}
// レスポンスの処理
ApiResponse<T> _handleResponse<T>(
http.Response response,
T Function(dynamic)? fromJson,
) {
try {
final responseData = jsonDecode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
T data;
if (fromJson != null) {
data = fromJson(responseData['data'] ?? responseData);
} else {
data = (responseData['data'] ?? responseData) as T;
}
return ApiResponse.success(
data,
statusCode: response.statusCode,
message: responseData['message'],
);
} else {
return ApiResponse.error(
responseData['message'] ?? 'サーバーエラーが発生しました',
statusCode: response.statusCode,
);
}
} catch (error) {
return ApiResponse.error(
'レスポンスの解析に失敗しました: $error',
statusCode: response.statusCode,
);
}
}
// GET リクエスト
Future<ApiResponse<T>> get<T>(
String endpoint, {
Map<String, String>? queryParams,
T Function(dynamic)? fromJson,
}) async {
String url = endpoint;
if (queryParams != null && queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
.join('&');
url += '?$queryString';
}
return _executeRequest<T>('GET', url, fromJson: fromJson);
}
// POST リクエスト
Future<ApiResponse<T>> post<T>(
String endpoint, {
Map<String, dynamic>? body,
T Function(dynamic)? fromJson,
}) async {
return _executeRequest<T>('POST', endpoint, body: body, fromJson: fromJson);
}
// PUT リクエスト
Future<ApiResponse<T>> put<T>(
String endpoint, {
Map<String, dynamic>? body,
T Function(dynamic)? fromJson,
}) async {
return _executeRequest<T>('PUT', endpoint, body: body, fromJson: fromJson);
}
// PATCH リクエスト
Future<ApiResponse<T>> patch<T>(
String endpoint, {
Map<String, dynamic>? body,
T Function(dynamic)? fromJson,
}) async {
return _executeRequest<T>('PATCH', endpoint, body: body, fromJson: fromJson);
}
// DELETE リクエスト
Future<ApiResponse<T>> delete<T>(
String endpoint, {
T Function(dynamic)? fromJson,
}) async {
return _executeRequest<T>('DELETE', endpoint, fromJson: fromJson);
}
// ファイルアップロード
Future<ApiResponse<T>> uploadFile<T>(
String endpoint,
File file, {
Map<String, String>? fields,
T Function(dynamic)? fromJson,
}) async {
try {
final uri = Uri.parse('$_baseUrl$endpoint');
final request = http.MultipartRequest('POST', uri);
// ヘッダーの設定
if (_authToken != null) {
request.headers['Authorization'] = 'Bearer $_authToken';
}
// ファイルの追加
request.files.add(await http.MultipartFile.fromPath('file', file.path));
// フィールドの追加
if (fields != null) {
request.fields.addAll(fields);
}
final streamedResponse = await request.send().timeout(_timeout);
final response = await http.Response.fromStream(streamedResponse);
return _handleResponse<T>(response, fromJson);
} catch (error) {
return ApiResponse.error('ファイルアップロードに失敗しました: $error');
}
}
// サービスの終了処理
void dispose() {
_httpClient.close();
}
}
// 投稿モデル
class Post {
final String id;
final String title;
final String content;
final String authorId;
final String authorName;
final DateTime createdAt;
final DateTime updatedAt;
final int likes;
final int comments;
final List<String> tags;
const Post({
required this.id,
required this.title,
required this.content,
required this.authorId,
required this.authorName,
required this.createdAt,
required this.updatedAt,
required this.likes,
required this.comments,
required this.tags,
});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
content: json['content'],
authorId: json['authorId'],
authorName: json['authorName'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
likes: json['likes'],
comments: json['comments'],
tags: List<String>.from(json['tags']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'authorId': authorId,
'authorName': authorName,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'likes': likes,
'comments': comments,
'tags': tags,
};
}
}
// 投稿 API サービス
class PostApiService {
static final ApiService _apiService = ApiService.instance;
// 投稿一覧取得(ページネーション付き)
static Future<ApiResponse<PaginatedResponse<Post>>> getPosts({
int page = 1,
int limit = 10,
}) async {
final response = await _apiService.get<Map<String, dynamic>>(
'/posts',
queryParams: {
'page': page.toString(),
'limit': limit.toString(),
},
);
if (response.success) {
final paginatedPosts = PaginatedResponse.fromJson(
response.data,
(json) => Post.fromJson(json),
);
return ApiResponse.success(paginatedPosts);
} else {
return ApiResponse.error(response.message ?? 'Failed to fetch posts');
}
}
// 投稿詳細取得
static Future<ApiResponse<Post>> getPost(String postId) async {
return _apiService.get<Post>(
'/posts/$postId',
fromJson: (json) => Post.fromJson(json),
);
}
// 投稿作成
static Future<ApiResponse<Post>> createPost({
required String title,
required String content,
required List<String> tags,
}) async {
return _apiService.post<Post>(
'/posts',
body: {
'title': title,
'content': content,
'tags': tags,
},
fromJson: (json) => Post.fromJson(json),
);
}
// 投稿更新
static Future<ApiResponse<Post>> updatePost(
String postId,
Map<String, dynamic> updates,
) async {
return _apiService.put<Post>(
'/posts/$postId',
body: updates,
fromJson: (json) => Post.fromJson(json),
);
}
// 投稿削除
static Future<ApiResponse<void>> deletePost(String postId) async {
return _apiService.delete<void>('/posts/$postId');
}
// 投稿いいね
static Future<ApiResponse<void>> likePost(String postId) async {
return _apiService.post<void>('/posts/$postId/like');
}
// 投稿いいね解除
static Future<ApiResponse<void>> unlikePost(String postId) async {
return _apiService.delete<void>('/posts/$postId/like');
}
// 投稿検索
static Future<ApiResponse<List<Post>>> searchPosts({
required String query,
List<String>? tags,
}) async {
final queryParams = <String, String>{'q': query};
if (tags != null && tags.isNotEmpty) {
queryParams['tags'] = tags.join(',');
}
final response = await _apiService.get<List<dynamic>>(
'/posts/search',
queryParams: queryParams,
);
if (response.success) {
final posts = response.data.map((json) => Post.fromJson(json)).toList();
return ApiResponse.success(posts);
} else {
return ApiResponse.error(response.message ?? 'Search failed');
}
}
}
プッシュ通知とメッセージング
// lib/services/notification_service.dart - プッシュ通知サービス
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
// バックグラウンド メッセージハンドラー(トップレベル関数である必要があります)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
debugPrint('Background message received: ${message.messageId}');
// バックグラウンドでの通知処理
await NotificationService.instance._showLocalNotification(
title: message.notification?.title ?? 'New Message',
body: message.notification?.body ?? 'You have a new message',
data: message.data,
);
}
class NotificationData {
final String title;
final String body;
final Map<String, dynamic>? data;
final String? imageUrl;
final DateTime? scheduledTime;
const NotificationData({
required this.title,
required this.body,
this.data,
this.imageUrl,
this.scheduledTime,
});
Map<String, dynamic> toJson() {
return {
'title': title,
'body': body,
'data': data,
'imageUrl': imageUrl,
'scheduledTime': scheduledTime?.toIso8601String(),
};
}
factory NotificationData.fromJson(Map<String, dynamic> json) {
return NotificationData(
title: json['title'],
body: json['body'],
data: json['data'],
imageUrl: json['imageUrl'],
scheduledTime: json['scheduledTime'] != null
? DateTime.parse(json['scheduledTime'])
: null,
);
}
}
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
static NotificationService get instance => _instance;
NotificationService._internal();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
String? _fcmToken;
// 通知タップ時のコールバック
Function(Map<String, dynamic>)? onNotificationTapped;
Function(RemoteMessage)? onForegroundMessage;
bool get isInitialized => _isInitialized;
String? get fcmToken => _fcmToken;
// サービスの初期化
Future<void> initialize() async {
if (_isInitialized) return;
try {
// 権限のリクエスト
await _requestPermissions();
// ローカル通知の初期化
await _initializeLocalNotifications();
// Firebase Messaging の初期化
await _initializeFirebaseMessaging();
_isInitialized = true;
debugPrint('Notification service initialized successfully');
} catch (error) {
debugPrint('Notification service initialization failed: $error');
rethrow;
}
}
// 通知権限のリクエスト
Future<void> _requestPermissions() async {
// Firebase Messaging の権限リクエスト
final settings = await _firebaseMessaging.requestPermission(
alert: true,
badge: true,
sound: true,
carPlay: false,
criticalAlert: false,
provisional: false,
announcement: false,
);
debugPrint('Notification permission status: ${settings.authorizationStatus}');
if (settings.authorizationStatus == AuthorizationStatus.denied) {
throw Exception('通知の許可が拒否されました');
}
}
// ローカル通知の初期化
Future<void> _initializeLocalNotifications() async {
// Android 初期化設定
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS 初期化設定
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
// 初期化設定
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
// 通知タップ時の処理
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
_handleNotificationTap(response);
},
);
// Android 通知チャンネルの作成
if (Platform.isAndroid) {
await _createNotificationChannels();
}
}
// Firebase Messaging の初期化
Future<void> _initializeFirebaseMessaging() async {
// バックグラウンド メッセージハンドラーの設定
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// FCM トークンの取得
_fcmToken = await _firebaseMessaging.getToken();
debugPrint('FCM Token: $_fcmToken');
// トークンをサーバーに登録
if (_fcmToken != null) {
await _registerTokenWithServer(_fcmToken!);
}
// トークン更新リスナー
_firebaseMessaging.onTokenRefresh.listen((newToken) {
_fcmToken = newToken;
debugPrint('FCM Token refreshed: $newToken');
_registerTokenWithServer(newToken);
});
// フォアグラウンド メッセージリスナー
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
debugPrint('Foreground message received: ${message.messageId}');
_handleForegroundMessage(message);
});
// 通知タップでアプリが開かれた場合
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint('Notification opened app: ${message.messageId}');
_handleNotificationOpen(message);
});
// アプリ起動時の初期通知チェック
final RemoteMessage? initialMessage =
await _firebaseMessaging.getInitialMessage();
if (initialMessage != null) {
debugPrint('App opened from notification: ${initialMessage.messageId}');
_handleNotificationOpen(initialMessage);
}
}
// Android 通知チャンネルの作成
Future<void> _createNotificationChannels() async {
const androidNotificationChannels = [
AndroidNotificationChannel(
'default_channel',
'Default Notifications',
description: 'Default notification channel',
importance: Importance.high,
enableVibration: true,
enableLights: true,
),
AndroidNotificationChannel(
'important_channel',
'Important Notifications',
description: 'Important notification channel',
importance: Importance.max,
enableVibration: true,
enableLights: true,
sound: RawResourceAndroidNotificationSound('notification_important'),
),
];
for (final channel in androidNotificationChannels) {
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
}
// フォアグラウンド メッセージの処理
void _handleForegroundMessage(RemoteMessage message) {
// ローカル通知として表示
_showLocalNotification(
title: message.notification?.title ?? 'New Message',
body: message.notification?.body ?? 'You have a new message',
data: message.data,
);
// カスタムハンドラーがあれば実行
onForegroundMessage?.call(message);
}
// 通知タップの処理
void _handleNotificationTap(NotificationResponse response) {
try {
Map<String, dynamic> data = {};
if (response.payload != null) {
data = jsonDecode(response.payload!);
}
onNotificationTapped?.call(data);
} catch (error) {
debugPrint('Handle notification tap error: $error');
}
}
// 通知オープンの処理
void _handleNotificationOpen(RemoteMessage message) {
onNotificationTapped?.call(message.data);
}
// ローカル通知の表示
Future<void> _showLocalNotification({
required String title,
required String body,
Map<String, dynamic>? data,
String? imageUrl,
int? id,
}) async {
try {
// 通知ID の生成
final notificationId = id ?? DateTime.now().millisecondsSinceEpoch.remainder(100000);
// Android 通知詳細
AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
'default_channel',
'Default Notifications',
channelDescription: 'Default notification channel',
importance: Importance.high,
priority: Priority.high,
enableVibration: true,
enableLights: true,
icon: '@mipmap/ic_launcher',
largeIcon: imageUrl != null
? NetworkImageAndroidBitmap(imageUrl)
: null,
);
// iOS 通知詳細
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
// 通知詳細
NotificationDetails notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
// 通知の表示
await _localNotifications.show(
notificationId,
title,
body,
notificationDetails,
payload: data != null ? jsonEncode(data) : null,
);
} catch (error) {
debugPrint('Show local notification error: $error');
}
}
// スケジュール通知
Future<void> scheduleNotification({
required NotificationData notification,
required DateTime scheduledTime,
}) async {
try {
final notificationId = DateTime.now().millisecondsSinceEpoch.remainder(100000);
AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
'default_channel',
'Default Notifications',
channelDescription: 'Scheduled notifications',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _localNotifications.zonedSchedule(
notificationId,
notification.title,
notification.body,
scheduledTime,
notificationDetails,
payload: jsonEncode(notification.data ?? {}),
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
debugPrint('Notification scheduled for: $scheduledTime');
} catch (error) {
debugPrint('Schedule notification error: $error');
}
}
// 即座にローカル通知を送信
Future<void> showLocalNotification(NotificationData notification) async {
await _showLocalNotification(
title: notification.title,
body: notification.body,
data: notification.data,
imageUrl: notification.imageUrl,
);
}
// 通知のキャンセル
Future<void> cancelNotification(int notificationId) async {
await _localNotifications.cancel(notificationId);
}
// 全通知のキャンセル
Future<void> cancelAllNotifications() async {
await _localNotifications.cancelAll();
}
// トピックの購読
Future<void> subscribeToTopic(String topic) async {
try {
await _firebaseMessaging.subscribeToTopic(topic);
debugPrint('Subscribed to topic: $topic');
} catch (error) {
debugPrint('Subscribe to topic error: $error');
}
}
// トピックの購読解除
Future<void> unsubscribeFromTopic(String topic) async {
try {
await _firebaseMessaging.unsubscribeFromTopic(topic);
debugPrint('Unsubscribed from topic: $topic');
} catch (error) {
debugPrint('Unsubscribe from topic error: $error');
}
}
// バッジカウントの設定(iOS)
Future<void> setBadgeCount(int count) async {
if (Platform.isIOS) {
await _firebaseMessaging.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
}
}
// FCM トークンをサーバーに登録
Future<void> _registerTokenWithServer(String token) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('fcm_token', token);
// API サーバーにトークンを送信
// await ApiService.instance.post('/users/fcm-token', body: {'token': token});
debugPrint('FCM token registered with server');
} catch (error) {
debugPrint('Register FCM token error: $error');
}
}
// 通知権限の状態確認
Future<AuthorizationStatus> getPermissionStatus() async {
final settings = await _firebaseMessaging.getNotificationSettings();
return settings.authorizationStatus;
}
// 通知設定画面を開く
Future<void> openNotificationSettings() async {
await _firebaseMessaging.requestPermission();
}
// サービスの終了処理
void dispose() {
// 必要に応じてリスナーの解除などを行う
}
}
分析とパフォーマンス監視
// lib/services/analytics_service.dart - アナリティクスサービス
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AnalyticsEvent {
final String name;
final Map<String, Object?>? parameters;
final DateTime timestamp;
AnalyticsEvent({
required this.name,
this.parameters,
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();
Map<String, Object?> toJson() {
return {
'name': name,
'parameters': parameters,
'timestamp': timestamp.toIso8601String(),
};
}
}
class AnalyticsService {
static final AnalyticsService _instance = AnalyticsService._internal();
static AnalyticsService get instance => _instance;
AnalyticsService._internal();
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;
final FirebasePerformance _performance = FirebasePerformance.instance;
bool _isInitialized = false;
String? _userId;
final Map<String, Trace> _activeTraces = {};
final DateTime _sessionStartTime = DateTime.now();
bool get isInitialized => _isInitialized;
// サービスの初期化
Future<void> initialize() async {
if (_isInitialized) return;
try {
// Crashlytics の有効化
await _crashlytics.setCrashlyticsCollectionEnabled(true);
// Flutter フレームワークエラーの捕捉
FlutterError.onError = (FlutterErrorDetails details) {
_crashlytics.recordFlutterFatalError(details);
};
// プラットフォームエラーの捕捉
PlatformDispatcher.instance.onError = (error, stack) {
_crashlytics.recordError(error, stack, fatal: true);
return true;
};
// デバイス情報の設定
await _setDeviceProperties();
// アプリ開始イベントの送信
await _trackAppStart();
_isInitialized = true;
debugPrint('Analytics service initialized successfully');
} catch (error) {
debugPrint('Analytics service initialization failed: $error');
rethrow;
}
}
// デバイス情報の設定
Future<void> _setDeviceProperties() async {
try {
final deviceInfo = DeviceInfoPlugin();
final packageInfo = await PackageInfo.fromPlatform();
Map<String, String> properties = {
'app_version': packageInfo.version,
'build_number': packageInfo.buildNumber,
'package_name': packageInfo.packageName,
'platform': Platform.operatingSystem,
};
if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
properties.addAll({
'device_model': androidInfo.model,
'device_manufacturer': androidInfo.manufacturer,
'android_version': androidInfo.version.release,
'sdk_int': androidInfo.version.sdkInt.toString(),
});
} else if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
properties.addAll({
'device_model': iosInfo.model,
'device_name': iosInfo.name,
'ios_version': iosInfo.systemVersion,
'is_physical_device': iosInfo.isPhysicalDevice.toString(),
});
}
// Firebase Analytics にユーザープロパティとして設定
for (final entry in properties.entries) {
await _analytics.setUserProperty(
name: entry.key,
value: entry.value,
);
}
// Crashlytics にカスタムキーとして設定
for (final entry in properties.entries) {
await _crashlytics.setCustomKey(entry.key, entry.value);
}
debugPrint('Device properties set: $properties');
} catch (error) {
debugPrint('Set device properties error: $error');
}
}
// アプリ開始の追跡
Future<void> _trackAppStart() async {
try {
final startupTime = DateTime.now().difference(_sessionStartTime);
await trackEvent(AnalyticsEvent(
name: 'app_start',
parameters: {
'startup_time_ms': startupTime.inMilliseconds,
'session_id': _generateSessionId(),
},
));
// パフォーマンストレース
final trace = _performance.newTrace('app_startup');
await trace.start();
trace.setMetric('startup_time_ms', startupTime.inMilliseconds);
await trace.stop();
} catch (error) {
debugPrint('Track app start error: $error');
}
}
// カスタムイベントの追跡
Future<void> trackEvent(AnalyticsEvent event) async {
try {
await _analytics.logEvent(
name: event.name,
parameters: event.parameters,
);
debugPrint('Event tracked: ${event.name} - ${event.parameters}');
} catch (error) {
debugPrint('Track event error: $error');
recordError(error, additionalInfo: {'event': event.name});
}
}
// 画面表示の追跡
Future<void> trackScreenView({
required String screenName,
String? screenClass,
Map<String, Object?>? parameters,
}) async {
try {
await _analytics.logScreenView(
screenName: screenName,
screenClass: screenClass ?? screenName,
);
// カスタムイベントとしても記録
await trackEvent(AnalyticsEvent(
name: 'screen_view',
parameters: {
'screen_name': screenName,
'screen_class': screenClass ?? screenName,
...?parameters,
},
));
debugPrint('Screen view tracked: $screenName');
} catch (error) {
debugPrint('Track screen view error: $error');
}
}
// ユーザーIDの設定
Future<void> setUserId(String userId) async {
try {
_userId = userId;
await _analytics.setUserId(id: userId);
await _crashlytics.setUserIdentifier(userId);
debugPrint('User ID set: $userId');
} catch (error) {
debugPrint('Set user ID error: $error');
}
}
// ユーザープロパティの設定
Future<void> setUserProperties(Map<String, String> properties) async {
try {
for (final entry in properties.entries) {
await _analytics.setUserProperty(
name: entry.key,
value: entry.value,
);
await _crashlytics.setCustomKey(entry.key, entry.value);
}
debugPrint('User properties set: $properties');
} catch (error) {
debugPrint('Set user properties error: $error');
}
}
// 購入イベントの追跡
Future<void> trackPurchase({
required String transactionId,
required double value,
required String currency,
required List<Map<String, Object?>> items,
}) async {
try {
await _analytics.logPurchase(
currency: currency,
value: value,
transactionId: transactionId,
parameters: {
'items': items,
},
);
// 個別アイテムイベント
for (final item in items) {
await _analytics.logSelectItem(
itemListId: 'purchase_items',
itemListName: 'Purchase Items',
parameters: item,
);
}
debugPrint('Purchase tracked: $transactionId - $value $currency');
} catch (error) {
debugPrint('Track purchase error: $error');
}
}
// ユーザーエンゲージメントの追跡
Future<void> trackUserEngagement({
required String action,
int? durationMs,
Map<String, Object?>? additionalParameters,
}) async {
try {
final parameters = <String, Object?>{
'action': action,
...?additionalParameters,
};
if (durationMs != null) {
parameters['engagement_time_msec'] = durationMs;
}
await trackEvent(AnalyticsEvent(
name: 'user_engagement',
parameters: parameters,
));
} catch (error) {
debugPrint('Track user engagement error: $error');
}
}
// パフォーマンストレースの開始
Future<bool> startPerformanceTrace(String traceName) async {
try {
if (_activeTraces.containsKey(traceName)) {
debugPrint('Trace already active: $traceName');
return false;
}
final trace = _performance.newTrace(traceName);
await trace.start();
_activeTraces[traceName] = trace;
debugPrint('Performance trace started: $traceName');
return true;
} catch (error) {
debugPrint('Start performance trace error: $error');
return false;
}
}
// パフォーマンストレースの停止
Future<void> stopPerformanceTrace(
String traceName, {
Map<String, int>? metrics,
Map<String, String>? attributes,
}) async {
try {
final trace = _activeTraces.remove(traceName);
if (trace == null) {
debugPrint('Trace not found: $traceName');
return;
}
// メトリクスの追加
metrics?.forEach((key, value) {
trace.setMetric(key, value);
});
// 属性の追加
attributes?.forEach((key, value) {
trace.putAttribute(key, value);
});
await trace.stop();
debugPrint('Performance trace stopped: $traceName');
} catch (error) {
debugPrint('Stop performance trace error: $error');
}
}
// 非同期処理のパフォーマンス測定
Future<T> measureAsync<T>(
String traceName,
Future<T> Function() operation,
) async {
await startPerformanceTrace(traceName);
try {
final result = await operation();
await stopPerformanceTrace(traceName, metrics: {'success': 1});
return result;
} catch (error) {
await stopPerformanceTrace(traceName, metrics: {'success': 0});
rethrow;
}
}
// エラーの記録
void recordError(
dynamic error, {
StackTrace? stackTrace,
bool fatal = false,
Map<String, String>? additionalInfo,
}) {
try {
// 追加情報の設定
additionalInfo?.forEach((key, value) {
_crashlytics.setCustomKey(key, value);
});
// エラーの記録
_crashlytics.recordError(
error,
stackTrace,
fatal: fatal,
);
debugPrint('Error recorded: $error');
} catch (e) {
debugPrint('Record error failed: $e');
}
}
// カスタムログの記録
void log(String message, {String level = 'info'}) {
try {
_crashlytics.log('[$level] $message');
debugPrint('Custom log: $message');
} catch (error) {
debugPrint('Custom log error: $error');
}
}
// ネットワークリクエストの追跡
Future<void> trackNetworkRequest({
required String url,
required String method,
required int statusCode,
required int durationMs,
int? responseSize,
}) async {
try {
await trackEvent(AnalyticsEvent(
name: 'network_request',
parameters: {
'url': url,
'method': method,
'status_code': statusCode,
'duration_ms': durationMs,
'response_size': responseSize ?? 0,
'success': statusCode >= 200 && statusCode < 300 ? 1 : 0,
},
));
} catch (error) {
debugPrint('Track network request error: $error');
}
}
// セッション時間の取得
Duration getSessionDuration() {
return DateTime.now().difference(_sessionStartTime);
}
// セッションID の生成
String _generateSessionId() {
return 'session_${_sessionStartTime.millisecondsSinceEpoch}_${DateTime.now().millisecondsSinceEpoch}';
}
// サービスの終了処理
Future<void> dispose() async {
try {
// アクティブなトレースの停止
for (final traceName in _activeTraces.keys.toList()) {
await stopPerformanceTrace(traceName);
}
// セッション終了イベント
await trackEvent(AnalyticsEvent(
name: 'session_end',
parameters: {
'session_duration_ms': getSessionDuration().inMilliseconds,
},
));
debugPrint('Analytics service disposed');
} catch (error) {
debugPrint('Analytics dispose error: $error');
}
}
// クラッシュテスト(開発環境のみ)
void testCrash() {
if (kDebugMode) {
_crashlytics.crash();
}
}
}
// パフォーマンス監視のユーティリティ
class PerformanceMonitor {
static final AnalyticsService _analytics = AnalyticsService.instance;
// API 呼び出しの測定
static Future<T> measureApiCall<T>(
String apiName,
Future<T> Function() apiCall,
) async {
return _analytics.measureAsync('api_$apiName', apiCall);
}
// 画面レンダリング時間の測定
static Future<void> measureScreenRender(
String screenName,
Future<void> Function() renderFunction,
) async {
await _analytics.measureAsync('screen_render_$screenName', renderFunction);
}
// データベース操作の測定
static Future<T> measureDatabaseOperation<T>(
String operation,
Future<T> Function() dbOperation,
) async {
return _analytics.measureAsync('db_$operation', dbOperation);
}
// ファイル操作の測定
static Future<T> measureFileOperation<T>(
String operation,
Future<T> Function() fileOperation,
) async {
return _analytics.measureAsync('file_$operation', fileOperation);
}
}
デプロイメントと設定
# pubspec.yaml - プロジェクト設定ファイル
name: my_awesome_app
description: A comprehensive Flutter application with all platform support
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
# UI & Navigation
cupertino_icons: ^1.0.6
material_symbols_icons: ^4.2719.3
# State Management
provider: ^6.1.1
flutter_riverpod: ^2.4.9
# Firebase
firebase_core: ^2.24.2
firebase_auth: ^4.15.3
firebase_analytics: ^10.7.4
firebase_crashlytics: ^3.4.8
firebase_messaging: ^14.7.10
firebase_performance: ^0.9.3+8
cloud_firestore: ^4.13.6
firebase_storage: ^11.5.6
# Authentication
google_sign_in: ^6.1.6
sign_in_with_apple: ^5.0.0
# Network & API
http: ^1.1.2
dio: ^5.4.0
# Local Storage
shared_preferences: ^2.2.2
sqflite: ^2.3.0
hive: ^2.2.3
hive_flutter: ^1.1.0
# UI Components
cached_network_image: ^3.3.0
image_picker: ^1.0.4
file_picker: ^6.1.1
photo_view: ^0.14.0
flutter_staggered_grid_view: ^0.7.0
pull_to_refresh: ^2.0.0
# Utils
path_provider: ^2.1.1
url_launcher: ^6.2.2
share_plus: ^7.2.1
device_info_plus: ^9.1.1
package_info_plus: ^5.0.1
connectivity_plus: ^5.0.2
# Notifications
flutter_local_notifications: ^16.3.0
# Internationalization
intl: ^0.19.0
flutter_localizations:
sdk: flutter
# Animation
lottie: ^2.7.0
flutter_animate: ^4.3.0
# Development Tools
logger: ^2.0.2+1
dev_dependencies:
flutter_test:
sdk: flutter
# Code Quality
flutter_lints: ^3.0.1
very_good_analysis: ^5.1.0
# Code Generation
build_runner: ^2.4.7
json_annotation: ^4.8.1
json_serializable: ^6.7.1
hive_generator: ^2.0.1
# Testing
mockito: ^5.4.2
integration_test:
sdk: flutter
# Platform-specific configurations
flutter:
uses-material-design: true
# Assets
assets:
- assets/images/
- assets/icons/
- assets/animations/
- assets/fonts/
# Fonts
fonts:
- family: NotoSans
fonts:
- asset: assets/fonts/NotoSans-Regular.ttf
- asset: assets/fonts/NotoSans-Bold.ttf
weight: 700
# Platform configurations
flutter:
platforms:
android:
embedding: v2
ios:
deployment_target: '12.0'
macos:
deployment_target: '10.14'
windows:
deployment_target: '10.0.17763.0'
web:
compiler: dart2js
#!/bin/bash
# scripts/build_and_deploy.sh - ビルド・デプロイ自動化スクリプト
set -e
echo "🚀 Flutter アプリケーション ビルド・デプロイ開始"
# バージョン情報の取得
VERSION=$(grep "version:" pubspec.yaml | cut -d ' ' -f 2)
echo "📱 アプリバージョン: $VERSION"
# 環境チェック
echo "🔍 環境チェック中..."
flutter doctor
# 依存関係の取得
echo "📦 依存関係を取得中..."
flutter pub get
# コード生成
echo "🔨 コード生成中..."
flutter packages pub run build_runner build --delete-conflicting-outputs
# テスト実行
echo "🧪 テスト実行中..."
flutter test
# 静的解析
echo "🔍 静的解析実行中..."
flutter analyze
# プラットフォーム別ビルド
PLATFORM=$1
BUILD_TYPE=${2:-release}
case $PLATFORM in
"android")
echo "🤖 Android ビルド開始..."
# Android アプリバンドルの生成
flutter build appbundle --$BUILD_TYPE
# APK の生成
flutter build apk --$BUILD_TYPE
# ファイルサイズの確認
BUNDLE_SIZE=$(du -h build/app/outputs/bundle/${BUILD_TYPE}Release/app-${BUILD_TYPE}.aab | cut -f1)
APK_SIZE=$(du -h build/app/outputs/flutter-apk/app-${BUILD_TYPE}.apk | cut -f1)
echo "📱 Android アプリバンドルサイズ: $BUNDLE_SIZE"
echo "📱 Android APKサイズ: $APK_SIZE"
# ビルド成果物の移動
mkdir -p builds/android
cp build/app/outputs/bundle/${BUILD_TYPE}Release/app-${BUILD_TYPE}.aab builds/android/
cp build/app/outputs/flutter-apk/app-${BUILD_TYPE}.apk builds/android/
;;
"ios")
echo "🍎 iOS ビルド開始..."
# iOS アーカイブの作成
flutter build ios --$BUILD_TYPE --no-codesign
# Xcode アーカイブ
cd ios
xcodebuild -workspace Runner.xcworkspace \
-scheme Runner \
-configuration Release \
-destination generic/platform=iOS \
-archivePath build/Runner.xcarchive \
archive
# IPA エクスポート
xcodebuild -exportArchive \
-archivePath build/Runner.xcarchive \
-exportPath build \
-exportOptionsPlist ExportOptions.plist
cd ..
# ビルド成果物の移動
mkdir -p builds/ios
cp ios/build/Runner.ipa builds/ios/ 2>/dev/null || echo "IPA ファイルが見つかりません"
;;
"web")
echo "🌐 Web ビルド開始..."
# Web ビルド
flutter build web --$BUILD_TYPE --web-renderer canvaskit
# ビルド成果物の移動
mkdir -p builds/web
cp -r build/web/* builds/web/
# ビルドサイズの確認
WEB_SIZE=$(du -sh builds/web | cut -f1)
echo "🌐 Web ビルドサイズ: $WEB_SIZE"
;;
"windows")
echo "🪟 Windows ビルド開始..."
# Windows ビルド
flutter build windows --$BUILD_TYPE
# ビルド成果物の移動
mkdir -p builds/windows
cp -r build/windows/runner/Release/* builds/windows/
# ビルドサイズの確認
WINDOWS_SIZE=$(du -sh builds/windows | cut -f1)
echo "🪟 Windows ビルドサイズ: $WINDOWS_SIZE"
;;
"macos")
echo "🍎 macOS ビルド開始..."
# macOS ビルド
flutter build macos --$BUILD_TYPE
# ビルド成果物の移動
mkdir -p builds/macos
cp -r build/macos/Build/Products/Release/my_awesome_app.app builds/macos/
# ビルドサイズの確認
MACOS_SIZE=$(du -sh builds/macos | cut -f1)
echo "🍎 macOS ビルドサイズ: $MACOS_SIZE"
;;
"linux")
echo "🐧 Linux ビルド開始..."
# Linux ビルド
flutter build linux --$BUILD_TYPE
# ビルド成果物の移動
mkdir -p builds/linux
cp -r build/linux/x64/release/bundle/* builds/linux/
# ビルドサイズの確認
LINUX_SIZE=$(du -sh builds/linux | cut -f1)
echo "🐧 Linux ビルドサイズ: $LINUX_SIZE"
;;
"all")
echo "🚀 全プラットフォーム ビルド開始..."
# 各プラットフォームのビルドを順次実行
for platform in android ios web windows macos linux; do
echo "ビルド中: $platform"
./scripts/build_and_deploy.sh $platform $BUILD_TYPE
done
;;
*)
echo "❌ 不正なプラットフォーム: $PLATFORM"
echo "使用方法: ./scripts/build_and_deploy.sh [android|ios|web|windows|macos|linux|all] [debug|profile|release]"
exit 1
;;
esac
# リリースノートの生成
echo "📝 リリースノート生成中..."
cat > "builds/release-notes-v$VERSION.md" << EOF
# Release Notes v$VERSION
## 📱 アプリケーション情報
- バージョン: $VERSION
- ビルド日時: $(date)
- Flutter バージョン: $(flutter --version | head -n1)
- Dart バージョン: $(dart --version | cut -d' ' -f4)
## 📦 ビルド成果物
- プラットフォーム: $PLATFORM
- ビルドタイプ: $BUILD_TYPE
## 🧪 品質保証
- ✅ 静的解析チェック完了
- ✅ 単体テスト完了
- ✅ ビルド成功
## 📁 ビルドファイル
builds/ディレクトリを確認してください。
---
自動生成されたリリースノートです。
EOF
echo "🎉 ビルド完了!"
echo "📄 リリースノート: builds/release-notes-v$VERSION.md"
echo "📱 ビルドファイル: builds/ ディレクトリを確認してください"
# 成功通知(macOSの場合)
if [[ "$OSTYPE" == "darwin"* ]]; then
osascript -e "display notification \"Flutter アプリのビルドが完了しました!\" with title \"🎉 ビルド成功\""
fi