Help System
Context-sensitive help system for effective user assistance
Help System Integration
The Vyuh Entity System includes a comprehensive help system that provides context-sensitive assistance throughout your application. This guide covers help content management, UI integration, and best practices for creating effective help documentation.
Help System Overview
The help system provides:
- Context-sensitive help for entities and features
- Interactive tutorials and walkthroughs
- Searchable documentation
- Video guides and tooltips
- User onboarding flows
Core Components
Help Models
class HelpContent extends EntityBase {
final String key;
final String title;
final String content;
final HelpContentType type;
final List<String> tags;
final Map<String, dynamic>? metadata;
final List<HelpSection>? sections;
final List<HelpLink>? relatedLinks;
final String? videoUrl;
final int? displayOrder;
HelpContent({
required super.id,
required super.schemaType,
required this.key,
required this.title,
required this.content,
required this.type,
this.tags = const [],
this.metadata,
this.sections,
this.relatedLinks,
this.videoUrl,
this.displayOrder,
super.createdAt,
super.layout,
super.modifiers,
});
factory HelpContent.fromJson(Map<String, dynamic> json) =>
_$HelpContentFromJson(json);
@override
Map<String, dynamic> toJson() => _$HelpContentToJson(this);
}
enum HelpContentType {
overview,
tutorial,
reference,
faq,
video,
tip,
}
class HelpSection {
final String title;
final String content;
final List<HelpSection>? subsections;
final String? anchor;
const HelpSection({
required this.title,
required this.content,
this.subsections,
this.anchor,
});
}
class HelpLink {
final String title;
final String url;
final HelpLinkType type;
final String? description;
const HelpLink({
required this.title,
required this.url,
required this.type,
this.description,
});
}
enum HelpLinkType {
internal,
external,
video,
documentation,
}HelpService
class HelpService {
final HelpRepository repository;
final Map<String, HelpContent> _cache = {};
HelpService({required this.repository});
Future<HelpContent?> getHelp(String key) async {
// Check cache
if (_cache.containsKey(key)) {
return _cache[key];
}
// Load from repository
final help = await repository.getByKey(key);
if (help != null) {
_cache[key] = help;
}
return help;
}
Future<List<HelpContent>> search(String query) async {
return repository.search(query);
}
Future<List<HelpContent>> getByTags(List<String> tags) async {
return repository.getByTags(tags);
}
Future<List<HelpContent>> getForEntity(String entityType) async {
return repository.getByTags([entityType, 'entity']);
}
Future<Map<String, List<HelpContent>>> getGroupedHelp() async {
final allHelp = await repository.getAll();
return groupBy(allHelp, (help) {
if (help.type == HelpContentType.faq) return 'FAQ';
if (help.type == HelpContentType.tutorial) return 'Tutorials';
if (help.type == HelpContentType.video) return 'Videos';
return 'Documentation';
});
}
void clearCache() {
_cache.clear();
}
}
abstract class HelpRepository {
Future<HelpContent?> getByKey(String key);
Future<List<HelpContent>> search(String query);
Future<List<HelpContent>> getByTags(List<String> tags);
Future<List<HelpContent>> getAll();
Future<void> save(HelpContent content);
Future<void> delete(String id);
}Help UI Components
HelpButton
Context-aware help button:
class HelpButton extends StatelessWidget {
final String helpKey;
final String? fallbackUrl;
final HelpDisplayMode displayMode;
const HelpButton({
required this.helpKey,
this.fallbackUrl,
this.displayMode = HelpDisplayMode.dialog,
});
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help_outline),
tooltip: 'Help',
onPressed: () => _showHelp(context),
);
}
Future<void> _showHelp(BuildContext context) async {
final helpService = context.read<HelpService>();
final help = await helpService.getHelp(helpKey);
if (help == null && fallbackUrl != null) {
// Open fallback URL
await launchUrl(Uri.parse(fallbackUrl!));
return;
}
if (help == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Help content not found')),
);
return;
}
switch (displayMode) {
case HelpDisplayMode.dialog:
showDialog(
context: context,
builder: (_) => HelpDialog(content: help),
);
break;
case HelpDisplayMode.bottomSheet:
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) => HelpBottomSheet(content: help),
);
break;
case HelpDisplayMode.page:
context.go('/help/${help.id}');
break;
case HelpDisplayMode.tooltip:
// Show as overlay tooltip
_showTooltip(context, help);
break;
}
}
void _showTooltip(BuildContext context, HelpContent help) {
final overlay = Overlay.of(context);
final renderBox = context.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
final entry = OverlayEntry(
builder: (context) => HelpTooltip(
content: help,
targetPosition: position,
targetSize: renderBox.size,
),
);
overlay.insert(entry);
// Remove after delay or on tap
Future.delayed(const Duration(seconds: 5), () {
entry.remove();
});
}
}
enum HelpDisplayMode {
dialog,
bottomSheet,
page,
tooltip,
}HelpDialog
Full-featured help dialog:
class HelpDialog extends StatefulWidget {
final HelpContent content;
const HelpDialog({required this.content});
@override
State<HelpDialog> createState() => _HelpDialogState();
}
class _HelpDialogState extends State<HelpDialog> {
String? _selectedSection;
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 800,
height: 600,
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: Row(
children: [
Icon(
_getIconForType(widget.content.type),
color: Colors.white,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.content.title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Content
Expanded(
child: Row(
children: [
// Table of contents
if (widget.content.sections != null &&
widget.content.sections!.isNotEmpty)
Container(
width: 250,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: _buildTableOfContents(),
),
// Main content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _buildContent(),
),
),
],
),
),
// Footer
if (widget.content.relatedLinks != null &&
widget.content.relatedLinks!.isNotEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: _buildRelatedLinks(),
),
],
),
),
);
}
Widget _buildTableOfContents() {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Contents',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
...widget.content.sections!.map((section) => ListTile(
title: Text(section.title),
selected: _selectedSection == section.anchor,
onTap: () {
setState(() {
_selectedSection = section.anchor;
});
// Scroll to section
_scrollToSection(section.anchor);
},
)),
],
);
}
Widget _buildContent() {
if (_selectedSection != null) {
final section = widget.content.sections?.firstWhere(
(s) => s.anchor == _selectedSection,
);
if (section != null) {
return _buildSection(section);
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Main content
MarkdownBody(
data: widget.content.content,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
onTapLink: (text, href, title) {
if (href != null) {
launchUrl(Uri.parse(href));
}
},
),
// Sections
if (widget.content.sections != null)
...widget.content.sections!.map((section) => Padding(
padding: const EdgeInsets.only(top: 32),
child: _buildSection(section),
)),
// Video
if (widget.content.videoUrl != null) ...[
const SizedBox(height: 32),
_buildVideoSection(),
],
],
);
}
Widget _buildSection(HelpSection section) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
section.title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
MarkdownBody(
data: section.content,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
),
// Subsections
if (section.subsections != null)
...section.subsections!.map((subsection) => Padding(
padding: const EdgeInsets.only(top: 16, left: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subsection.title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
MarkdownBody(
data: subsection.content,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
),
],
),
)),
],
);
}
Widget _buildVideoSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.play_circle_outline),
const SizedBox(width: 8),
Text(
'Video Tutorial',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
launchUrl(Uri.parse(widget.content.videoUrl!));
},
icon: const Icon(Icons.play_arrow),
label: const Text('Watch Video'),
),
],
),
),
);
}
Widget _buildRelatedLinks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Related Topics',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
Wrap(
spacing: 16,
children: widget.content.relatedLinks!.map((link) => TextButton.icon(
onPressed: () => _openLink(link),
icon: Icon(_getIconForLinkType(link.type)),
label: Text(link.title),
)).toList(),
),
],
);
}
void _openLink(HelpLink link) {
switch (link.type) {
case HelpLinkType.internal:
Navigator.of(context).pop();
context.go(link.url);
break;
case HelpLinkType.external:
case HelpLinkType.documentation:
case HelpLinkType.video:
launchUrl(Uri.parse(link.url));
break;
}
}
IconData _getIconForType(HelpContentType type) {
switch (type) {
case HelpContentType.overview:
return Icons.info_outline;
case HelpContentType.tutorial:
return Icons.school;
case HelpContentType.reference:
return Icons.book;
case HelpContentType.faq:
return Icons.question_answer;
case HelpContentType.video:
return Icons.play_circle_outline;
case HelpContentType.tip:
return Icons.lightbulb_outline;
}
}
IconData _getIconForLinkType(HelpLinkType type) {
switch (type) {
case HelpLinkType.internal:
return Icons.arrow_forward;
case HelpLinkType.external:
return Icons.open_in_new;
case HelpLinkType.video:
return Icons.play_circle_outline;
case HelpLinkType.documentation:
return Icons.description;
}
}
void _scrollToSection(String? anchor) {
// Implementation depends on your scrolling setup
}
}Interactive Tutorials
class InteractiveTutorial {
final String id;
final String title;
final List<TutorialStep> steps;
final Map<String, dynamic>? metadata;
const InteractiveTutorial({
required this.id,
required this.title,
required this.steps,
this.metadata,
});
}
class TutorialStep {
final String title;
final String description;
final String? targetKey;
final TutorialAction? action;
final TutorialStepPosition position;
final bool skippable;
const TutorialStep({
required this.title,
required this.description,
this.targetKey,
this.action,
this.position = TutorialStepPosition.center,
this.skippable = true,
});
}
enum TutorialStepPosition {
center,
top,
bottom,
left,
right,
target,
}
class TutorialAction {
final String label;
final VoidCallback onTap;
const TutorialAction({
required this.label,
required this.onTap,
});
}
class TutorialOverlay extends StatefulWidget {
final InteractiveTutorial tutorial;
final VoidCallback onComplete;
final VoidCallback onSkip;
const TutorialOverlay({
required this.tutorial,
required this.onComplete,
required this.onSkip,
});
@override
State<TutorialOverlay> createState() => _TutorialOverlayState();
}
class _TutorialOverlayState extends State<TutorialOverlay> {
int _currentStep = 0;
@override
Widget build(BuildContext context) {
final step = widget.tutorial.steps[_currentStep];
return Stack(
children: [
// Dark overlay
Container(
color: Colors.black54,
),
// Spotlight on target
if (step.targetKey != null)
_buildSpotlight(step.targetKey!),
// Tutorial content
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child: _buildStepContent(step),
),
],
);
}
Widget _buildSpotlight(String targetKey) {
// Find target widget and cut hole in overlay
return CustomPaint(
painter: SpotlightPainter(
targetKey: targetKey,
),
);
}
Widget _buildStepContent(TutorialStep step) {
return Center(
child: Card(
margin: const EdgeInsets.all(32),
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
step.title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
step.description,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
if (step.action != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: step.action!.onTap,
child: Text(step.action!.label),
),
],
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (step.skippable)
TextButton(
onPressed: widget.onSkip,
child: const Text('Skip Tutorial'),
)
else
const SizedBox(),
Row(
children: [
if (_currentStep > 0)
TextButton(
onPressed: _previousStep,
child: const Text('Previous'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _nextStep,
child: Text(
_currentStep == widget.tutorial.steps.length - 1
? 'Complete'
: 'Next',
),
),
],
),
],
),
const SizedBox(height: 16),
// Progress indicator
LinearProgressIndicator(
value: (_currentStep + 1) / widget.tutorial.steps.length,
),
],
),
),
),
);
}
void _previousStep() {
setState(() {
_currentStep = (_currentStep - 1).clamp(0, widget.tutorial.steps.length - 1);
});
}
void _nextStep() {
if (_currentStep == widget.tutorial.steps.length - 1) {
widget.onComplete();
} else {
setState(() {
_currentStep++;
});
}
}
}Entity Help Integration
Automatic Help Registration
class EntityHelpIntegration {
static void registerEntityHelp<T extends EntityBase>(
EntityConfiguration<T> config,
HelpService helpService,
) {
final metadata = config.metadata;
// Use getHelp function if provided
if (metadata.getHelp != null) {
// Dynamic help content
return;
}
// Generate default help content
final helpContent = HelpContent(
id: 'entity_${metadata.identifier}',
schemaType: 'help',
key: 'entity.${metadata.identifier}',
title: '${metadata.pluralName} Overview',
content: _generateDefaultContent(metadata),
type: HelpContentType.overview,
tags: [
metadata.identifier,
'entity',
metadata.category ?? 'general',
],
sections: [
HelpSection(
title: 'What are ${metadata.pluralName}?',
content: metadata.description ??
'Manage ${metadata.pluralName.toLowerCase()} in your system.',
anchor: 'overview',
),
HelpSection(
title: 'Common Actions',
content: _generateActionsContent(config),
anchor: 'actions',
),
HelpSection(
title: 'Permissions',
content: _generatePermissionsContent(metadata),
anchor: 'permissions',
),
],
relatedLinks: [
HelpLink(
title: 'View All ${metadata.pluralName}',
url: metadata.route.list(null),
type: HelpLinkType.internal,
),
HelpLink(
title: 'Create ${metadata.name}',
url: metadata.route.create(null),
type: HelpLinkType.internal,
),
],
createdAt: DateTime.now(),
);
// Save to help service
helpService.repository.save(helpContent);
}
static String _generateDefaultContent(EntityMetadata metadata) {
return '''
# ${metadata.pluralName}
${metadata.description ?? 'Manage ${metadata.pluralName.toLowerCase()} in your system.'}
## Getting Started
To work with ${metadata.pluralName.toLowerCase()}, you can:
1. **View all ${metadata.pluralName.toLowerCase()}** - Browse and search existing records
2. **Create new ${metadata.name.toLowerCase()}** - Add new records to the system
3. **Edit existing records** - Update information as needed
4. **Delete records** - Remove records that are no longer needed
## Navigation
You can access ${metadata.pluralName} from:
- The main menu under "${metadata.category ?? 'Entities'}"
- The command palette (Cmd/Ctrl+K) by searching for "${metadata.name}"
- Direct URL: `/${metadata.identifier}`
''';
}
static String _generateActionsContent<T>(EntityConfiguration<T> config) {
final actions = <String>[];
actions.add('### Standard Actions\n');
actions.add('- **Create**: Add new ${config.metadata.name.toLowerCase()}');
actions.add('- **Edit**: Modify existing records');
actions.add('- **Delete**: Remove records');
actions.add('- **Export**: Download data in various formats');
if (config.actions?.list != null) {
actions.add('\n### Custom Actions\n');
for (final action in config.actions!.list!) {
actions.add('- **${action.label}**: Custom action for this entity type');
}
}
return actions.join('\n');
}
static String _generatePermissionsContent(EntityMetadata metadata) {
return '''
The following permissions control access to ${metadata.pluralName}:
- `${metadata.identifier}.create` - Create new records
- `${metadata.identifier}.read` - View records
- `${metadata.identifier}.update` - Edit existing records
- `${metadata.identifier}.delete` - Delete records
- `${metadata.identifier}.export` - Export data
- `${metadata.identifier}.import` - Import data
Contact your administrator if you need additional permissions.
''';
}
}Context-Sensitive Help
class ContextualHelpProvider extends InheritedWidget {
final Map<String, String> helpKeys;
const ContextualHelpProvider({
required this.helpKeys,
required super.child,
});
static String? getHelpKey(BuildContext context, String widgetKey) {
final provider = context.dependOnInheritedWidgetOfExactType<ContextualHelpProvider>();
return provider?.helpKeys[widgetKey];
}
@override
bool updateShouldNotify(ContextualHelpProvider oldWidget) {
return helpKeys != oldWidget.helpKeys;
}
}
// Usage in entity views
class EntityDetailView<T extends EntityBase> extends StatelessWidget {
final EntityConfiguration<T> configuration;
final String entityId;
@override
Widget build(BuildContext context) {
return ContextualHelpProvider(
helpKeys: {
'detail_view': 'entity.${configuration.metadata.identifier}.detail',
'edit_button': 'entity.${configuration.metadata.identifier}.edit',
'delete_button': 'entity.${configuration.metadata.identifier}.delete',
},
child: Scaffold(
appBar: AppBar(
title: Text(configuration.metadata.name),
actions: [
HelpButton(
helpKey: ContextualHelpProvider.getHelpKey(
context,
'detail_view',
) ?? 'entity.detail',
),
],
),
body: // ... entity detail content
),
);
}
}Help Search
Searchable Help Center
class HelpCenterPage extends StatefulWidget {
@override
State<HelpCenterPage> createState() => _HelpCenterPageState();
}
class _HelpCenterPageState extends State<HelpCenterPage> {
final _searchController = TextEditingController();
List<HelpContent> _searchResults = [];
Map<String, List<HelpContent>>? _groupedHelp;
@override
void initState() {
super.initState();
_loadHelp();
}
Future<void> _loadHelp() async {
final helpService = context.read<HelpService>();
final grouped = await helpService.getGroupedHelp();
setState(() {
_groupedHelp = grouped;
});
}
Future<void> _search(String query) async {
if (query.isEmpty) {
setState(() {
_searchResults = [];
});
return;
}
final helpService = context.read<HelpService>();
final results = await helpService.search(query);
setState(() {
_searchResults = results;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Help Center'),
),
body: Column(
children: [
// Search bar
Container(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search help topics...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
),
onChanged: _search,
),
),
// Content
Expanded(
child: _searchController.text.isNotEmpty
? _buildSearchResults()
: _buildGroupedHelp(),
),
],
),
);
}
Widget _buildSearchResults() {
if (_searchResults.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Theme.of(context).disabledColor,
),
const SizedBox(height: 16),
Text(
'No results found',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Try different keywords',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final help = _searchResults[index];
return _HelpCard(
content: help,
onTap: () => _openHelp(help),
);
},
);
}
Widget _buildGroupedHelp() {
if (_groupedHelp == null) {
return const Center(child: CircularProgressIndicator());
}
return ListView(
padding: const EdgeInsets.all(16),
children: _groupedHelp!.entries.map((entry) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
entry.key,
style: Theme.of(context).textTheme.titleLarge,
),
),
...entry.value.map((help) => _HelpCard(
content: help,
onTap: () => _openHelp(help),
)),
const SizedBox(height: 24),
],
);
}).toList(),
);
}
void _openHelp(HelpContent help) {
showDialog(
context: context,
builder: (_) => HelpDialog(content: help),
);
}
}
class _HelpCard extends StatelessWidget {
final HelpContent content;
final VoidCallback onTap;
const _HelpCard({
required this.content,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: Icon(_getIconForType(content.type)),
title: Text(content.title),
subtitle: Text(
content.content.split('\n').first,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.arrow_forward),
onTap: onTap,
),
);
}
IconData _getIconForType(HelpContentType type) {
// Same as in HelpDialog
}
}Onboarding System
User Onboarding Flow
class OnboardingService {
final SharedPreferences prefs;
final TutorialService tutorialService;
OnboardingService({
required this.prefs,
required this.tutorialService,
});
Future<bool> shouldShowOnboarding() async {
return !prefs.getBool('onboarding_completed') ?? true;
}
Future<void> startOnboarding(BuildContext context) async {
final tutorial = InteractiveTutorial(
id: 'onboarding',
title: 'Welcome to ${AppConfig.appName}',
steps: [
TutorialStep(
title: 'Welcome!',
description: 'Let\'s take a quick tour to get you started.',
position: TutorialStepPosition.center,
),
TutorialStep(
title: 'Navigation',
description: 'Use the sidebar to navigate between different sections.',
targetKey: 'navigation_drawer',
position: TutorialStepPosition.target,
),
TutorialStep(
title: 'Quick Search',
description: 'Press Cmd/Ctrl+K to quickly search and navigate.',
action: TutorialAction(
label: 'Try It',
onTap: () => CommandPaletteService.show(context),
),
),
TutorialStep(
title: 'Create Your First Entity',
description: 'Click the + button to create your first record.',
targetKey: 'create_button',
position: TutorialStepPosition.target,
),
TutorialStep(
title: 'Get Help',
description: 'Click the help icon anytime for context-sensitive help.',
targetKey: 'help_button',
position: TutorialStepPosition.target,
),
TutorialStep(
title: 'You\'re Ready!',
description: 'That\'s all you need to get started. Enjoy!',
position: TutorialStepPosition.center,
skippable: false,
),
],
);
tutorialService.start(
tutorial,
onComplete: () async {
await prefs.setBool('onboarding_completed', true);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Welcome aboard! 🎉'),
),
);
}
},
);
}
Future<void> resetOnboarding() async {
await prefs.remove('onboarding_completed');
}
}Best Practices
- Keep Content Updated - Regularly review and update help content
- Use Clear Language - Write for your target audience
- Include Examples - Show, don't just tell
- Provide Context - Help should be relevant to current task
- Make It Searchable - Use good keywords and tags
- Track Usage - Monitor which help topics are most viewed
- Gather Feedback - Allow users to rate help content
Next: Best Practices - Recommended patterns and guidelines