flutter_app_inspector 0.1.0
flutter_app_inspector: ^0.1.0 copied to clipboard
A developer observability and runtime inspection layer for Flutter applications.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_inspector/flutter_app_inspector.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize AppInspector with debug/profile mode defaults
AppInspector.initialize(
config: const AppInspectorConfig(
enabled: kDebugMode || kProfileMode,
consoleLogging: true,
logLevel: InspectorLogLevel.info,
overlayMode: InspectorOverlayMode.compact,
overlayPosition: InspectorOverlayPosition.topRight,
widgetMonitoring: true,
),
);
runApp(
const AppInspectorOverlay(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App Inspector Demo',
debugShowCheckedModeBanner: false,
navigatorObservers: [AppInspector.navigatorObserver],
theme: ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const DemoHomePage(),
);
}
}
class DemoHomePage extends StatefulWidget {
const DemoHomePage({super.key});
@override
State<DemoHomePage> createState() => _DemoHomePageState();
}
class _DemoHomePageState extends State<DemoHomePage> {
int _counter = 0;
int _spikeCounter = 0;
final List<Uint8List> _retainedBuffers = [];
String _statusMessage = 'Inspector overlay is floating on screen top-right.';
void _generateHeavyWork() {
setState(() {
_statusMessage = 'Executing CPU heavy work loop on UI thread...';
});
final stopwatch = Stopwatch()..start();
double result = 0.0;
for (int i = 0; i < 20000000; i++) {
result += (i * 0.00001);
}
stopwatch.stop();
setState(() {
_counter++;
_statusMessage =
'Heavy work completed in ${stopwatch.elapsedMilliseconds}ms. (Result: ${result.toStringAsFixed(1)})';
});
}
void _triggerJank() {
setState(() {
_statusMessage =
'Simulating synthetic jank delay on UI thread (150ms)...';
});
sleepSync(const Duration(milliseconds: 150));
setState(() {
_counter++;
_statusMessage =
'Jank triggered! Check overlay frame duration and jank count.';
});
}
void _triggerRebuildSpike() {
setState(() {
_statusMessage = 'Simulating rapid widget rebuild surge...';
});
// Rapidly trigger state rebuilds to simulate a widget rebuild spike
for (int i = 0; i < 25; i++) {
setState(() {
_spikeCounter++;
});
}
setState(() {
_statusMessage = 'Widget rebuild spike simulated! Check HUD Hotspots.';
});
}
void _triggerSlowWidgetBuild() {
setState(() {
_statusMessage = 'Simulating slow widget build pass (24.1ms)...';
});
AppInspector.trackWidgetRebuild(
'ProductList',
const Duration(microseconds: 24100),
);
setState(() {
_statusMessage =
'Slow widget build recorded! Check HUD Potential Slow Widget alert.';
});
}
void _navigateToDetailsScreen() {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: '/details'),
builder: (context) => const DemoDetailsScreen(),
),
);
}
void _simulateImageLoad() {
AppInspector.trackImageLoad(
urlOrKey: 'assets/sample_banner.png',
state: ImageLoadState.cacheMiss,
width: 1200,
height: 800,
displayWidth: 400,
displayHeight: 267,
loadDuration: const Duration(milliseconds: 45),
);
setState(() {
_statusMessage = 'Recorded image load (1200x800, cache miss)';
});
}
void _simulateOversizedImage() {
AppInspector.trackImageLoad(
urlOrKey: 'hero_full_res_photo.png',
state: ImageLoadState.cacheHit,
width: 6000,
height: 4000,
displayWidth: 400,
displayHeight: 280,
loadDuration: const Duration(milliseconds: 12),
);
setState(() {
_statusMessage =
'Recorded oversized image alert (6000x4000 -> 400x280 display)';
});
}
void _recordSingleError() {
try {
throw StateError('Cannot modify unmodifiable list element');
} catch (e, stack) {
AppInspector.recordError(e, stackTrace: stack);
}
setState(() {
_statusMessage = 'Recorded StateError in Error & Exception Monitor';
});
}
void _simulateRepeatedErrorSurge() {
for (int i = 0; i < 27; i++) {
AppInspector.recordError(
Exception('SocketException: Connection refused'),
severity: InspectorSeverity.warning,
);
}
setState(() {
_statusMessage = 'Recorded 27x repeated SocketException surge';
});
}
void _analyzePerformance() {
final report = AppInspector.analyzePerformance();
debugPrint(
'\n=== WHY IS MY APP SLOW? ===\n${report.formattedSummary}\n===========================\n');
setState(() {
_statusMessage =
'Ran "Why Is My App Slow?" analysis (Score: ${report.score.overall.round()}/100)';
});
}
void _simulateSlowNetworkRequest() {
AppInspector.store.add(
AppInspectorEvent(
id: 'net_${DateTime.now().millisecondsSinceEpoch}',
type: InspectorEventType.network,
timestamp: DateTime.now(),
name: '/api/v1/products',
severity: InspectorSeverity.warning,
metadata: {
'durationMs': 421,
'url': '/api/v1/products',
},
),
);
setState(() {
_statusMessage = 'Recorded slow network request (/api/v1/products 421ms)';
});
}
void _simulateAppStartup() {
AppInspector.store.add(
AppInspectorEvent(
id: 'start_${DateTime.now().millisecondsSinceEpoch}',
type: InspectorEventType.startup,
timestamp: DateTime.now(),
name: 'Cold Start Initialization',
severity: InspectorSeverity.info,
metadata: {
'durationMs': 1250,
},
),
);
setState(() {
_statusMessage = 'Recorded App Cold Startup timing event (1250ms)';
});
}
void _generateMemoryActivity() {
setState(() {
_statusMessage = 'Allocating 20 MB Uint8List buffer...';
});
final buffer = Uint8List(20 * 1024 * 1024);
for (int i = 0; i < buffer.length; i += 1024) {
buffer[i] = i % 256;
}
_retainedBuffers.add(buffer);
final totalMb = (_retainedBuffers.length * 20.0).toStringAsFixed(0);
setState(() {
_statusMessage = 'Allocated ${totalMb}MB of retained memory buffers.';
});
}
Future<void> _runCustomTracker() async {
setState(() {
_statusMessage = 'Running tracked custom operation...';
});
await AppInspector.measure('Checkout Process', () async {
await Future.delayed(const Duration(milliseconds: 800));
});
setState(() {
_statusMessage = 'Custom operation recorded in event store (800ms).';
});
}
Future<void> _runNetworkTracker() async {
setState(() {
_statusMessage = 'Simulating network request tracking...';
});
final tracker = AppInspector.startNetworkRequest(
method: 'GET', url: '/api/v1/products');
await Future.delayed(const Duration(milliseconds: 1200));
tracker.complete(statusCode: 200, responseSize: 48500);
setState(() {
_statusMessage = 'Network request recorded (1200ms latency warning).';
});
}
void _resetMetrics() {
_retainedBuffers.clear();
_spikeCounter = 0;
AppInspector.reset();
setState(() {
_statusMessage = 'Reset all metrics and cleared retained memory buffers.';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('⚡ App Inspector Demo'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded),
tooltip: 'Reset Inspector Metrics',
onPressed: _resetMetrics,
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Tracked Demo Widget: ProductList Card
InspectorWidgetTracker(
name: 'ProductList',
child: Card(
color: const Color(0xFF1E2838),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Icon(Icons.speed_rounded,
size: 36, color: Color(0xFF81D4FA)),
const SizedBox(height: 8),
const Text(
'Developer Observability & HUD Demo',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_statusMessage,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12, color: Color(0xFFB0BEC5)),
),
],
),
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView(
children: [
// Tracked Demo Widget: ProductCard Action
InspectorWidgetTracker(
name: 'ProductCard',
child: _DemoActionButton(
icon: Icons.widgets_rounded,
color: Colors.cyanAccent,
title: 'Simulate Rebuild Surge (Spike)',
subtitle:
'Triggers 25 rapid rebuilds on ProductList & ProductCard',
onPressed: _triggerRebuildSpike,
),
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.hourglass_bottom_rounded,
color: Colors.deepOrangeAccent,
title: 'Simulate Slow Widget Build (24ms)',
subtitle:
'Records a 24.1ms build pass exceeding 16.7ms frame budget',
onPressed: _triggerSlowWidgetBuild,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.alt_route_rounded,
color: Colors.tealAccent,
title: 'Test Route Transition (/details)',
subtitle:
'Pushes /details route & records navigation transition duration',
onPressed: _navigateToDetailsScreen,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.image_rounded,
color: Colors.purpleAccent,
title: 'Simulate Image Load (1200x800)',
subtitle:
'Records an image load metric with cache miss duration',
onPressed: _simulateImageLoad,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.photo_size_select_large_rounded,
color: Colors.deepOrangeAccent,
title: 'Simulate Oversized Image (6000x4000)',
subtitle:
'Triggers ⚠️ Large Image alert (~91.5 MB estimated decoded RAM)',
onPressed: _simulateOversizedImage,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.warning_amber_rounded,
color: Colors.amberAccent,
title: 'Record Single Exception (StateError)',
subtitle:
'Records a StateError exception with sanitized stack trace',
onPressed: _recordSingleError,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.error_outline_rounded,
color: Colors.redAccent,
title:
'Simulate Repeated Error Surge (27x SocketException)',
subtitle:
'Triggers ⚠️ Repeated Error alert (SocketException 27 occurrences)',
onPressed: _simulateRepeatedErrorSurge,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.analytics_rounded,
color: Colors.lightBlueAccent,
title: 'Analyze Performance ("Why Is My App Slow?")',
subtitle:
'Runs full diagnostic evaluation across frames, rebuilds, images & errors',
onPressed: _analyzePerformance,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.wifi_tethering_off_rounded,
color: Colors.orangeAccent,
title: 'Simulate Slow Network Request (/api/v1/products)',
subtitle:
'Records a 421ms network latency event in store',
onPressed: _simulateSlowNetworkRequest,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.power_settings_new_rounded,
color: Colors.lightGreenAccent,
title: 'Simulate App Cold Startup Timing (1250ms)',
subtitle:
'Records a 1250ms cold start initialization event',
onPressed: _simulateAppStartup,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.flash_on_rounded,
color: Colors.amber,
title: 'Generate Heavy Work',
subtitle:
'Runs a 20M iteration loop synchronously on UI thread',
onPressed: _generateHeavyWork,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.report_problem_rounded,
color: Colors.orangeAccent,
title: 'Trigger Jank (150ms delay)',
subtitle:
'Forces a frame drop exceeding 16.7ms target budget',
onPressed: _triggerJank,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.memory_rounded,
color: Colors.lightBlueAccent,
title: 'Generate Memory Activity',
subtitle:
'Allocates and retains 20 MB byte buffer in memory',
onPressed: _generateMemoryActivity,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.timer_rounded,
color: Colors.greenAccent,
title: 'Run Custom Operation Tracking',
subtitle: 'Uses AppInspector.measure() for custom timing',
onPressed: _runCustomTracker,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.http_rounded,
color: Colors.purpleAccent,
title: 'Simulate Slow Network Request',
subtitle: 'Uses AppInspector.startNetworkRequest()',
onPressed: _runNetworkTracker,
),
const SizedBox(height: 10),
_DemoActionButton(
icon: Icons.restart_alt_rounded,
color: Colors.redAccent,
title: 'Reset Metrics & Memory',
subtitle: 'Clears event buffer and memory allocations',
onPressed: _resetMetrics,
),
],
),
),
Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Counter: $_counter | Spikes: $_spikeCounter',
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13),
),
Text(
'Retained Buffers: ${_retainedBuffers.length}',
style:
const TextStyle(fontSize: 12, color: Colors.white70),
),
],
),
),
],
),
),
),
);
}
}
class _DemoActionButton extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
final VoidCallback onPressed;
const _DemoActionButton({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E2838),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
alignment: Alignment.centerLeft,
),
onPressed: onPressed,
child: Row(
children: [
CircleAvatar(
backgroundColor: color.withValues(alpha: 0.2),
radius: 18,
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(color: Colors.white54, fontSize: 11),
),
],
),
),
],
),
);
}
}
void sleepSync(Duration duration) {
final end = DateTime.now().add(duration);
while (DateTime.now().isBefore(end)) {
// Synchronous busy wait
}
}
class DemoDetailsScreen extends StatelessWidget {
const DemoDetailsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Details Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.alt_route_rounded,
size: 48, color: Colors.tealAccent),
const SizedBox(height: 12),
const Text(
'Route /details loaded successfully!',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Navigation transition duration was measured by InspectorNavigatorObserver.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 20),
ElevatedButton.icon(
icon: const Icon(Icons.arrow_back_rounded),
label: const Text('Pop Back to Home'),
onPressed: () => Navigator.pop(context),
),
],
),
),
);
}
}