flutter_user_recorder 0.2.5
flutter_user_recorder: ^0.2.5 copied to clipboard
Record and replay user interactions in Flutter apps. Automatically captures taps, text input, scrolls, navigation, and gestures. Perfect for testing, automation, and user flow analysis. Features autom [...]
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.
🎉 What's New in v0.2.4 #
- 🎯 Automatic Persistence: Sessions are automatically saved and persist across app restarts - no data loss!
- 🏷️ Session Naming: Give meaningful names to your recording sessions for better organization
- 🎨 Modern UI: Beautiful glassmorphism design with responsive layouts that work on all screen sizes
- 🔄 Smart Replay: Replay automatically navigates to the initial screen before executing events
- 🏗️ Clean Architecture: Refactored with Clean Architecture and BLoC pattern for better maintainability
- 💾 Auto-Save: All changes (stop recording, delete session) are automatically persisted
✨ 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, starting from the initial screen
- 💾 Automatic Persistence: Sessions are automatically saved to local storage and persist across app restarts
- 🏷️ Session Naming: Give meaningful names to your recording sessions
- 📦 Export/Import: Export recordings as JSON for sharing or analysis
- 🎨 Modern UI: Beautiful glassmorphism design with responsive layouts
- 🎛️ 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: Built with Clean Architecture and BLoC pattern
- 🔒 Null-Safe: Fully null-safety enabled
📦 Installation #
Add flutter_user_recorder to your pubspec.yaml:
dependencies:
flutter_user_recorder: ^0.2.4
Then run:
flutter pub get
🚀 Quick Start #
1. Minimal Setup (Recommended) #
The simplest way to get started:
import 'package:flutter/material.dart';
import 'package:flutter_user_recorder/flutter_user_recorder.dart';
// Create global instances (optional - can be created automatically)
final recorderController = RecorderController();
final replayer = Replayer();
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return RecorderLayer(
controller: recorderController,
replayer: replayer,
recordNavigation: true,
recordGlobalScroll: true,
recordDrag: true,
showControls: false, // Set to true to show global FAB, or add RecorderFAB manually
child: MaterialApp.router(
title: 'My App',
routerConfig: buildRouter(recorderController, replayer),
),
);
}
}
Key Points:
- ✅ Sessions are automatically saved to local storage when you stop recording
- ✅ Sessions are automatically loaded when the app starts (no data loss!)
- ✅ You can name your sessions when starting a recording
- ✅ The floating control panel (RecorderFAB) provides all controls
- ✅ Modern, responsive UI with glassmorphism design
- ✅ Replay automatically starts from the initial screen of the session
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 with optional session name
controller.start(sessionName: 'Login Flow Test');
// Stop recording (automatically saves to local storage)
controller.stop();
// Get current events
final events = controller.events;
// Get all sessions (automatically loaded from storage)
final sessions = controller.sessions;
// Sessions are automatically saved, but you can manually save/load:
await controller.save(); // Manually save (usually not needed)
await controller.load(); // Manually reload (usually not needed)
// Export current session as JSON
final json = controller.export();
// Export all sessions as JSON
final allSessionsJson = controller.exportAllSessions();
// Replay events (starts from initial screen automatically)
await replayer.replay(events, context: context);
// Set replay speed
replayer.setSpeed(2.0); // 2x speed
// Set loop count
replayer.setLoopCount(5); // Repeat 5 times
// Delete a session (automatically saves changes)
controller.deleteSession(sessionId);
// Delete all sessions (automatically saves changes)
controller.clearAllSessions();
Important Notes:
- ✅ Sessions are automatically saved when you stop recording
- ✅ Sessions are automatically loaded when the app starts
- ✅ All changes (delete, clear) are automatically persisted
- ✅ No need to manually call
save()orload()in most cases
📖 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({String? sessionName})- Start recording with optional session name (creates a new session)stop()- Stop recording (automatically saves the current session to local storage)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()- Manually save all sessions to local storage (usually not needed - auto-save is enabled)load()- Manually load all sessions from local storage (usually not needed - auto-load on init)clear()- Clear current session eventsdeleteSession(String id)- Delete a specific session (automatically saves changes)clearAllSessions()- Delete all sessions (automatically saves changes)getSession(String id)- Get a specific session
Auto-Persistence:
- Sessions are automatically saved when you stop recording
- Sessions are automatically loaded when the app starts
- All changes (delete, clear) are automatically persisted
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: [/* ... */],
),
)
RecorderTabBar
Record tab selections as discrete events (no need to record swipes).
final tabController = TabController(length: 3, vsync: this);
RecorderTabBar(
id: 'main_tabs',
controller: tabController,
tabs: const [
Tab(text: 'Home'),
Tab(text: 'Feed'),
Tab(text: 'Profile'),
],
)
RecorderPageView
Record page changes with a PageController, replay animates to the recorded page.
final pageController = PageController();
RecorderPageView(
id: 'main_pager',
controller: pageController,
children: const [
HomeView(),
FeedView(),
ProfileView(),
],
)
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 (starts from initial screen)
- ✅ Managing multiple sessions with names
- ✅ Automatic persistence (sessions survive app restarts)
- ✅ Speed control and looping
- ✅ Modern UI with glassmorphism design
- ✅ Using the floating control panel
Run the example:
cd example
flutter run
The example app shows:
- How to integrate with
go_router - How to use the built-in widgets (
RecorderTap,RecorderTextField,RecorderScroll) - How sessions are automatically saved and loaded
- How to name sessions when starting a recording
- How replay automatically navigates to the initial screen
🏗️ Architecture #
The package follows Clean Architecture with BLoC pattern:
Domain Layer #
entities/:RecorderEventEntity,RecorderSessionEntityrepositories/:RecorderRepositoryinterfaceuse_cases/: Business logic (StartRecording, StopRecording, RecordEvent, ReplayEvents)
Data Layer #
repositories/:RecorderRepositoryImpl- implements repository with SharedPreferences storagemappers/: Converts between entities and JSONuse_cases/:ReplayEventsUseCaseImpl- replay implementation
Presentation Layer #
bloc/:RecorderBloc- manages state with BLoC patterncompatibility/: Wrappers for backward compatibility (RecorderController,Replayer)
Core Components #
core/: Storage abstraction, registryrecording/: Navigation tracking, event recordingreplay/: Event replay logicwidgets/: UI components (RecorderFAB, RecorderSessionsList, etc.)
Key Features:
- ✅ Automatic persistence using SharedPreferences
- ✅ Clean separation of concerns
- ✅ Backward compatible API
- ✅ Easy to test and extend
📋 Requirements #
- Flutter >= 3.0.0
- Dart >= 3.0.0
📦 Dependencies #
shared_preferences: ^2.2.2- For automatic persistent storageflutter_bloc: ^8.1.6- For state managementequatable: ^2.0.5- For value equalitygo_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.