api_track_inspector 2.0.0
api_track_inspector: ^2.0.0 copied to clipboard
Dependency-free Flutter network inspector for debug builds. Logs any HTTP client (Dio, http, chopper) and shows requests in an in-app viewer behind a draggable FAB.
import 'package:api_track_inspector/api_track_inspector.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:share_plus/share_plus.dart';
export 'package:dio/dio.dart' show RequestOptions, Response;
export 'package:dio/dio.dart'
show RequestInterceptorHandler, ResponseInterceptorHandler;
void main() {
NetworkInspector.init(
enabled: kDebugMode,
maxLogs: 100,
// Optional. Without this the export/share buttons copy to the clipboard,
// so the package itself needs no sharing plugin.
onShare: (data, subject) => SharePlus.instance.share(
ShareParams(text: data, subject: subject),
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Network Inspector Example',
theme: ThemeData(useMaterial3: true),
home: const HomePage(),
// wrapWithFAB returns child untouched when the inspector is disabled,
// so this is safe to leave in for release builds.
builder: (context, child) => NetworkInspector.wrapWithFAB(
child ?? const SizedBox.shrink(),
),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _dio = Dio();
String _result = 'Run any request and open the inspector FAB.';
@override
void initState() {
super.initState();
_dio.interceptors.add(InspectorInterceptor());
}
Future<void> _sendWithHttp() async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/todos/1');
final startedAt = DateTime.now();
final logId = NetworkInspector.logRequest(
method: 'GET',
url: uri.toString(),
headers: const {'Accept': 'application/json'},
);
try {
final response = await http.get(uri);
final duration = DateTime.now().difference(startedAt);
if (logId != null) {
NetworkInspector.logResponse(
logId: logId,
statusCode: response.statusCode,
body: response.body,
duration: duration,
);
}
setState(() {
_result = 'package:http -> ${response.statusCode} '
'(${duration.inMilliseconds}ms)';
});
} catch (e) {
if (logId != null) {
NetworkInspector.logResponse(
logId: logId,
duration: DateTime.now().difference(startedAt),
error: e.toString(),
);
}
setState(() => _result = 'package:http failed: $e');
}
}
Future<void> _sendWithDio() async {
try {
final response = await _dio.get(
'https://jsonplaceholder.typicode.com/posts/1',
);
setState(() => _result = 'Dio -> ${response.statusCode}');
} catch (e) {
setState(() => _result = 'Dio failed: $e');
}
}
Future<void> _sendFailingRequest() async {
try {
final response = await _dio.get(
'https://jsonplaceholder.typicode.com/nope/404',
);
setState(() => _result = 'Dio -> ${response.statusCode}');
} catch (e) {
setState(() => _result = 'Dio failed as expected (see the inspector)');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Network Inspector Example')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: _sendWithDio,
child: const Text('Send with Dio'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _sendWithHttp,
child: const Text('Send with package:http'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _sendFailingRequest,
child: const Text('Send a failing request'),
),
const SizedBox(height: 12),
// The FAB is always available, but you can open the inspector
// from anywhere with a BuildContext.
OutlinedButton(
onPressed: () => NetworkInspector.show(context),
child: const Text('Open inspector'),
),
const SizedBox(height: 24),
Text(_result),
],
),
),
);
}
}
/// A ready-to-copy Dio interceptor.
///
/// The package does not ship this — that would mean depending on Dio. Copy it
/// into your own project (it is ~30 lines) and adapt as needed.
class InspectorInterceptor extends Interceptor {
static const String _logIdKey = '_inspectorLogId';
static const String _startKey = '_inspectorStart';
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final logId = NetworkInspector.logRequest(
method: options.method,
url: options.uri.toString(),
headers: Map<String, dynamic>.from(options.headers),
body: options.data,
);
if (logId != null) {
options.extra[_logIdKey] = logId;
options.extra[_startKey] = DateTime.now();
}
handler.next(options);
}
@override
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) {
_complete(
response.requestOptions,
statusCode: response.statusCode,
body: response.data,
);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_complete(
err.requestOptions,
statusCode: err.response?.statusCode,
body: err.response?.data,
error: err.message ?? err.type.name,
);
handler.next(err);
}
void _complete(
RequestOptions options, {
int? statusCode,
dynamic body,
String? error,
}) {
final logId = options.extra[_logIdKey] as String?;
final start = options.extra[_startKey] as DateTime?;
if (logId == null) return;
NetworkInspector.logResponse(
logId: logId,
statusCode: statusCode,
body: body,
duration: start == null ? null : DateTime.now().difference(start),
error: error,
);
}
}