flutter_debug_tracker 0.2.0
flutter_debug_tracker: ^0.2.0 copied to clipboard
In-app debug tool for Flutter on Android and iOS: FPS monitor, network inspector, logs, Firebase Analytics viewer and FCM push notification tester.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_debug_tracker/flutter_debug_tracker.dart';
void main() {
DebugTracker.runZoned(
() => runApp(const DemoApp()),
config: const DebugTrackerConfig(
customInfo: <String, String>{'App': 'Demo', 'Flavor': 'dev'},
),
);
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter_debug_tracker demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
navigatorObservers: <NavigatorObserver>[DebugTracker.observer],
builder: DebugTracker.builder(),
routes: <String, WidgetBuilder>{
'/': (_) => const HomePage(),
'/details': (_) => const DetailsPage(),
},
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final HttpClient _client = HttpClient();
final List<List<int>> _retained = <List<int>>[];
String _status = 'Tap a button, then open the bubble.';
StreamSubscription<TestNotification>? _taps;
@override
void initState() {
super.initState();
// Without FCM, test notification taps arrive here.
_taps = DebugTracker.instance.notifications.onTap.listen(
(n) =>
setState(() => _status = 'Tapped notification: ${n.title} ${n.data}'),
);
}
@override
void dispose() {
_taps?.cancel();
_client.close();
super.dispose();
}
Future<void> _request(String method, String url, {Object? body}) async {
try {
final request = await _client.openUrl(method, Uri.parse(url));
if (body != null) {
request.headers.contentType = ContentType.json;
request.write(jsonEncode(body));
}
final response = await request.close();
final text = await utf8.decoder.bind(response).join();
setState(
() => _status = '$method ${response.statusCode} · ${text.length} chars',
);
} catch (error) {
setState(() => _status = 'Failed: $error');
}
}
void _jank() {
// Blocks the UI thread for ~300 ms to produce janky frames.
final watch = Stopwatch()..start();
var x = 0.0;
while (watch.elapsedMilliseconds < 300) {
x += math.sqrt(x + 1);
}
setState(() => _status = 'Blocked UI thread (${x.toStringAsFixed(0)})');
}
// Sends exactly what firebase_analytics' FirebaseAnalytics.instance
// .logEvent() sends to the native SDK, so the demo runs without a Firebase
// project. In a real app, just call FirebaseAnalytics as usual.
Future<void> _firebaseLogEvent() async {
const channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.firebase_analytics_platform_interface.'
'FirebaseAnalyticsHostApi.logEvent',
StandardMessageCodec(),
);
await channel.send(<Object?>[
<String, Object?>{
'eventName': 'add_to_cart',
'parameters': <String, Object?>{
'item_id': 'sku-42',
'price': 19.99,
'quantity': 1,
},
},
]);
setState(() => _status = 'Sent Firebase add_to_cart');
}
Future<void> _trace() async {
await DebugTracker.trace('fake database load', () async {
await Future<void>.delayed(const Duration(milliseconds: 420));
});
setState(() => _status = 'Trace recorded');
}
@override
Widget build(BuildContext context) {
final actions = <(String, VoidCallback)>[
(
'GET JSON',
() => _request('GET', 'https://jsonplaceholder.typicode.com/todos/1'),
),
(
'POST JSON',
() => _request(
'POST',
'https://jsonplaceholder.typicode.com/posts',
body: <String, Object>{'title': 'hello', 'userId': 1},
),
),
('GET 404', () => _request('GET', 'https://httpbin.org/status/404')),
('Unreachable host', () => _request('GET', 'https://nope.invalid/')),
('print()', () => print('Hello from print at ${DateTime.now()}')),
('debugPrint()', () => debugPrint('Hello from debugPrint')),
(
'Warning log',
() => DebugTracker.log(
'Token expires soon',
level: LogLevel.warning,
tag: 'auth',
),
),
('Throw async error', () => Future<void>.error(StateError('Boom'))),
('Jank 300 ms', _jank),
('Trace 420 ms', _trace),
(
'Allocate 20 MB',
() => setState(() {
_retained.add(List<int>.filled(20 * 1024 * 1024 ~/ 8, 1));
_status = 'Retained ${_retained.length * 20} MB';
}),
),
('Firebase logEvent', _firebaseLogEvent),
(
'Test notification (5 s)',
() => DebugTracker.sendTestNotification(
title: 'Order shipped',
body: 'Your order #1042 is on its way',
data: <String, String>{'orderId': '1042'},
delay: const Duration(seconds: 5),
),
),
(
'Custom analytics',
() => DebugTracker.logAnalyticsEvent(
'signup_started',
provider: 'mixpanel',
parameters: <String, Object?>{'source': 'demo'},
),
),
('Open details', () => Navigator.of(context).pushNamed('/details')),
];
return Scaffold(
appBar: AppBar(title: const Text('Debug Tracker demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
Text(_status, style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
for (final (label, onPressed) in actions)
FilledButton.tonal(onPressed: onPressed, child: Text(label)),
],
),
const SizedBox(height: 24),
const _Spinner(),
],
),
);
}
}
/// Keeps frames flowing so FPS is visible.
class _Spinner extends StatefulWidget {
const _Spinner();
@override
State<_Spinner> createState() => _SpinnerState();
}
class _SpinnerState extends State<_Spinner>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: RotationTransition(
turns: _controller,
child: const FlutterLogo(size: 72),
),
);
}
}
class DetailsPage extends StatelessWidget {
const DetailsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: FilledButton(
onPressed: () => throw FlutterError('Tapped the broken button'),
child: const Text('Throw FlutterError'),
),
),
);
}
}