flutter_user_recorder 0.2.0
flutter_user_recorder: ^0.2.0 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 #
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
RecorderLayerand 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. 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(),
);
}
}
4. 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 stringexportAllSessions()- Export all sessions as JSON stringimport()- Import events from JSON stringimportSessions()- Import sessions from JSON stringsave()- Save all sessions to local storageload()- Load all sessions from local storageclear()- Clear current session eventsdeleteSession(String id)- Delete a specific sessionclearAllSessions()- Delete all sessionsgetSession(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
}
Navigation with Data #
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 storagego_router(optional) - For routing integration via navigationDelegate
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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.