idle_activity_detector 0.1.1-dev.2 copy "idle_activity_detector: ^0.1.1-dev.2" to clipboard
idle_activity_detector: ^0.1.1-dev.2 copied to clipboard

A Flutter package for detecting user idle and activity states with comprehensive browser event detection (mouse, keyboard, scroll, touch) and configurable timeouts.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:idle_activity_detector/idle_activity_detector.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Idle Activity Detector Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MainScreen(),
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  final IdleActivityController _controller = IdleActivityController();
  int _pageIndex = 0;
  int _idleCount = 0;
  int _activeCount = 0;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleIdle() {
    setState(() => _idleCount++);
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Idle: No activity for 15 seconds'),
          duration: Duration(seconds: 2),
          backgroundColor: Colors.orange,
        ),
      );
    }
  }

  void _handleActive() => setState(() => _activeCount++);

  void _handleStateChanged(ActivityState state) {
    debugPrint(
        'Analytics Event: state=${state.name}, idle=$_idleCount, active=$_activeCount');
  }

  @override
  Widget build(BuildContext context) {
    return IdleActivityDetector(
      controller: _controller,
      config: const IdleConfig(
        idleTimeout: Duration(seconds: 15),
        detectMouse: true,
        detectMouseClicks: true,
        detectKeyboard: true,
        detectScroll: true,
        detectTouch: true,
      ),
      onIdle: _handleIdle,
      onActive: _handleActive,
      onStateChanged: _handleStateChanged,
      child: Scaffold(
        body: IndexedStack(
          index: _pageIndex,
          children: [
            HomePage(
              controller: _controller,
              idleCount: _idleCount,
              activeCount: _activeCount,
            ),
            ProfilePage(
              controller: _controller,
              idleCount: _idleCount,
              activeCount: _activeCount,
            ),
          ],
        ),
        bottomNavigationBar: NavigationBar(
          selectedIndex: _pageIndex,
          onDestinationSelected: (i) => setState(() => _pageIndex = i),
          destinations: const [
            NavigationDestination(
              icon: Icon(Icons.home_outlined),
              selectedIcon: Icon(Icons.home),
              label: 'Home',
            ),
            NavigationDestination(
              icon: Icon(Icons.person_outlined),
              selectedIcon: Icon(Icons.person),
              label: 'Profile',
            ),
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  final IdleActivityController controller;
  final int idleCount;
  final int activeCount;

  const HomePage({
    super.key,
    required this.controller,
    required this.idleCount,
    required this.activeCount,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home - Idle Detection'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            tooltip: 'Reset State',
            onPressed: () => controller.reset(),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            StatusCard(controller: controller),
            const SizedBox(height: 16),
            StatsCard(idleCount: idleCount, activeCount: activeCount),
            const SizedBox(height: 16),
            const DetectionInfoCard(),
            const SizedBox(height: 16),
            TestButtonsCard(controller: controller),
          ],
        ),
      ),
    );
  }
}

class ProfilePage extends StatelessWidget {
  final IdleActivityController controller;
  final int idleCount;
  final int activeCount;

  const ProfilePage({
    super.key,
    required this.controller,
    required this.idleCount,
    required this.activeCount,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Profile'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            const ProfileInfoCard(),
            const SizedBox(height: 16),
            StatusCard(controller: controller),
            const SizedBox(height: 16),
            StatsCard(idleCount: idleCount, activeCount: activeCount),
            const SizedBox(height: 16),
            const CrossPageInfoCard(),
          ],
        ),
      ),
    );
  }
}

class StatusCard extends StatelessWidget {
  final IdleActivityController controller;

  const StatusCard({super.key, required this.controller});

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) {
        final isIdle = controller.isIdle;
        final color = isIdle ? Colors.orange : Colors.green;
        return Card(
          color: isIdle ? Colors.orange.shade50 : Colors.green.shade50,
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                Icon(
                  isIdle
                      ? Icons.pause_circle_outline
                      : Icons.play_circle_outline,
                  size: 64,
                  color: color,
                ),
                const SizedBox(height: 12),
                Text(
                  isIdle ? 'IDLE' : 'ACTIVE',
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                    color: color,
                  ),
                ),
                const SizedBox(height: 4),
                Text(isIdle ? 'No activity detected' : 'User is active'),
              ],
            ),
          ),
        );
      },
    );
  }
}

class StatsCard extends StatelessWidget {
  final int idleCount;
  final int activeCount;

  const StatsCard({
    super.key,
    required this.idleCount,
    required this.activeCount,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Statistics', style: Theme.of(context).textTheme.titleLarge),
            const Divider(),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Idle Triggers'),
                Text(
                  '$idleCount',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.orange,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Active Triggers'),
                Text(
                  '$activeCount',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Total Changes'),
                Text(
                  '${idleCount + activeCount}',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class DetectionInfoCard extends StatelessWidget {
  const DetectionInfoCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.blue.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Active Detection Types',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: Colors.blue.shade700,
              ),
            ),
            const Divider(),
            const Text('• Mouse movement (mousemove)'),
            const Text('• Mouse clicks (mousedown, click)'),
            const Text('• Keyboard input (keydown)'),
            const Text('• Scroll events (scroll)'),
            const Text('• Touch gestures (touchstart, touchmove)'),
          ],
        ),
      ),
    );
  }
}

class TestButtonsCard extends StatelessWidget {
  final IdleActivityController controller;

  const TestButtonsCard({super.key, required this.controller});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Test Activity Detection',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                ElevatedButton.icon(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('Mouse click detected'),
                        duration: Duration(seconds: 1),
                      ),
                    );
                  },
                  icon: const Icon(Icons.touch_app),
                  label: const Text('Test Click'),
                ),
                ElevatedButton.icon(
                  onPressed: () {
                    showDialog<void>(
                      context: context,
                      builder: (c) => AlertDialog(
                        title: const Text('Keyboard Test'),
                        content: const TextField(
                          decoration: InputDecoration(labelText: 'Type here'),
                          autofocus: true,
                        ),
                        actions: [
                          TextButton(
                            onPressed: () => Navigator.pop(c),
                            child: const Text('Close'),
                          ),
                        ],
                      ),
                    );
                  },
                  icon: const Icon(Icons.keyboard),
                  label: const Text('Test Keyboard'),
                ),
                OutlinedButton.icon(
                  onPressed: () {
                    controller.reset();
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('State reset to active'),
                        duration: Duration(seconds: 1),
                      ),
                    );
                  },
                  icon: const Icon(Icons.replay),
                  label: const Text('Reset State'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Container(
              height: 100,
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey.shade300),
                borderRadius: BorderRadius.circular(8),
              ),
              child: const SingleChildScrollView(
                child: Text(
                  'Scrollable Test Area\n\n'
                  'Move mouse here, click, scroll, or touch to trigger activity detection.\n\n'
                  'Wait 15 seconds without interaction to see idle state.',
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ProfileInfoCard extends StatelessWidget {
  const ProfileInfoCard({super.key});

  @override
  Widget build(BuildContext context) {
    return const Card(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            CircleAvatar(
              radius: 50,
              child: Icon(Icons.person, size: 50),
            ),
            SizedBox(height: 16),
            Text(
              'Demo User',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            Text(
              'demo@example.com',
              style: TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

class CrossPageInfoCard extends StatelessWidget {
  const CrossPageInfoCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.green.shade50,
      child: const Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Cross-Page Detection',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            Divider(),
            Text(
              'Idle detection continues working across page navigation. '
              'Switch between Home and Profile to see how state persists.',
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
158
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for detecting user idle and activity states with comprehensive browser event detection (mouse, keyboard, scroll, touch) and configurable timeouts.

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on idle_activity_detector