flutter_user_recorder
flutter_user_recorder - A comprehensive Flutter package for recording and replaying user interactions. Automatically captures taps, text input, scrolls, navigation, and gestures. Perfect for automated testing, user flow analysis, and interaction replay. Features automatic persistence, session management, and a modern UI.
🎉 What's New in v0.3.0
- 🏷️ Package Renamed: Changed to
flutter_user_recorderfor better clarity and expressiveness - 📦 Breaking Change: Import path updated - see migration guide below
🎉 Previous Updates (v0.2.7)
- 🔧 Fixed go_router Detection: Fixed issue where go_router wasn't detected when
RecorderLayerwrapsMaterialApp.router. Now works reliably! - 🚀 Automatic go_router Integration: go_router integration is fully automatic - just wrap
MaterialApp.routerand it works! - 📚 Enhanced Documentation: Improved package description and step-by-step installation guide
- 📖 Better README: Clearer organization and more comprehensive examples
🎉 Previous Updates (v0.2.6)
- 🚀 Automatic go_router Integration: go_router integration is now fully automatic
- 📚 Enhanced Documentation: Improved package description and step-by-step installation guide
🎉 Previous Updates (v0.2.5)
- ✅ Fixed Persistence: Sessions now properly save and load on app restart
- 📱 Responsive UI: Dialog and bottom sheet layouts adapt to all screen sizes
- 💾 Auto-Save: All operations (save, delete, clear) are automatically persisted
🎉 Previous Updates (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
Step 1: Add dependency
Add flutter_user_recorder to your pubspec.yaml file:
dependencies:
flutter_user_recorder: ^0.3.0
Step 2: Install the package
Run this command in your terminal:
flutter pub get
Step 3: Import the package
Add this import to your Dart files:
import 'package:flutter_user_recorder/flutter_user_recorder.dart';
Step 4: Start using it!
Wrap your app with RecorderLayer and you're ready to go. See Quick Start below for examples.
🚀 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';
import 'package:go_router/go_router.dart';
// Create global instances (optional - can be created automatically)
final recorderController = RecorderController();
final replayer = Replayer();
void main() {
// That's it! No manual setup needed.
// RecorderLayer will automatically configure go_router integration.
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Create GoRouter with RecorderNavigatorObserver
final router = GoRouter(
initialLocation: '/',
observers: [RecorderNavigatorObserver(controller: recorderController)],
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => HomePage(),
),
],
);
// Wrap MaterialApp.router with RecorderLayer
// go_router integration is automatic - no manual setup needed!
return RecorderLayer(
controller: recorderController,
replayer: replayer,
recordNavigation: true,
recordGlobalScroll: true,
recordDrag: true,
showControls: true, // Set to false to hide global FAB
child: MaterialApp.router(
title: 'My App',
routerConfig: router,
),
);
}
}
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
- ✅ go_router integration is automatic - no manual setup needed!
2. go_router integration (Automatic! 🎉)
go_router integration is now fully automatic! Just wrap your MaterialApp.router with RecorderLayer and everything works:
import 'package:flutter/material.dart';
import 'package:flutter_user_recorder/flutter_user_recorder.dart';
import 'package:go_router/go_router.dart';
// Create global instances
final recorderController = RecorderController();
final replayer = Replayer();
void main() {
// That's it! No manual setup needed.
// RecorderLayer will automatically configure go_router integration.
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Create GoRouter with RecorderNavigatorObserver
final router = GoRouter(
initialLocation: '/',
observers: [RecorderNavigatorObserver(controller: recorderController)],
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => HomePage(),
routes: [
GoRoute(
path: 'second',
name: 'second',
builder: (context, state) => SecondPage(),
),
],
),
],
);
// Wrap MaterialApp.router with RecorderLayer
// go_router integration is automatic - no manual setup needed!
return RecorderLayer(
controller: recorderController,
replayer: replayer,
recordNavigation: true,
recordGlobalScroll: true,
recordDrag: true,
showControls: true, // Set to false to hide global FAB
child: MaterialApp.router(
title: 'My App',
routerConfig: router,
),
);
}
}
No manual setup needed! RecorderLayer automatically:
- ✅ Detects
MaterialApp.router - ✅ Configures
go_routernavigation for replay - ✅ Handles all navigation types (push, pop, replace, etc.)
- ✅ Works with both named routes and paths
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.