flutter_user_recorder 0.2.2 copy "flutter_user_recorder: ^0.2.2" to clipboard
flutter_user_recorder: ^0.2.2 copied to clipboard

A powerful Flutter package for automatically recording and replaying user interactions on any screen or widget. Perfect for testing, automation, and user flow analysis.

flutter_user_recorder #

pub package License: MIT GitHub

A powerful Flutter package for automatically recording and replaying user interactions on any screen or widget. Perfect for testing, automation, and user flow analysis.

✨ Features #

  • 🎯 Easy Integration: Wrap your app with RecorderLayer and start recording
  • 📝 Comprehensive Recording: Records taps, text input, scrolls, navigation, drag/swipe gestures
  • 🔄 Accurate Replay: Replays interactions exactly as recorded, with timing preserved
  • 💾 Session Management: Save and manage multiple recording sessions
  • 📦 Export/Import: Export recordings as JSON for sharing or analysis
  • 🎨 Built-in Widgets: Ready-to-use widgets for common interactions
  • 🚀 Speed Control: Adjust replay speed (0.5x, 1x, 2x, 5x)
  • 🔂 Script Looping: Repeat actions seamlessly with loop functionality
  • 🎛️ Floating Control Panel: Beautiful FAB with all controls
  • 🏗️ Clean Architecture: MVVM-ready, modular, and extensible
  • 🔒 Null-Safe: Fully null-safety enabled

📦 Installation #

Add flutter_user_recorder to your pubspec.yaml:

dependencies:
  flutter_user_recorder: ^0.1.0

Then run:

flutter pub get

🚀 Quick Start #

1. Minimal wrapping #

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

final recorder = RecorderController();
final replayer = Replayer(); // sensible defaults

void main() {
  runApp(const MyApp());
}

/// Main App Widget
/// Wrap MaterialApp directly with RecorderLayer (easy to remove later)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return RecorderLayer(
      controller: recorder,
      replayer: replayer,
      child: Builder(builder: (context) {
        final navObserver = RecorderLayer.navigatorObserver(
          RecorderLayer.of(context),
        );
        return MaterialApp(
          title: 'App',
          navigatorObservers: [navObserver],
          home: HomePage(recorderController: recorder, replayer: replayer),
        );
      }),
    );
  }
}

2. go_router integration (optional) #

import 'package:go_router/go_router.dart';
import 'package:flutter_user_recorder/flutter_user_recorder.dart';

final recorder = RecorderController();
final replayer = Replayer();

// Record navigation via observer
final router = GoRouter(
  initialLocation: '/',
  observers: [RecorderNavigatorObserver(controller: recorder)],
  routes: [/* ... */],
);

// Optional: delegate navigation during replay to go_router
replayer.navigationDelegate = (route, type, args, ctx) async {
  final r = GoRouter.of(ctx);
  final useNamed = (args is Map && args['useNamed'] == true);
  switch (type) {
    case 'pop':
      if (r.canPop()) r.pop(args);
      return true;
    case 'replace':
      useNamed ? r.goNamed(route, extra: args) : r.go(route, extra: args);
      return true;
    default:
      useNamed ? r.pushNamed(route, extra: args) : r.push(route, extra: args);
      return true;
  }
};

// Wrap MaterialApp.router directly
return RecorderLayer(
  controller: recorder,
  replayer: replayer,
  child: MaterialApp.router(routerConfig: router),
);

3. Show/Hide built-in controls #

The floating control panel (RecorderFAB) is now auto-injected by default.

// Default (auto show controls)
RecorderLayer(
  controller: recorder,
  replayer: replayer,
  child: MaterialApp(...),
);

// Hide global controls (you can place RecorderFAB manually)
RecorderLayer(
  controller: recorder,
  replayer: replayer,
  showControls: false,
  child: MaterialApp(...),
);

4. Use built-in recorder widgets #

class HomePage extends StatelessWidget {
  final RecorderController recorderController;
  final Replayer replayer;

  const HomePage({
    super.key,
    required this.recorderController,
    required this.replayer,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: Column(
        children: [
          // Record text input
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: RecorderTextField(
              id: 'username_field',
              decoration: const InputDecoration(
                labelText: 'Username',
                border: OutlineInputBorder(),
              ),
            ),
          ),

          // Record button taps
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: RecorderTap(
              id: 'submit_button',
              child: ElevatedButton(
                onPressed: () {
                  print('Submitted!');
                },
                child: const Text('Submit'),
              ),
            ),
          ),

          // Record scrolls
          Expanded(
            child: RecorderScroll(
              id: 'my_list',
              child: ListView.builder(
                itemCount: 50,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text('Item $index'),
                  );
                },
              ),
            ),
          ),
        ],
      ),
      // Add floating control panel
      floatingActionButton: RecorderFAB(),
    );
  }
}

5. Control recording programmatically #

// Get the controller
final controller = RecorderLayer.of(context);

// Start recording (creates a new session)
controller.start();

// Stop recording (saves the current session)
controller.stop();

// Get current events
final events = controller.events;

// Get all sessions
final sessions = controller.sessions;

// Save all sessions to local storage
await controller.save();

// Load all sessions from local storage
await controller.load();

// Export current session as JSON
final json = controller.export();

// Export all sessions as JSON
final allSessionsJson = controller.exportAllSessions();

// Replay events
await replayer.replay(events, context: context);

// Set replay speed
replayer.setSpeed(2.0);  // 2x speed

// Set loop count
replayer.setLoopCount(5);  // Repeat 5 times

📖 Core Components #

RecorderLayer #

Wraps your widget tree and provides RecorderController to descendant widgets. Similar to BlocProvider.

RecorderLayer(
  controller: recorderController,  // Optional - created automatically if not provided
  replayer: replayer,              // Optional - created automatically if not provided
  recordNavigation: true,          // Record route changes
  recordGlobalScroll: true,        // Record full-screen scrolls
  recordDrag: true,                // Record drag/swipe gestures
  minScrollDelta: 10.0,            // Minimum scroll delta to record
  child: YourApp(),
)

Important: Make sure to add the navigator observer to your MaterialApp:

MaterialApp(
  navigatorObservers: [
    RecorderLayer.navigatorObserver(RecorderLayer.of(context)),
  ],
  // ...
)

Note: When using routes, make sure to pass recorderController and replayer to your page widgets as shown above.

RecorderController #

Manages recording state and events. Provides methods to:

  • start() - Start recording (creates a new session)
  • stop() - Stop recording (saves the current session)
  • record() - Record an event (called automatically by widgets)
  • export() - Export current session events as JSON string
  • exportAllSessions() - Export all sessions as JSON string
  • import() - Import events from JSON string
  • importSessions() - Import sessions from JSON string
  • save() - Save all sessions to local storage
  • load() - Load all sessions from local storage
  • clear() - Clear current session events
  • deleteSession(String id) - Delete a specific session
  • clearAllSessions() - Delete all sessions
  • getSession(String id) - Get a specific session

Replayer #

Replays recorded events in order, with state-based readiness and timing controls.

final replayer = Replayer(
  minDelayMs: 150,            // Minimum delay between events
  maxInterEventDelayMs: 800,  // Cap long gaps to avoid stalls
  respectTiming: true,        // Use original timing
  awaitNavigationReady: true, // Wait for route & tree readiness after navigation
  awaitTextCommit: true,      // Wait a short moment after text input
  maxConditionWaitMs: 1200,   // Max time to await readiness conditions
  onReplayStart: () => print('Started'),
  onReplayComplete: () => print('Done'),
  onEventReplayed: (event) => print('Replayed: $event'),
  onEventFailed: (event, error) => print('Failed: $error'),
);

// Replay events
await replayer.replay(events, context: context);

// Set replay speed
replayer.setSpeed(2.0);  // 2x speed

// Set loop count
replayer.setLoopCount(5);  // Repeat 5 times

// Replay single event
await replayer.replaySingleEvent(event, context: context);

Built-in Widgets #

RecorderTap

Wraps any widget with tap gesture recording.

RecorderTap(
  id: 'my_button',
  onTap: () {
    // Your tap handler
  },
  onLongPress: () {
    // Optional long press handler
  },
  recordLongPress: true,  // Record long press events
  child: ElevatedButton(
    onPressed: () {},
    child: Text('Tap me'),
  ),
)

RecorderTextField

Wraps TextField/TextFormField with text input recording.

RecorderTextField(
  id: 'my_text_field',
  controller: textController,  // Optional
  decoration: InputDecoration(labelText: 'Enter text'),
  // Recording is debounced, and a final value is captured on blur
  onChanged: (value) {
    print('Text: $value');
  },
)

RecorderScroll

Wraps scrollable widgets with scroll position recording.

RecorderScroll(
  id: 'my_list',
  minScrollDelta: 10.0,  // Minimum scroll delta to record
  recordScroll: true,     // Enable scroll recording
  child: ListView(
    children: [/* ... */],
  ),
)

Control Widgets #

RecorderFAB

A floating action button that provides a control panel for recording and replaying.

RecorderFAB(
  controller: recorderController,  // Optional - retrieved from RecorderLayer if not provided
  replayer: replayer,              // Optional - retrieved from RecorderLayer if not provided
)

Features:

  • Start/Stop recording
  • Replay current session
  • View events list
  • View sessions list
  • Speed control (0.5x, 1x, 2x, 5x)
  • Loop control (Once, 2x, 5x, 10x)

RecorderEventsList

Displays a list of recorded events for the current session.

RecorderEventsList(
  controller: recorderController,
  replayer: replayer,
)

RecorderSessionsList

Displays a list of all saved sessions.

RecorderSessionsList(
  controller: recorderController,
  replayer: replayer,
)

🎯 Advanced Usage #

Custom RecorderTarget #

Create custom recordable widgets by implementing RecorderTarget:

class MyCustomWidget extends StatefulWidget {
  final String id;

  @override
  State<MyCustomWidget> createState() => _MyCustomWidgetState();
}

class _MyCustomWidgetState extends State<MyCustomWidget>
    implements RecorderTarget {
  @override
  String get id => widget.id;

  @override
  void initState() {
    super.initState();
    RecorderRegistry().register(this);
  }

  @override
  void dispose() {
    RecorderRegistry().unregister(id);
    super.dispose();
  }

  @override
  Future<bool> perform(RecorderEventType type, dynamic value) async {
    // Perform the action based on type and value
    if (type == RecorderEventType.tap) {
      // Handle tap
      return true;
    }
    return false;
  }

  void _handleInteraction() {
    final controller = RecorderLayer.maybeOf(context);
    if (controller != null && controller.isRecording) {
      final currentRoute = RecorderLayer.currentRouteOf(context);
      controller.record(
        RecorderEventType.tap,
        id,
        null,
        route: currentRoute,  // Automatically stores route with event
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleInteraction,
      child: Container(/* ... */),
    );
  }
}

Event Model #

Events are represented by RecorderEvent:

class RecorderEvent {
  final RecorderEventType type;      // tap, textInput, scroll, navigation, etc.
  final String targetId;              // Widget ID
  final dynamic value;                // Event value (text, scroll position, etc.)
  final int timestamp;                // When it occurred (milliseconds since epoch)
  final Map<String, dynamic>? metadata;  // Optional metadata (route, arguments, etc.)
}

Session Model #

Sessions are represented by RecorderSession:

class RecorderSession {
  final String id;                    // Unique session ID
  final String name;                  // Session name
  final List<RecorderEvent> events;   // Events in this session
  final int startTimestamp;           // When recording started
  final int? stopTimestamp;           // When recording stopped
}

The package automatically records navigation events with arguments:

// Navigate with data
Navigator.pushNamed(
  context,
  '/second',
  arguments: {
    'name': 'John',
    'from': 'home',
  },
);

// The navigation event is automatically recorded with the arguments
// During replay, the arguments are passed to the route

Export/Import Events #

// Export current session to JSON
final jsonString = controller.export();
print(jsonString);

// Export all sessions to JSON
final allSessionsJson = controller.exportAllSessions();

// Import events from JSON
controller.import(jsonString);

// Import sessions from JSON
controller.importSessions(allSessionsJson);

// Save to device storage
await controller.save();

// Load from device storage
await controller.load();

📱 Example App #

See the example/ folder for a complete demo app that demonstrates:

  • Recording text input
  • Recording button taps
  • Recording scrolls
  • Recording navigation with data transfer
  • Replaying interactions
  • Managing multiple sessions
  • Speed control and looping
  • Using the floating control panel

Run the example:

cd example
flutter run

🏗️ Architecture #

The package follows a clean, modular architecture:

  • core/:
    • models.dart, registry.dart, core/index.dart
    • storage.dart (StorageService + SharedPrefsStorageService)
  • recording/:
    • recorder_controller.dart, recorder_layer.dart
    • navigation_observer.dart, navigation_tracker.dart, recording/index.dart
  • replay/:
    • replayer.dart, replay/index.dart
  • widgets/: Built-in recorder widgets and control panels

📋 Requirements #

  • Flutter >= 3.0.0
  • Dart >= 3.0.0

📦 Dependencies #

  • shared_preferences: ^2.2.2 - For persistent storage
  • go_router (optional) - For routing integration via navigationDelegate

🤝 Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 License #

This project is licensed under the MIT License - see the LICENSE file for details.

👤 Author #

Created with ❤️ for the Flutter community.

🙏 Acknowledgments #

Inspired by browser automation tools like "Action Replay" Chrome extension.

📚 Additional Resources #


Note: This package is designed for testing and automation purposes. Make sure to handle user data and privacy appropriately in your application.

2
likes
0
points
52
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter package for automatically recording and replaying user interactions on any screen or widget. Perfect for testing, automation, and user flow analysis.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, shared_preferences

More

Packages that depend on flutter_user_recorder