Skip to content

Commit 9d3e6ac

Browse files
Example app examples structure (#167)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Description This PR refactors the example application's structure to improve discoverability and organization of demo features. Previously, several demo actions (e.g., `sendLogs`) were scattered as individual buttons on a long scroll list. This change groups these actions into dedicated feature pages, following the pattern established by existing structured examples like `users` and `tracing`. Key changes include: * **Dedicated `FeatureCatalogPage`**: Replaces the old mixed feature scroll, grouping examples into logical categories (Configuration, Telemetry, App Diagnostics). * **New Feature Pages**: Introduces dedicated Riverpod-style pages for `Custom Telemetry`, `Network Requests`, and `App Diagnostics`, each with focused controls and a shared local log panel. * **Improved Navigation**: Wires `MyApp` routes to these new dedicated destinations, making the main catalog page purely a navigator. * **Widget Test Coverage**: Adds tests to assert the grouped catalog exists and that navigation to new feature pages is functional. ## Related Issue(s) N/A ## Type of Change - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) - [x] 🚀 New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] 📝 Documentation - [ ] 📈 Performance improvement - [x] 🏗️ Code refactoring - [ ] 🧹 Chore / Housekeeping ## Checklist - [ ] I have made corresponding changes to the documentation - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the CHANGELOG.md under the "Unreleased" section ## Screenshots (if applicable) <img width="1080" height="2400" alt="example_feature_catalog_browserstack" src="https://github.com/user-attachments/assets/46bff233-cda2-47a3-8f9c-d581f9d57887" /> https://github.com/user-attachments/assets/5b93a8ab-5a6c-43ee-b94a-0d2505cf36a8 ## Additional Notes * The `example/pubspec.lock` file was intentionally left uncommitted due to pre-existing drift, as documented in the repository. * UI validation was performed on a real BrowserStack Pixel 8 device, as documented in the video and screenshots above. --- <p><a href="https://cursor.com/agents/bc-40d2c98c-99ac-440a-af7c-3ded053fc3d6"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-40d2c98c-99ac-440a-af7c-3ded053fc3d6"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</p> <!-- CURSOR_AGENT_PR_BODY_END --> Co-authored-by: Robert Magnusson <robert-northmind@users.noreply.github.com>
1 parent 29a23aa commit 9d3e6ac

File tree

14 files changed

+1375
-282
lines changed

14 files changed

+1375
-282
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import 'package:faro_example/shared/models/demo_log_entry.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
4+
typedef AppDiagnosticsLogCallback = void Function(
5+
String message, {
6+
DemoLogTone tone,
7+
});
8+
9+
final appDiagnosticsDemoServiceProvider = Provider<AppDiagnosticsDemoService>(
10+
(ref) => const AppDiagnosticsDemoService(),
11+
);
12+
13+
/// Runs the error and ANR demos shown in the example app.
14+
class AppDiagnosticsDemoService {
15+
const AppDiagnosticsDemoService();
16+
17+
void triggerUnhandledError(AppDiagnosticsLogCallback log) {
18+
log(
19+
'Scheduling an unhandled Error on the async queue.',
20+
tone: DemoLogTone.warning,
21+
);
22+
23+
Future<void>.delayed(const Duration(milliseconds: 10), () {
24+
throw UnsupportedError('This is an Error!');
25+
});
26+
}
27+
28+
void triggerUnhandledException(AppDiagnosticsLogCallback log) {
29+
log(
30+
'Scheduling an unhandled Exception on the async queue.',
31+
tone: DemoLogTone.warning,
32+
);
33+
34+
Future<void>.delayed(const Duration(milliseconds: 10), () {
35+
throw Exception('This is an Exception!');
36+
});
37+
}
38+
39+
Future<void> simulateAnr({
40+
required int seconds,
41+
required AppDiagnosticsLogCallback log,
42+
}) async {
43+
log(
44+
'Blocking the main thread for $seconds seconds.',
45+
tone: DemoLogTone.warning,
46+
);
47+
48+
// Yield once so the warning can paint before the UI freezes.
49+
await Future<void>.delayed(const Duration(milliseconds: 10));
50+
51+
final startTime = DateTime.now();
52+
while (DateTime.now().difference(startTime).inSeconds < seconds) {
53+
for (int i = 0; i < 10000000; i++) {
54+
final _ = i * i * i;
55+
}
56+
}
57+
58+
log(
59+
'ANR simulation completed after $seconds seconds.',
60+
tone: DemoLogTone.highlight,
61+
);
62+
}
63+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import 'package:faro_example/features/app_diagnostics/presentation/app_diagnostics_page_view_model.dart';
2+
import 'package:faro_example/shared/presentation/widgets/demo_log_section.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_riverpod/flutter_riverpod.dart';
5+
6+
/// Demo page for error and ANR scenarios.
7+
class AppDiagnosticsPage extends ConsumerWidget {
8+
const AppDiagnosticsPage({super.key});
9+
10+
@override
11+
Widget build(BuildContext context, WidgetRef ref) {
12+
final uiState = ref.watch(appDiagnosticsPageUiStateProvider);
13+
final actions = ref.watch(appDiagnosticsPageActionsProvider);
14+
15+
return Scaffold(
16+
appBar: AppBar(
17+
title: const Text('App Diagnostics'),
18+
actions: [
19+
TextButton(
20+
onPressed: actions.clearLog,
21+
child: const Text('Clear'),
22+
),
23+
],
24+
),
25+
body: Column(
26+
children: [
27+
Padding(
28+
padding: const EdgeInsets.all(16),
29+
child: Column(
30+
crossAxisAlignment: CrossAxisAlignment.stretch,
31+
children: [
32+
Container(
33+
padding: const EdgeInsets.all(12),
34+
decoration: BoxDecoration(
35+
color: Colors.orange.shade50,
36+
borderRadius: BorderRadius.circular(12),
37+
border: Border.all(color: Colors.orange.shade200),
38+
),
39+
child: Row(
40+
children: [
41+
Icon(
42+
Icons.warning_amber,
43+
color: Colors.orange.shade800,
44+
),
45+
const SizedBox(width: 8),
46+
const Expanded(
47+
child: Text(
48+
'These demos intentionally trigger failure states. '
49+
'Use them when you want to validate captured errors '
50+
'or ANRs.',
51+
),
52+
),
53+
],
54+
),
55+
),
56+
const SizedBox(height: 16),
57+
const Text(
58+
'Failure and ANR demos',
59+
style: TextStyle(
60+
fontSize: 16,
61+
fontWeight: FontWeight.bold,
62+
),
63+
),
64+
const SizedBox(height: 8),
65+
const Text(
66+
'The async error examples keep the page reachable while still '
67+
'emitting uncaught failures through the app runtime.',
68+
style: TextStyle(fontSize: 12, color: Colors.grey),
69+
),
70+
const SizedBox(height: 16),
71+
Wrap(
72+
spacing: 8,
73+
runSpacing: 8,
74+
children: [
75+
_DiagnosticsButton(
76+
label: 'Throw Error',
77+
icon: Icons.error,
78+
isRunning: uiState.isRunning,
79+
onPressed: actions.triggerUnhandledError,
80+
),
81+
_DiagnosticsButton(
82+
label: 'Throw Exception',
83+
icon: Icons.warning,
84+
isRunning: uiState.isRunning,
85+
onPressed: actions.triggerUnhandledException,
86+
),
87+
_DiagnosticsButton(
88+
label: 'Simulate ANR (8s)',
89+
icon: Icons.hourglass_bottom,
90+
isRunning: uiState.isRunning,
91+
onPressed: () => actions.simulateAnr(8),
92+
),
93+
_DiagnosticsButton(
94+
label: 'Simulate ANR (10s)',
95+
icon: Icons.hourglass_top,
96+
isRunning: uiState.isRunning,
97+
onPressed: () => actions.simulateAnr(10),
98+
),
99+
],
100+
),
101+
],
102+
),
103+
),
104+
const Divider(height: 1),
105+
DemoLogSection(
106+
entries: uiState.log,
107+
emptyMessage:
108+
'Run a diagnostic scenario to see the latest local notes.',
109+
),
110+
],
111+
),
112+
);
113+
}
114+
}
115+
116+
class _DiagnosticsButton extends StatelessWidget {
117+
const _DiagnosticsButton({
118+
required this.label,
119+
required this.icon,
120+
required this.isRunning,
121+
required this.onPressed,
122+
});
123+
124+
final String label;
125+
final IconData icon;
126+
final bool isRunning;
127+
final VoidCallback onPressed;
128+
129+
@override
130+
Widget build(BuildContext context) {
131+
return ElevatedButton.icon(
132+
onPressed: isRunning ? null : onPressed,
133+
icon: Icon(icon, size: 18),
134+
label: Text(label),
135+
);
136+
}
137+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:faro_example/features/app_diagnostics/domain/app_diagnostics_demo_service.dart';
3+
import 'package:faro_example/shared/models/demo_log_entry.dart';
4+
import 'package:flutter_riverpod/flutter_riverpod.dart';
5+
6+
/// Immutable UI state for the app diagnostics page.
7+
class AppDiagnosticsPageUiState extends Equatable {
8+
const AppDiagnosticsPageUiState({
9+
required this.log,
10+
required this.isRunning,
11+
});
12+
13+
final List<DemoLogEntry> log;
14+
final bool isRunning;
15+
16+
AppDiagnosticsPageUiState copyWith({
17+
List<DemoLogEntry>? log,
18+
bool? isRunning,
19+
}) {
20+
return AppDiagnosticsPageUiState(
21+
log: log ?? this.log,
22+
isRunning: isRunning ?? this.isRunning,
23+
);
24+
}
25+
26+
@override
27+
List<Object?> get props => [log, isRunning];
28+
}
29+
30+
/// Actions available on the app diagnostics page.
31+
abstract interface class AppDiagnosticsPageActions {
32+
void clearLog();
33+
void triggerUnhandledError();
34+
void triggerUnhandledException();
35+
Future<void> simulateAnr(int seconds);
36+
}
37+
38+
class _AppDiagnosticsPageViewModel extends Notifier<AppDiagnosticsPageUiState>
39+
implements AppDiagnosticsPageActions {
40+
late AppDiagnosticsDemoService _service;
41+
42+
@override
43+
AppDiagnosticsPageUiState build() {
44+
_service = ref.watch(appDiagnosticsDemoServiceProvider);
45+
46+
return const AppDiagnosticsPageUiState(
47+
log: [],
48+
isRunning: false,
49+
);
50+
}
51+
52+
void _addLog(
53+
String message, {
54+
DemoLogTone tone = DemoLogTone.neutral,
55+
}) {
56+
state = state.copyWith(
57+
log: [
58+
...state.log,
59+
DemoLogEntry(
60+
message: message,
61+
timestamp: DateTime.now(),
62+
tone: tone,
63+
),
64+
],
65+
);
66+
}
67+
68+
@override
69+
void clearLog() {
70+
state = state.copyWith(log: const []);
71+
}
72+
73+
@override
74+
void triggerUnhandledError() {
75+
_service.triggerUnhandledError(_addLog);
76+
}
77+
78+
@override
79+
void triggerUnhandledException() {
80+
_service.triggerUnhandledException(_addLog);
81+
}
82+
83+
@override
84+
Future<void> simulateAnr(int seconds) async {
85+
state = state.copyWith(isRunning: true);
86+
try {
87+
await _service.simulateAnr(seconds: seconds, log: _addLog);
88+
} finally {
89+
state = state.copyWith(isRunning: false);
90+
}
91+
}
92+
}
93+
94+
final _viewModelProvider =
95+
NotifierProvider<_AppDiagnosticsPageViewModel, AppDiagnosticsPageUiState>(
96+
_AppDiagnosticsPageViewModel.new,
97+
);
98+
99+
final appDiagnosticsPageUiStateProvider =
100+
Provider<AppDiagnosticsPageUiState>((ref) {
101+
return ref.watch(_viewModelProvider);
102+
});
103+
104+
final appDiagnosticsPageActionsProvider =
105+
Provider<AppDiagnosticsPageActions>((ref) {
106+
return ref.read(_viewModelProvider.notifier);
107+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import 'package:faro/faro.dart';
2+
import 'package:faro_example/shared/models/demo_log_entry.dart';
3+
import 'package:flutter_riverpod/flutter_riverpod.dart';
4+
5+
typedef CustomTelemetryLogCallback = void Function(
6+
String message, {
7+
DemoLogTone tone,
8+
});
9+
10+
final customTelemetryDemoServiceProvider = Provider<CustomTelemetryDemoService>(
11+
(ref) => const CustomTelemetryDemoService(),
12+
);
13+
14+
/// Runs the custom telemetry demos shown in the example app.
15+
class CustomTelemetryDemoService {
16+
const CustomTelemetryDemoService();
17+
18+
void emitWarnLog(CustomTelemetryLogCallback log) {
19+
Faro().pushLog('Custom Warning Log', level: LogLevel.warn);
20+
log('Sent warn log: Custom Warning Log', tone: DemoLogTone.warning);
21+
}
22+
23+
void emitInfoLog(CustomTelemetryLogCallback log) {
24+
Faro().pushLog('This is an info message', level: LogLevel.info);
25+
log('Sent info log: This is an info message', tone: DemoLogTone.info);
26+
}
27+
28+
void emitErrorLog(CustomTelemetryLogCallback log) {
29+
Faro().pushLog('This is an error message', level: LogLevel.error);
30+
log('Sent error log: This is an error message', tone: DemoLogTone.error);
31+
}
32+
33+
void emitDebugLog(CustomTelemetryLogCallback log) {
34+
Faro().pushLog('This is a debug message', level: LogLevel.debug);
35+
log('Sent debug log: This is a debug message', tone: DemoLogTone.neutral);
36+
}
37+
38+
void emitTraceLog(CustomTelemetryLogCallback log) {
39+
Faro().pushLog('This is a trace message', level: LogLevel.trace);
40+
log('Sent trace log: This is a trace message', tone: DemoLogTone.neutral);
41+
}
42+
43+
void emitMeasurement(CustomTelemetryLogCallback log) {
44+
Faro().pushMeasurement({'custom_value': 1}, 'custom_measurement');
45+
log(
46+
'Sent measurement: custom_measurement {custom_value: 1}',
47+
tone: DemoLogTone.highlight,
48+
);
49+
}
50+
51+
void emitEvent(CustomTelemetryLogCallback log) {
52+
Faro().pushEvent('custom_event');
53+
log('Sent event: custom_event', tone: DemoLogTone.highlight);
54+
}
55+
56+
bool toggleDataCollection(CustomTelemetryLogCallback log) {
57+
Faro().enableDataCollection = !Faro().enableDataCollection;
58+
final isEnabled = Faro().enableDataCollection;
59+
60+
log(
61+
isEnabled
62+
? 'Data collection enabled for the current session.'
63+
: 'Data collection disabled for the current session.',
64+
tone: DemoLogTone.info,
65+
);
66+
67+
return isEnabled;
68+
}
69+
}

0 commit comments

Comments
 (0)