api_network_logger 1.0.0
api_network_logger: ^1.0.0 copied to clipboard
A production-ready, publishable Flutter package that transparently logs and stores API calls, offline events, and navigation routes.
example/lib/main.dart
import 'package:api_network_logger/api_network_logger.dart';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize API Logger with zero-config defaults + customizable options
await ApiLogger.instance.init(
config: const ApiLoggerConfig(
exportEndpoint: 'https://httpbin.org/post', // Mock endpoint for exporting
redactedFields: ['password', 'token', 'cvv', 'secret'],
redactedHeaders: ['Authorization', 'Cookie'],
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Api Logger Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
fontFamily: 'system-ui',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2CAC5C), // Brand green color
primary: const Color(0xFF2563EB), // Royal blue
secondary: const Color(0xFF38BDF8), // Sky blue
surface: const Color(0xFFF8FAFC),
),
scaffoldBackgroundColor: const Color(0xFFF1F5F9),
),
builder: (context, child) => ApiLoggerOverlay(child: child!),
// Register navigator observer to automatically log page transitions
navigatorObservers: [ApiLoggerNavigatorObserver()],
home: const HomeScreen(),
routes: {
'/settings': (context) => const SettingsScreen(),
'/details': (context) => const DetailsScreen(),
'/db_viewer': (context) => const DatabaseViewerScreen(),
},
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _httpStyleClient = HttpApiLoggerClient();
final _dioClient = dio.Dio()..interceptors.add(DioApiLoggerInterceptor());
bool _isOfflineSimulated = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Icon(Icons.terminal_rounded, color: Color(0xFF2CAC5C)),
SizedBox(width: 8),
Text(
'Tractal DevConsole',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color(0xFF0F172A)),
),
],
),
elevation: 0,
backgroundColor: Colors.white,
actions: [
Container(
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF2CAC5C).withOpacity(0.12),
borderRadius: BorderRadius.circular(20),
),
child: const Row(
children: [
Badge(isLabelVisible: true, backgroundColor: Color(0xFF2CAC5C)),
SizedBox(width: 6),
Text(
'SANDBOX ACTIVE',
style: TextStyle(color: Color(0xFF2CAC5C), fontWeight: FontWeight.bold, fontSize: 10),
),
],
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Welcome Card
_buildWelcomeCard(),
const SizedBox(height: 24),
// HTTP Clients Section
const Text(
'TEST HTTP CLIENT ADAPTERS',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildActionCard(
title: 'HTTP Success',
description: 'Fires a success GET request via HttpApiLoggerClient',
icon: Icons.cloud_done_rounded,
iconColor: const Color(0xFF2CAC5C),
buttonText: 'Trigger GET',
onTap: _fireHttpGetSuccess,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionCard(
title: 'Dio PII Redactor',
description: 'Sends passwords/tokens & logs redacted error paths',
icon: Icons.lock_rounded,
iconColor: const Color(0xFF2563EB),
buttonText: 'Trigger POST',
onTap: _fireDioPostError,
),
),
],
),
const SizedBox(height: 12),
// Parallel Concurrent card
_buildParallelActionCard(),
const SizedBox(height: 24),
// Simulated environments Section
const Text(
'SIMULATION GATEWAYS',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.black.withOpacity(0.04)),
),
child: SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
title: const Row(
children: [
Icon(Icons.wifi_off_rounded, color: Colors.orange, size: 20),
SizedBox(width: 8),
Text('Simulate Offline Mode', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
],
),
subtitle: const Text('Logs offline connectivity states to storage', style: TextStyle(fontSize: 11)),
value: _isOfflineSimulated,
activeColor: const Color(0xFF2CAC5C),
onChanged: (val) {
setState(() => _isOfflineSimulated = val);
ApiLogger.instance.logOfflineEvent(val);
_showResultSnackBar('Offline simulation state toggled: ${val ? 'ON' : 'OFF'}');
},
),
),
const SizedBox(height: 24),
// Routing Section
const Text(
'DYNAMIC APP ROUTING NAVIGATION',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.settings_outlined, size: 16),
label: const Text('Push Settings Page'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2563EB),
side: const BorderSide(color: Color(0xFF2563EB)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => Navigator.pushNamed(context, '/settings', arguments: {'source': 'home_grid', 'init': true}),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
icon: const Icon(Icons.info_outline_rounded, size: 16),
label: const Text('Push Sandbox Details'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2563EB),
side: const BorderSide(color: Color(0xFF2563EB)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => Navigator.pushNamed(context, '/details', arguments: 'sandbox_payload_id_889'),
),
),
],
),
const SizedBox(height: 20),
// Large Database Programmatic call
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: const LinearGradient(
colors: [Color(0xFF2CAC5C), Color(0xFF10B981)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF2CAC5C).withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton.icon(
icon: const Icon(Icons.storage_rounded, size: 18),
label: const Text('Open Programmatic Database Screen', style: TextStyle(fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () => Navigator.pushNamed(context, '/db_viewer'),
),
),
],
),
),
);
}
Widget _buildWelcomeCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(color: Colors.black.withOpacity(0.03)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF2CAC5C).withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.hub_rounded, color: Color(0xFF2CAC5C), size: 24),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tractal Solutions Hub',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF0F172A)),
),
Text('Local Network Logger v1.0.0', style: TextStyle(color: Color(0xFF64748B), fontSize: 11)),
],
),
],
),
const SizedBox(height: 16),
const Text(
'This premium sandbox app executes parallel test sequences, logs routing paths, and showcases the robust programmatic database. Tap the floating green badge in the corner at any time to open the visual debugger sheet.',
style: TextStyle(fontSize: 12, color: Color(0xFF64748B), height: 1.5),
),
],
),
);
}
Widget _buildActionCard({
required String title,
required String description,
required IconData icon,
required Color iconColor,
required String buttonText,
required VoidCallback onTap,
}) {
return Container(
height: 180,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Color(0xFF0F172A)),
),
],
),
const SizedBox(height: 6),
Text(
description,
style: const TextStyle(fontSize: 10, color: Color(0xFF64748B), height: 1.3),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: iconColor.withOpacity(0.1),
foregroundColor: iconColor,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onTap,
child: Text(buttonText, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
),
)
],
),
);
}
Widget _buildParallelActionCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF2563EB).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.bolt_rounded, color: Color(0xFF2563EB), size: 22),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Concurrent Test Cycle',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Color(0xFF0F172A)),
),
const SizedBox(height: 2),
const Text(
'Fires 5 requests simultaneously to test safe DB queueing',
style: TextStyle(fontSize: 10, color: Color(0xFF64748B)),
),
const SizedBox(height: 8),
SizedBox(
height: 32,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _fireParallelRequests,
child: const Text('Run Concurrency Test', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
),
),
],
),
),
],
),
);
}
void _fireHttpGetSuccess() async {
try {
final res = await _httpStyleClient.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
_showResultSnackBar('GET Success: status ${res.statusCode}');
} catch (e) {
_showResultSnackBar('GET Exception: $e');
}
}
void _fireDioPostError() async {
try {
await _dioClient.post(
'https://jsonplaceholder.typicode.com/invalid-path-endpoint',
data: {
'user': 'satish_parmar',
'password': 'super_secret_password_123',
'token': 'jwt_secret_token_xyz_987',
'nested': {
'cvv': 321,
'secret': 'nested_key_value',
}
},
options: dio.Options(
headers: {
'Authorization': 'Bearer super_secret_api_key_jwt',
'Cookie': 'session_token=abc_123; user_id=456',
'Accept': 'application/json',
},
),
);
} catch (e) {
_showResultSnackBar('Dio Post simulated successfully (logged error path)');
}
}
void _fireParallelRequests() async {
_showResultSnackBar('Firing 5 concurrent requests...');
final urls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/posts/3',
'https://jsonplaceholder.typicode.com/posts/4',
'https://jsonplaceholder.typicode.com/posts/5',
];
await Future.wait([
_httpStyleClient.get(Uri.parse(urls[0])),
_dioClient.get(urls[1]),
_httpStyleClient.get(Uri.parse(urls[2])),
_dioClient.get(urls[3]),
_httpStyleClient.get(Uri.parse(urls[4])),
]);
_showResultSnackBar('All 5 concurrent requests logged successfully!');
}
void _showResultSnackBar(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF0F172A),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.all(12),
),
);
}
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
// Sync state with ApiLogger configs
bool _logReq = true;
bool _logResp = true;
bool _logOffline = true;
bool _enableComp = true;
@override
void initState() {
super.initState();
// Read live runtime configurations programmatically
final config = ApiLogger.instance.config;
_logReq = config.logRequestBody;
_logResp = config.logResponseBody;
_logOffline = config.logOfflineEvents;
_enableComp = config.enableCompression;
}
void _syncConfig() {
// Dynamic run-time config update!
final updatedConfig = ApiLogger.instance.config.copyWith(
logRequestBody: _logReq,
logResponseBody: _logResp,
logOfflineEvents: _logOffline,
enableCompression: _enableComp,
);
ApiLogger.instance.init(config: updatedConfig);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('ApiLogger runtime settings synchronized!'),
backgroundColor: const Color(0xFF2CAC5C),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Logging Settings', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
backgroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'LIVE LOGGER BEHAVIORS',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.black.withOpacity(0.04)),
),
child: Column(
children: [
_buildSwitchRow(
title: 'Log Request Body',
subtitle: 'Capture and format request parameters',
value: _logReq,
icon: Icons.upload_file_rounded,
onChanged: (val) => setState(() => _logReq = val),
),
const Divider(height: 1, indent: 16),
_buildSwitchRow(
title: 'Log Response Body',
subtitle: 'Capture and beautify API JSON responses',
value: _logResp,
icon: Icons.file_download_rounded,
onChanged: (val) => setState(() => _logResp = val),
),
const Divider(height: 1, indent: 16),
_buildSwitchRow(
title: 'Track Offline Events',
subtitle: 'Monitor failed socket connections',
value: _logOffline,
icon: Icons.cloud_off_rounded,
onChanged: (val) => setState(() => _logOffline = val),
),
const Divider(height: 1, indent: 16),
_buildSwitchRow(
title: 'Payload Compression',
subtitle: 'Compress binary payloads exceeding 10KB',
value: _enableComp,
icon: Icons.compress_rounded,
onChanged: (val) => setState(() => _enableComp = val),
),
],
),
),
const SizedBox(height: 20),
// Sync settings CTA
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2CAC5C),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _syncConfig,
child: const Text('Save runtime config updates', style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 24),
const Text(
'DEFAULT SECURITY LAWS',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.security_rounded, color: Colors.blueAccent, size: 20),
SizedBox(width: 8),
Text('PII Auto-Scrub list', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
],
),
SizedBox(height: 10),
Text(
'The following keys and values are automatically redacted with [REDACTED] before writing to the local database file:',
style: TextStyle(fontSize: 11.5, color: Colors.black54, height: 1.4),
),
SizedBox(height: 10),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
_ChipBadge('Authorization'),
_ChipBadge('Cookie'),
_ChipBadge('password'),
_ChipBadge('token'),
_ChipBadge('cvv'),
_ChipBadge('secret'),
],
),
],
),
),
const SizedBox(height: 32),
// Danger Zone Clear
OutlinedButton.icon(
icon: const Icon(Icons.delete_sweep_rounded),
label: const Text('Clear All Logs (Danger Zone)'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
await ApiLogger.instance.clearAll();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Database cleared!'), backgroundColor: Colors.redAccent),
);
},
),
],
),
),
);
}
Widget _buildSwitchRow({
required String title,
required String subtitle,
required bool value,
required IconData icon,
required ValueChanged<bool> onChanged,
}) {
return SwitchListTile(
value: value,
onChanged: onChanged,
activeColor: const Color(0xFF2CAC5C),
secondary: Icon(icon, color: const Color(0xFF64748B), size: 20),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Color(0xFF0F172A))),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 10, color: Color(0xFF64748B))),
);
}
}
class _ChipBadge extends StatelessWidget {
final String text;
const _ChipBadge(this.text);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.04),
borderRadius: BorderRadius.circular(8),
),
child: Text(text, style: const TextStyle(fontSize: 10, fontFamily: 'monospace', fontWeight: FontWeight.bold, color: Colors.black87)),
);
}
}
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key});
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)?.settings.arguments;
return Scaffold(
appBar: AppBar(
title: const Text('Sandbox Profile', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
backgroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Premium profile card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: Column(
children: [
const CircleAvatar(
radius: 36,
backgroundColor: Color(0xFF2563EB),
child: Icon(Icons.person_rounded, size: 40, color: Colors.white),
),
const SizedBox(height: 12),
const Text('Satish Parmar', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF0F172A))),
const Text('Tractal Lead Architect', style: TextStyle(fontSize: 11, color: Color(0xFF64748B))),
const SizedBox(height: 20),
const Divider(height: 1),
const SizedBox(height: 20),
_buildProfileRow('Company', 'Tractal Solutions Pvt Ltd'),
_buildProfileRow('Active ID', 'V-29e-Vivo-Local'),
_buildProfileRow('Status', 'Logged In'),
],
),
),
const SizedBox(height: 24),
const Text(
'INTERCEPTED ARGS PAYLOAD',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF64748B), letterSpacing: 1.2),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.folder_shared_rounded, color: Colors.blue, size: 20),
SizedBox(width: 8),
Text('Route Parameter Payload', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
],
),
const SizedBox(height: 10),
const Text(
'The page transition observer captured the following programmatic payload during navigation:',
style: TextStyle(fontSize: 11.5, color: Colors.black54, height: 1.4),
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.03),
borderRadius: BorderRadius.circular(8),
),
child: Text(
args?.toString() ?? 'No navigation payload was sent.',
style: const TextStyle(fontFamily: 'monospace', fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
),
),
],
),
),
const SizedBox(height: 32),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () => Navigator.pop(context),
child: const Text('Back to Dashboard', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
),
);
}
Widget _buildProfileRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF64748B))),
Text(value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF0F172A))),
],
),
);
}
}
/// Custom Developer Page showcasing the programmatic query capabilities
/// of the package using predefined methods: [ApiLogger.getApiLogs] and [ApiLogger.getNavigationLogs].
class DatabaseViewerScreen extends StatefulWidget {
const DatabaseViewerScreen({super.key});
@override
State<DatabaseViewerScreen> createState() => _DatabaseViewerScreenState();
}
class _DatabaseViewerScreenState extends State<DatabaseViewerScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
List<ApiLogEntry> _apiLogs = [];
List<NavigationLog> _navLogs = [];
bool _isLoading = true;
// Stats variables
int _totalCalls = 0;
double _successRate = 0.0;
double _avgDuration = 0.0;
int _errorCount = 0;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
_fetchLogs();
}
});
_fetchLogs();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _fetchLogs() async {
setState(() => _isLoading = true);
try {
// 1. Programmatically fetch logs using predefined ApiLogger methods
final apis = await ApiLogger.instance.getApiLogs();
final navs = await ApiLogger.instance.getNavigationLogs();
// 2. Calculate programmatic telemetry stats
_totalCalls = apis.length;
if (_totalCalls > 0) {
final successes = apis.where((e) => e.statusCode != null && e.statusCode! >= 200 && e.statusCode! < 300).length;
_successRate = (successes / _totalCalls) * 100;
final totalDuration = apis.fold<int>(0, (sum, e) => sum + e.durationMs);
_avgDuration = totalDuration / _totalCalls;
_errorCount = apis.where((e) => e.error != null || (e.statusCode != null && e.statusCode! >= 400)).length;
} else {
_successRate = 0.0;
_avgDuration = 0.0;
_errorCount = 0;
}
setState(() {
_apiLogs = apis;
_navLogs = navs;
});
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Programmatic DB Viewer', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
backgroundColor: Colors.white,
elevation: 0,
bottom: TabBar(
controller: _tabController,
labelColor: const Color(0xFF2CAC5C),
unselectedLabelColor: Colors.black54,
indicatorColor: const Color(0xFF2CAC5C),
tabs: const [
Tab(icon: Icon(Icons.swap_calls_rounded), text: 'API Logs'),
Tab(icon: Icon(Icons.map_rounded), text: 'Route Navigation'),
],
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF2CAC5C)))
: Column(
children: [
// Display statistics cards if API log tab is open
if (_tabController.index == 0) _buildStatsHeader(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildApiLogsList(),
_buildNavLogsList(),
],
),
),
],
),
);
}
Widget _buildStatsHeader() {
return Container(
padding: const EdgeInsets.all(12),
color: Colors.grey.withOpacity(0.05),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'LOG TELEMETRY SUMMARY',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.black54),
),
const SizedBox(height: 8),
Row(
children: [
_buildStatTile('Total Calls', '$_totalCalls', Icons.import_export_rounded, Colors.blue),
const SizedBox(width: 8),
_buildStatTile('Success Rate', '${_successRate.toStringAsFixed(1)}%', Icons.check_circle_rounded, const Color(0xFF2CAC5C)),
const SizedBox(width: 8),
_buildStatTile('Avg Latency', '${_avgDuration.toStringAsFixed(0)} ms', Icons.timer_rounded, Colors.orange),
const SizedBox(width: 8),
_buildStatTile('Failed', '$_errorCount', Icons.error_rounded, Colors.red),
],
),
],
),
);
}
Widget _buildStatTile(String label, String value, IconData icon, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black.withOpacity(0.04)),
),
child: Column(
children: [
Icon(icon, color: color, size: 18),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.black87)),
const SizedBox(height: 2),
Text(label, style: const TextStyle(fontSize: 9, color: Colors.black45)),
],
),
),
);
}
Widget _buildApiLogsList() {
if (_apiLogs.isEmpty) {
return _buildEmptyState('No API calls captured yet.');
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _apiLogs.length,
itemBuilder: (context, index) {
final entry = _apiLogs[index];
final isErr = entry.error != null || (entry.statusCode != null && entry.statusCode! >= 400);
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: 8),
color: isErr ? Colors.red.withOpacity(0.03) : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: isErr ? Colors.red.withOpacity(0.15) : Colors.black.withOpacity(0.06)),
),
child: ExpansionTile(
leading: Icon(
isErr ? Icons.cancel_rounded : Icons.check_circle_rounded,
color: isErr ? Colors.redAccent : const Color(0xFF2CAC5C),
size: 22,
),
title: Text(
'${entry.method} - ${entry.statusCode ?? 'ERR'}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
subtitle: Text(
entry.url,
style: const TextStyle(fontSize: 11, color: Colors.black54),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMetaRow('Logged At', entry.timestamp.toIso8601String()),
_buildMetaRow('Duration', '${entry.durationMs} ms'),
_buildMetaRow('Is Offline', '${entry.isOffline}'),
_buildMetaRow('Is Compressed', '${entry.isCompressed}'),
if (entry.error != null) ...[
const SizedBox(height: 6),
const Text('ERROR DETAILS', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, color: Colors.red)),
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.red.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
child: Text(entry.error!, style: const TextStyle(fontFamily: 'monospace', fontSize: 11, color: Colors.red)),
),
],
if (entry.requestBody != null) ...[
const SizedBox(height: 6),
const Text('REQUEST BODY', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, color: Colors.black54)),
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.grey.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
child: Text(entry.requestBody!, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
),
],
if (entry.responseBody != null) ...[
const SizedBox(height: 6),
const Text('RESPONSE BODY', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, color: Colors.black54)),
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.grey.withOpacity(0.05), borderRadius: BorderRadius.circular(8)),
child: Text(entry.responseBody!, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
),
],
],
),
)
],
),
);
},
);
}
Widget _buildNavLogsList() {
if (_navLogs.isEmpty) {
return _buildEmptyState('No navigation routes logged yet.');
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _navLogs.length,
itemBuilder: (context, index) {
final entry = _navLogs[index];
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: 8),
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.black.withOpacity(0.05)),
),
child: ListTile(
leading: const Icon(Icons.arrow_forward_rounded, color: Color(0xFF2563EB)),
title: Text(
'Route: ${entry.toRoute}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
subtitle: Text(
'From: ${entry.fromRoute ?? 'Initial Launch'} • At: ${entry.timestamp.hour}:${entry.timestamp.minute.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 11, color: Colors.black54),
),
),
);
},
);
}
Widget _buildEmptyState(String text) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.storage_rounded, size: 48, color: Colors.grey),
const SizedBox(height: 8),
Text(text, style: const TextStyle(color: Colors.grey, fontSize: 14)),
],
),
);
}
Widget _buildMetaRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 11, color: Colors.black54)),
Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.black87)),
],
),
);
}
}