debug:flutter について
Flutter開発者向けの実用的なデバッグガイドで、レイアウト、状態、プラットフォーム、パフォーマンスの問題を扱います。診断と修正のパターンに加え、Flutter DevToolsの使い方、コマンド、再現・切り分け・修正・検証の手順をまとめています。
主な機能
- 01よくあるFlutterエラーへの対処法
- 02Flutter DevToolsとインスペクターのガイド
- 03エラーの再現と切り分け手順
- 04診断・テスト・ビルド用コマンド
- 05パフォーマンスとメモリのデバッグ
- 06プラットフォーム別のデバッグ情報
活用シーン
- 01RenderFlexのオーバーフロー診断
- 02ウィジェット破棄後の状態更新の修正
- 03プラットフォームチャネルの問題調査
- 04ジャンクやメモリリークの追跡
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ snakeo/ claude-debug-and-refactor-skills-plugin --skill debug:flutterインストール後、Claude Code で/debug:flutterを入力して読み込まれたか確認します。
詳細
Flutter デバッグガイド
このスキルでは、よくあるエラーパターン、デバッグツール、問題を効率的に解決するためのベストプラクティスを取り上げ、Flutter アプリケーションを体系的にデバッグする方法を説明します。
よくあるエラーパターン
1. RenderFlex オーバーフローエラー
症状:
- UI にオーバーフロー領域を示す黄色と黒の縞模様が表示される
- エラーメッセージ:「A RenderFlex overflowed by X pixels」
原因:
- Row/Column 内のコンテンツが利用可能な領域を超えている
- 制約のあるコンテナー内の固定サイズウィジェット
- 適切なオーバーフロー処理がされていないテキスト
解決策:
// Solution 1: Wrap in SingleChildScrollView
SingleChildScrollView(
child: Column(
children: [...],
),
)
// Solution 2: Use Flexible or Expanded
Row(
children: [
Expanded(
child: Text('Long text that might overflow...'),
),
],
)
// Solution 3: Handle text overflow explicitly
Text(
'Long text...',
overflow: TextOverflow.ellipsis,
maxLines: 2,
)
2. Dispose 後に setState() が呼び出された
症状:
- 実行時エラー:「setState() called after dispose()」
- 非同期処理の完了後にアプリがクラッシュする
原因:
- ウィジェットがツリーから削除された後に setState() を呼び出している
- 画面遷移後に非同期処理が完了する
- Dispose 済みのウィジェットでタイマーのコールバックが実行される
解決策:
// Solution 1: Check mounted before setState
Future<void> fetchData() async {
final data = await api.getData();
if (mounted) {
setState(() {
_data = data;
});
}
}
// Solution 2: Cancel async operations in dispose
late final StreamSubscription _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
// Solution 3: Use CancelableOperation
CancelableOperation<Data>? _operation;
Future<void> fetchData() async {
_operation = CancelableOperation.fromFuture(api.getData());
final data = await _operation!.value;
if (mounted) {
setState(() => _data = data);
}
}
@override
void dispose() {
_operation?.cancel();
super.dispose();
}
3. Null チェック演算子のエラー
症状:
- エラー:「Null check operator used on a null value」
!を使って null 許容値にアクセスした際にアプリがクラッシュする
原因:
- null 値に対して
!演算子を使用している - late 変数を初期化していない
- 非同期コードに競合状態がある
解決策:
// Bad: Using ! without null check
final name = user!.name;
// Good: Use null-aware operators
final name = user?.name ?? 'Unknown';
// Good: Use if-null check
if (user != null) {
final name = user.name;
}
// Good: Use pattern matching (Dart 3+)
if (user case final u?) {
final name = u.name;
}
// For late initialization, consider nullable with check
String? _data;
String get data {
if (_data == null) {
throw StateError('Data not initialized');
}
return _data!;
}
4. プラットフォームチャネルの問題
症状:
- MissingPluginException
- ネイティブコードエラーを伴う PlatformException
- メソッドチャネルが応答しない
解決策:
// Solution 1: Ensure proper platform setup
// Run flutter clean && flutter pub get
// Solution 2: Check method channel registration
const platform = MethodChannel('com.example/channel');
try {
final result = await platform.invokeMethod('methodName');
} on PlatformException catch (e) {
debugPrint('Platform error: ${e.message}');
} on MissingPluginException {
debugPrint('Plugin not registered for this platform');
}
// Solution 3: For plugins, ensure proper initialization
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize plugins here
runApp(MyApp());
}
5. ビルドコンテキストのエラー
症状:
- 「Looking up a deactivated widget's ancestor is unsafe」
- Navigator の操作が失敗する
- Theme/MediaQuery を利用できない
原因:
- ウィジェットが破棄された後に context を使用している
- build の前に initState で context を使用している
- context の参照を保持している
解決策:
// Bad: Using context in initState
@override
void initState() {
super.initState();
// Theme.of(context); // Error!
}
// Good: Use didChangeDependencies
@override
void didChangeDependencies() {
super.didChangeDependencies();
final theme = Theme.of(context);
}
// Good: Use Builder for nested contexts
Scaffold(
body: Builder(
builder: (context) {
// This context has access to Scaffold
return ElevatedButton(
onPressed: () {
Scaffold.of(context).showSnackBar(...);
},
child: Text('Show Snackbar'),
);
},
),
)
// Good: Use GlobalKey for cross-widget access
final scaffoldKey = GlobalKey<ScaffoldState>();
6. ホットリロードの失敗
症状:
- 保存後も変更が反映されない
- アプリの状態が予期せず失われる
- 「Hot reload not available」というメッセージが表示される
原因:
- アプリの初期化コードを変更している
- const コンストラクターを変更している
- ネイティブコードを変更している
- グローバル変数を変更している
解決策:
# Solution 1: Hot restart instead of hot reload
# Press 'R' (capital) in terminal or Shift+Cmd+\ in VS Code
# Solution 2: Full restart
flutter run --no-hot
# Solution 3: Clean build
flutter clean && flutter pub get && flutter run
ホットリスタートが必要なコードパターン:
// Changes to main() require restart
void main() {
runApp(MyApp()); // Modification here needs restart
}
// Changes to initState logic often need restart
@override
void initState() {
super.initState();
_controller = AnimationController(...); // Changes here need restart
}
// Const constructor changes need restart
const MyWidget({super.key}); // Adding/removing const needs restart
7. 垂直ビューポートに無限の高さが指定された
症状:
- エラー:「Vertical viewport was given unbounded height」
- Column 内の ListView が失敗する
解決策:
// Bad: ListView in Column without constraints
Column(
children: [
ListView(...), // Error!
],
)
// Good: Use Expanded
Column(
children: [
Expanded(
child: ListView(...),
),
],
)
// Good: Use shrinkWrap (for small lists only)
Column(
children: [
ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: [...],
),
],
)
// Good: Use SizedBox with fixed height
Column(
children: [
SizedBox(
height: 200,
child: ListView(...),
),
],
)
8. RenderBox がレイアウトされていない
症状:
- エラー:「RenderBox was not laid out」
- ウィジェットが描画されない
解決策:
// Ensure parent provides constraints
SizedBox(
width: 200,
height: 200,
child: CustomPaint(...),
)
// For intrinsic sizing
IntrinsicHeight(
child: Row(
children: [
Container(color: Colors.red),
Container(color: Colors.blue),
],
),
)
9. ParentDataWidget の誤った使用
症状:
- エラー:「Incorrect use of ParentDataWidget」
- Positioned/Expanded が誤って使用されている
解決策:
// Bad: Positioned outside Stack
Column(
children: [
Positioned(...), // Error!
],
)
// Good: Positioned inside Stack
Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Text('Hello'),
),
],
)
// Bad: Expanded outside Flex widget
Container(
child: Expanded(...), // Error!
)
// Good: Expanded inside Row/Column
Row(
children: [
Expanded(child: Text('Hello')),
],
)
10. 赤/グレー画面の死
症状:
- デバッグ/プロファイルモードで赤い画面が表示される
- リリースモードでグレーの画面が表示される
- アプリがフリーズしたように見える
原因:
- 捕捉されていない例外
- 描画エラー
- アサーションの失敗
解決策:
// Global error handling
void main() {
FlutterError.onError = (details) {
FlutterError.presentError(details);
// Log to crash reporting service
crashReporter.recordFlutterError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
// Handle async errors
crashReporter.recordError(error, stack);
return true;
};
runApp(MyApp());
}
// Custom error widget
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
ErrorWidget.builder = (FlutterErrorDetails details) {
return CustomErrorWidget(details: details);
};
return MaterialApp(...);
}
}
デバッグツール
Flutter DevTools
Flutter のデバッグに利用できる包括的なツールスイートです。
# Launch DevTools
flutter pub global activate devtools
flutter pub global run devtools
# Or access via VS Code Flutter extension
DevTools の主な機能:
- ウィジェットインスペクター:ウィジェットツリー、プロパティ、描画オブジェクトを調べる
- パフォーマンスビュー:フレーム描画とジャンクを分析する
- メモリビュー:割り当てを追跡し、メモリリークを検出する
- ネットワークビュー:HTTP リクエストを監視する
- ログビュー:すべてのデバッグ出力を表示する
- CPU プロファイラー:パフォーマンスのボトルネックを特定する
flutter doctor
環境の問題を診断します。
# Full diagnostic
flutter doctor -v
# Check specific issues
flutter doctor --android-licenses
# Common fixes
flutter clean
flutter pub get
flutter pub upgrade
Dart DevTools デバッガー
// Set breakpoints in code
debugger(when: condition);
// Conditional breakpoints in IDE
// Right-click line number > Add Conditional Breakpoint
ロギングのベストプラクティス
// Basic logging
print('Debug message'); // stdout
// Better: debugPrint for large outputs (prevents dropped logs)
debugPrint('Large debug output...');
// Best: dart:developer log for granular control
import 'dart:developer';
log(
'User action',
name: 'UserFlow',
error: exception,
stackTrace: stackTrace,
);
// Conditional logging
assert(() {
debugPrint('Only in debug mode');
return true;
}());
// Using kDebugMode
import 'package:flutter/foundation.dart';
if (kDebugMode) {
print('Debug only');
}
Flutter インスペクター(ウィジェットインスペクター)
// Enable debug painting
import 'package:flutter/rendering.dart';
debugPaintSizeEnabled = true;
debugPaintBaselinesEnabled = true;
debugPaintLayerBordersEnabled = true;
debugPaintPointersEnabled = true;
// In widget
debugPrint(context.widget.toStringDeep());
Flutter デバッグの 4 つのフェーズ
フェーズ 1:エラーの種類を特定する
エラーを分類します。
- コンパイル時エラー:構文エラー、型エラー(赤い波線)
- 実行時エラー:実行中に発生する例外
- レイアウトエラー:RenderFlex のオーバーフロー、制約が無限
- 状態エラー:Dispose 後の setState、不整合な状態
- プラットフォームエラー:ネイティブプラグインの問題、権限
- パフォーマンスの問題:ジャンク、メモリリーク、フレームの遅延
# Get detailed error information
flutter analyze
flutter test --reporter expanded
フェーズ 2:再現して切り分ける
// Create minimal reproduction
class DebugWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Isolate the problematic widget
return Container(
child: ProblematicWidget(),
);
}
}
// Use assertions to validate state
assert(data != null, 'Data should not be null at this point');
assert(index >= 0 && index < list.length, 'Index out of bounds: $index');
フェーズ 3:デバッグして修正する
// Add strategic logging
void processData() {
debugPrint('processData called with: $data');
try {
final result = transform(data);
debugPrint('Transform result: $result');
} catch (e, stack) {
debugPrint('Transform failed: $e');
debugPrint('Stack trace: $stack');
rethrow;
}
}
// Use DevTools breakpoints
// Set breakpoints at:
// - Method entry points
// - Before suspected failure points
// - In catch blocks
フェーズ 4:検証と予防
// Add tests for the fix
testWidgets('Widget handles null data gracefully', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: MyWidget(data: null),
),
);
expect(find.text('No data'), findsOneWidget);
expect(tester.takeException(), isNull);
});
// Add defensive coding
Widget build(BuildContext context) {
if (data == null) {
return const EmptyState();
}
return DataDisplay(data: data!);
}
クイックリファレンスコマンド
診断
# Environment check
flutter doctor -v
# Analyze code for issues
flutter analyze
# Check for outdated packages
flutter pub outdated
# Upgrade packages
flutter pub upgrade --major-versions
テスト
# Run all tests
flutter test
# Run specific test file
flutter test test/widget_test.dart
# Run with coverage
flutter test --coverage
# Run integration tests
flutter test integration_test/
ビルドと実行
# Debug mode (default)
flutter run
# Profile mode (for performance debugging)
flutter run --profile
# Release mode
flutter run --release
# Specific device
flutter run -d <device_id>
# List devices
flutter devices
クリーンアップとリセット
# Clean build artifacts
flutter clean
# Get dependencies
flutter pub get
# Reset iOS pods
cd ios && pod deintegrate && pod install && cd ..
# Reset Android
cd android && ./gradlew clean && cd ..
DevTools
# Install DevTools globally
flutter pub global activate devtools
# Run DevTools
flutter pub global run devtools
# Or use dart devtools
dart devtools
パフォーマンスのデバッグ
カクつきの特定
// Enable performance overlay
MaterialApp(
showPerformanceOverlay: true,
...
)
// Or toggle in DevTools
// Performance > Show Performance Overlay
よくあるパフォーマンスの問題
// Bad: Building expensive widgets in build()
@override
Widget build(BuildContext context) {
final expensiveData = computeExpensiveData(); // Called every rebuild!
return ExpensiveWidget(data: expensiveData);
}
// Good: Cache expensive computations
late final expensiveData = computeExpensiveData();
// Good: Use const constructors
const MyWidget(key: Key('my-widget'));
// Good: Use RepaintBoundary for isolated repaints
RepaintBoundary(
child: ExpensiveAnimatedWidget(),
)
メモリのデバッグ
// Check for leaks in DevTools Memory view
// Common leak patterns:
// 1. Streams not disposed
@override
void dispose() {
_streamSubscription.cancel();
super.dispose();
}
// 2. Controllers not disposed
@override
void dispose() {
_textController.dispose();
_animationController.dispose();
super.dispose();
}
// 3. Listeners not removed
@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
_focusNode.dispose();
super.dispose();
}
状態管理のデバッグ
Provider/Riverpod
// Debug Provider rebuilds
Consumer<MyModel>(
builder: (context, model, child) {
debugPrint('Consumer rebuilding: ${model.value}');
return Text(model.value);
},
)
// Use ProviderScope observers
ProviderScope(
observers: [DebugProviderObserver()],
child: MyApp(),
)
class DebugProviderObserver extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
debugPrint('Provider ${provider.name}: $previousValue -> $newValue');
}
}
Bloc/Cubit
// Enable Bloc observer
Bloc.observer = DebugBlocObserver();
class DebugBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
debugPrint('${bloc.runtimeType} $change');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
debugPrint('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}
プラットフォーム固有のデバッグ
Android
# View Android logs
flutter logs
# Or use adb
adb logcat | grep flutter
# Debug native crashes
adb logcat -s AndroidRuntime:E
iOS
# View iOS logs
flutter logs
# Open Xcode console
open ios/Runner.xcworkspace
# Check crash logs
# Xcode > Window > Devices and Simulators > View Device Logs
Web
// Use browser DevTools console
import 'dart:html' as html;
html.window.console.log('Web debug message');
// Check for CORS issues in Network tab
// Check for CSP issues in Console
追加リソース
詳細なドキュメントについては、以下を参照してください。
コメント
Markdown 対応