api_network_logger 1.0.0 copy "api_network_logger: ^1.0.0" to clipboard
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)),
        ],
      ),
    );
  }
}
2
likes
0
points
151
downloads

Publisher

unverified uploader

Weekly Downloads

A production-ready, publishable Flutter package that transparently logs and stores API calls, offline events, and navigation routes.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

dio, flutter, http, meta, path, path_provider, sqflite

More

Packages that depend on api_network_logger