droido 1.1.1
droido: ^1.1.1 copied to clipboard
Debug-only network inspector for Flutter. Supports Dio, HTTP, Retrofit. Modern UI, zero release impact.
import 'package:dio/dio.dart';
import 'package:droido/droido.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'api_client.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Shared clients - initialized in main()
late final Dio dio;
late final http.Client httpClient;
late final ApiClient retrofitClient;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Create Dio instance (used for both Dio and Retrofit)
dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
// Create raw HTTP client
final rawHttpClient = http.Client();
// ✨ Single init - pass clients and navigatorKey!
// Droido handles everything: interceptors + notification callback
await Droido.init(
dio: dio,
httpClient: rawHttpClient,
navigatorKey: navigatorKey, // Auto-handles notification tap!
config: const DroidoConfig(
maxLogs: 500,
enableNotification: true,
notificationTitle: 'Droido Example',
),
);
// Get the wrapped HTTP client for use
httpClient = Droido.httpClient!;
// Create Retrofit client using the same Dio instance
// Requests are automatically intercepted!
retrofitClient = ApiClient(dio);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Droido Multi-Client Example',
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Droido Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.bug_report),
tooltip: 'Open Debug Panel',
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DroidoPanel()),
),
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Clear Logs',
onPressed: () async {
await Droido.clearLogs();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Logs cleared')),
);
}
},
),
],
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.bolt), text: 'Dio'),
Tab(icon: Icon(Icons.http), text: 'HTTP'),
Tab(icon: Icon(Icons.api), text: 'Retrofit'),
],
),
),
body: const TabBarView(
children: [
DioTab(),
HttpTab(),
RetrofitTab(),
],
),
),
);
}
}
// ============ DIO TAB ============
class DioTab extends StatelessWidget {
const DioTab({super.key});
@override
Widget build(BuildContext context) {
return _RequestsPanel(
title: 'Dio Requests',
subtitle: 'Using Dio HTTP client with interceptor',
color: Colors.blue,
icon: Icons.bolt,
requests: [
_RequestAction(
'GET Post',
'Fetch a single post',
Icons.download,
() => dio.get('/posts/1'),
),
_RequestAction(
'POST Data',
'Create a new post',
Icons.upload,
() => dio.post(
'/posts',
data: {
'title': 'Test from Dio',
'body': 'This is a test post from Dio client',
'userId': 1,
},
),
),
_RequestAction(
'GET Users',
'Fetch all users',
Icons.people,
() => dio.get('/users'),
),
_RequestAction(
'PUT Update',
'Update an existing post',
Icons.edit,
() => dio.put(
'/posts/1',
data: {'title': 'Updated Title', 'body': 'Updated body'},
),
),
_RequestAction(
'DELETE Post',
'Delete a post',
Icons.delete,
() => dio.delete('/posts/1'),
),
_RequestAction(
'Error 404',
'Trigger a 404 error',
Icons.error,
() => dio.get('/posts/99999'),
),
],
);
}
}
// ============ HTTP TAB ============
class HttpTab extends StatelessWidget {
const HttpTab({super.key});
@override
Widget build(BuildContext context) {
const baseUrl = 'https://jsonplaceholder.typicode.com';
return _RequestsPanel(
title: 'HTTP Package Requests',
subtitle: 'Using dart:http package with Droido wrapper',
color: Colors.green,
icon: Icons.http,
requests: [
_RequestAction(
'GET Post',
'Fetch a single post',
Icons.download,
() => httpClient.get(Uri.parse('$baseUrl/posts/1')),
),
_RequestAction(
'POST Data',
'Create a new post',
Icons.upload,
() => httpClient.post(
Uri.parse('$baseUrl/posts'),
headers: {'Content-Type': 'application/json'},
body: '{"title": "Test from HTTP", "body": "HTTP package test", "userId": 1}',
),
),
_RequestAction(
'GET Comments',
'Fetch comments for a post',
Icons.comment,
() => httpClient.get(Uri.parse('$baseUrl/comments?postId=1')),
),
_RequestAction(
'GET Albums',
'Fetch all albums',
Icons.album,
() => httpClient.get(Uri.parse('$baseUrl/albums')),
),
_RequestAction(
'Error 404',
'Trigger a 404 error',
Icons.error,
() => httpClient.get(Uri.parse('$baseUrl/posts/99999')),
),
],
);
}
}
// ============ RETROFIT TAB ============
class RetrofitTab extends StatelessWidget {
const RetrofitTab({super.key});
@override
Widget build(BuildContext context) {
return _RequestsPanel(
title: 'Retrofit Requests',
subtitle: 'Type-safe API calls via Retrofit (uses Dio)',
color: Colors.purple,
icon: Icons.api,
requests: [
_RequestAction(
'GET Post',
'Fetch post with typed response',
Icons.download,
() => retrofitClient.getPost(1),
),
_RequestAction(
'GET All Posts',
'Fetch all posts as List<Post>',
Icons.list,
() => retrofitClient.getPosts(),
),
_RequestAction(
'CREATE Post',
'Create post with typed body',
Icons.add,
() => retrofitClient.createPost(
Post(
title: 'Retrofit Test',
body: 'Created via Retrofit with typed model',
userId: 1,
),
),
),
_RequestAction(
'GET User',
'Fetch user with typed response',
Icons.person,
() => retrofitClient.getUser(1),
),
_RequestAction(
'GET Comments',
'Fetch comments with query param',
Icons.comment,
() => retrofitClient.getComments(1),
),
_RequestAction(
'UPDATE Post',
'Update post with PUT',
Icons.edit,
() => retrofitClient.updatePost(
1,
Post(title: 'Updated via Retrofit', body: 'Updated content'),
),
),
],
);
}
}
// ============ SHARED WIDGETS ============
class _RequestAction {
final String label;
final String description;
final IconData icon;
final Future<dynamic> Function() action;
const _RequestAction(this.label, this.description, this.icon, this.action);
}
class _RequestsPanel extends StatelessWidget {
final String title;
final String subtitle;
final Color color;
final IconData icon;
final List<_RequestAction> requests;
const _RequestsPanel({
required this.title,
required this.subtitle,
required this.color,
required this.icon,
required this.requests,
});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Header card
Card(
color: color.withAlpha((0.1 * 255).round()),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(icon, size: 40, color: color),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Request buttons
...requests.map(
(r) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _RequestButton(
label: r.label,
description: r.description,
icon: r.icon,
color: color,
onPressed: () => _executeRequest(context, r),
),
),
),
// Log count indicator
const SizedBox(height: 16),
StreamBuilder<List<NetworkLog>>(
stream: Droido.logsStream,
builder: (context, snapshot) {
final count = snapshot.data?.length ?? 0;
return Center(
child: Chip(
avatar: const Icon(Icons.history, size: 18),
label: Text('$count requests logged'),
),
);
},
),
],
);
}
Future<void> _executeRequest(
BuildContext context,
_RequestAction request,
) async {
try {
await request.action();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${request.label} - Success!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${request.label} - Error logged'),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
}
class _RequestButton extends StatelessWidget {
final String label;
final String description;
final IconData icon;
final Color color;
final VoidCallback onPressed;
const _RequestButton({
required this.label,
required this.description,
required this.icon,
required this.color,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Material(
color: color,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(icon, color: Colors.white),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
description,
style: TextStyle(
color: Colors.white.withAlpha((0.8 * 255).round()),
fontSize: 12,
),
),
],
),
),
const Icon(Icons.chevron_right, color: Colors.white),
],
),
),
),
);
}
}